docker-build-push / build-push (push) Successful in 4s
Mastodon has no RBL/DNSBL support — nothing in it consults an outside list when deciding whether to accept a signup, confirmed against v4.7.1 (the only outbound lookup in that path is EmailMxValidator's MX resolution, which feeds the local email_domain_blocks table). So a shared blocklist has to be imported rather than queried live, the same way Garden Fence and other community lists propagate. These are the pieces that make ours importable. - ?format=txt on both per-list endpoints: one entry per line under a '#' comment header, the form blocklists are traded in and the form a cron job can consume without a JSON parser. The combined /api/blocks stays JSON-only — a text rendering would mix CIDRs with domains in one file, defeating the point. An unrecognised ?format is a 400 rather than a silent fallback to JSON, and the enabled-gate still runs first so a disabled deployment 404s either way. - Access-Control-Allow-Origin: * on blocklist responses, so browser-side admin tooling can read them. The data is already public. - bin/yttrx-blocklist-import: standard-library-only importer another admin can run against their own instance. Dry run unless --apply; 422 from Mastodon counts as "already blocked", not an error. The importer defaults IP entries to sign_up_requires_approval rather than the sign_up_block yttrx uses itself. The entries are whole ASN routes, which suits yttrx because a flagged signup here is nearly always datacenter/VPN/proxy space — that calibration isn't transferable, so an importer gets the reviewable severity by default and has to opt into the hard one.
257 lines
12 KiB
Bash
257 lines
12 KiB
Bash
# Copy to .env and fill in. Do NOT commit .env.
|
|
|
|
# HMAC secret shown when you create the webhook in Mastodon
|
|
# (Administration -> Webhooks). Used to verify X-Hub-Signature.
|
|
WEBHOOK_SECRET=
|
|
|
|
# Access token for the bot account that sends the welcome DMs.
|
|
# Create a bot account on yttrx, then Preferences -> Development ->
|
|
# New application with scope: write:statuses (copy "Your access token").
|
|
BOT_ACCESS_TOKEN=
|
|
|
|
# Base URL of the Mastodon instance (no trailing slash).
|
|
MASTODON_BASE_URL=https://yttrx.com
|
|
|
|
# Welcome message template. {acct} is replaced with the new user's handle.
|
|
# The bot mentions @{acct} so a "direct" status reaches them.
|
|
WELCOME_MESSAGE=👋 Welcome to yttrx, @{acct}! Glad to have you here. If you have any questions, check out https://help.yttrx.com or just reply to this message.
|
|
|
|
# Only welcome accounts local to this instance (recommended: true).
|
|
LOCAL_ONLY=true
|
|
|
|
# Path to the sqlite dedup store inside the container (matches the volume).
|
|
DB_PATH=/data/welcomed.db
|
|
|
|
# --- Abuse-bot (report.created handling) -----------------------------------
|
|
# Access token for a DEDICATED MODERATOR bot account. That account must hold a
|
|
# role with the "Manage Users" + "Manage Reports" permissions, and the app
|
|
# token must request these scopes:
|
|
# admin:write:accounts admin:read:reports read:statuses write:statuses
|
|
# Leave blank to disable report handling entirely.
|
|
ABUSE_BOT_TOKEN=
|
|
|
|
# Master switch for report handling (also off if ABUSE_BOT_TOKEN is blank).
|
|
ABUSE_ENABLED=true
|
|
|
|
# Rollout safety: when true, log + DM what WOULD happen but take no action.
|
|
# Recommended for the first days in production; flip to false once happy.
|
|
ABUSE_DRY_RUN=true
|
|
|
|
# Moderation action: "silence" (reversible, agreed default) or "suspend".
|
|
ABUSE_ACTION=silence
|
|
|
|
# Only auto-act on accounts local to yttrx.
|
|
ABUSE_LOCAL_ONLY=true
|
|
|
|
# Account tiers (in days):
|
|
# young = oldest post newer than ABUSE_YOUNG_MAX_DAYS
|
|
# dormant = newest post older than ABUSE_DORMANT_MIN_DAYS
|
|
ABUSE_YOUNG_MAX_DAYS=30
|
|
ABUSE_DORMANT_MIN_DAYS=30
|
|
|
|
# Required number of DISTINCT reporter accounts (open reports) to auto-act:
|
|
# young / dormant / no-posts -> ABUSE_SOURCES_NEWDORMANT
|
|
# normally-active -> ABUSE_SOURCES_ACTIVE
|
|
ABUSE_SOURCES_NEWDORMANT=2
|
|
ABUSE_SOURCES_ACTIVE=3
|
|
|
|
# Safety cap on the backward status scan (40 statuses per page).
|
|
ABUSE_MAX_STATUS_PAGES=5
|
|
|
|
# Automatically never auto-act on accounts holding a staff role
|
|
# (Admin/Owner/Moderator), read from the report payload. Keep this true.
|
|
ABUSE_SKIP_PRIVILEGED=true
|
|
|
|
# Extra comma-separated handles (acct, no leading @) never to auto-act on —
|
|
# a backstop on top of ABUSE_SKIP_PRIVILEGED, and for non-staff special
|
|
# accounts. e.g. ABUSE_ALLOWLIST=waffles,tommertron,davis,bot
|
|
ABUSE_ALLOWLIST=
|
|
|
|
# Handle (acct, no @) to DM with a review summary after an auto-action.
|
|
# Leave blank to disable the DM. The DM is sent from the moderator bot.
|
|
MOD_ALERT_ACCT=
|
|
|
|
# Help/appeals page the silenced user is pointed to.
|
|
ABUSE_HELP_URL=https://welcome.yttrx.com/posts/account-limited/
|
|
|
|
# DM sent to the silenced user ({acct} + {help_url} substituted; must mention
|
|
# @{acct}). Leave blank to disable the user DM.
|
|
ABUSE_USER_DM=Hi @{acct} — your yttrx account has been temporarily limited while we review some reports. This is reversible and a moderator will take a look. Here's what happened and how to appeal: {help_url}
|
|
|
|
# --- IP-based signup scrutiny -----------------------------------------------
|
|
# Classifies each new signup's IP (Admin::Account.ip, delivered free on the
|
|
# account.created webhook) via ipapi.is, and flags it if ipapi.is reports it
|
|
# as any of datacenter, vpn, proxy, tor, or an independently-scored abuser.
|
|
# Master switch:
|
|
IP_SCRUTINY_ENABLED=true
|
|
|
|
# Optional ipapi.is API key for higher rate limits. Anonymous (blank) is
|
|
# capped at 1,000 req/day, which comfortably covers yttrx's signup volume;
|
|
# get a key at https://ipapi.is/ only if you expect to exceed that.
|
|
IP_SCRUTINY_IPAPI_KEY=
|
|
|
|
# Rollout safety: when true, classify + DM a moderator but take NO action
|
|
# (no held welcome, no ip_blocks write). Recommended for the first days in
|
|
# production; flip to false once you've reviewed the false-positive rate.
|
|
IP_SCRUTINY_DRY_RUN=true
|
|
|
|
# Hold the welcome DM for a flagged signup until account.approved fires
|
|
# (a human clears the existing approval-required registration gate), instead
|
|
# of welcoming immediately like every other signup.
|
|
IP_SCRUTINY_HOLD_WELCOME=true
|
|
|
|
# Distinct-reporter threshold used instead of the tier's usual
|
|
# ABUSE_SOURCES_* threshold (whichever is lower) when the reported account's
|
|
# signup IP was flagged.
|
|
IP_SCRUTINY_ABUSE_THRESHOLD=1
|
|
|
|
# Auto-register a flagged signup's network into Mastodon's native
|
|
# Admin::IpBlock — ipapi.is's asn.route CIDR when it's a valid network that
|
|
# actually contains the signup IP, else just that one /32 or /128. Blocking
|
|
# the whole network matters here because a flagged signup is almost always
|
|
# datacenter/VPN/proxy space, where a repeat bad actor is far more likely to
|
|
# come back from a different address in the same block than the exact one.
|
|
# Requires the ABUSE_BOT_TOKEN to carry the admin:write:ip_blocks scope (see
|
|
# CLAUDE.md's moderator-token-gotcha section) — without it this 403s and is
|
|
# logged as an error, but nothing else in the bot is affected.
|
|
IP_SCRUTINY_AUTO_IPBLOCK=true
|
|
|
|
# Severity applied to auto-registered blocks: sign_up_requires_approval (soft,
|
|
# sends flagged range's future signups to the moderator queue), sign_up_block
|
|
# (hard — flatly rejects future signups from the range at registration, no
|
|
# queue, cannot appeal via this bot), or no_access (blocks all access, not
|
|
# just signups).
|
|
IP_SCRUTINY_IPBLOCK_SEVERITY=sign_up_block
|
|
|
|
# Strong IP-reputation signals — datacenter, proxy, tor, or an independently-
|
|
# scored abuser, but NOT vpn alone (which also flags plenty of privacy-
|
|
# conscious real users and is a weaker signal on its own). A signup flagged
|
|
# with any of these is suspended immediately at signup time, regardless of
|
|
# the email-domain signal, instead of just the held-welcome/auto-ipblock/
|
|
# lowered-threshold treatment every other flagged signup gets. Master switch:
|
|
IP_SCRUTINY_STRONG_SUSPEND_ENABLED=true
|
|
|
|
# Moderation action taken immediately on a strong IP signal: "suspend"
|
|
# (default) or "silence".
|
|
IP_SCRUTINY_STRONG_SUSPEND_ACTION=suspend
|
|
|
|
# Rollout safety switch — ships "true" (dry-run) here, same reasoning as
|
|
# SUSPICIOUS_COMBINED_DRY_RUN, since this is a brand-new action path. Flip to
|
|
# "false" once the moderator DMs look right.
|
|
IP_SCRUTINY_STRONG_SUSPEND_DRY_RUN=true
|
|
|
|
# check-mail.org disposable/high-risk email domain scrutiny (roadmap item B,
|
|
# anti-abuse.md). Domain-only query (never the full email) against
|
|
# POST https://api.check-mail.org/v2/, Authorization: Bearer <key>.
|
|
# Free tier: 1,000 req/month.
|
|
CHECK_MAIL_ENABLED=true
|
|
CHECK_MAIL_API_KEY=
|
|
|
|
# Log/DM only — no held welcome, no email_domain_block write, no
|
|
# report-triggered suspend. Keep true for rollout until you've watched the
|
|
# false-positive rate for a while (same role as IP_SCRUTINY_DRY_RUN /
|
|
# ABUSE_DRY_RUN).
|
|
CHECK_MAIL_DRY_RUN=true
|
|
|
|
# A domain is flagged if check-mail.org marks it is_disposable OR its risk
|
|
# score (0-100) is at or above this threshold.
|
|
CHECK_MAIL_RISK_THRESHOLD=80
|
|
|
|
# Hold the welcome DM for a flagged signup until account.approved fires, same
|
|
# semantics as IP_SCRUTINY_HOLD_WELCOME.
|
|
CHECK_MAIL_HOLD_WELCOME=true
|
|
|
|
# Auto-register a flagged signup's email domain into Mastodon's native
|
|
# Admin::EmailDomainBlock. Requires ABUSE_BOT_TOKEN to carry the
|
|
# admin:write:email_domain_blocks scope (see CLAUDE.md) — without it this
|
|
# 403s and is logged as an error, but nothing else in the bot is affected.
|
|
# Also gates the report-triggered suspend+block override in handle_report().
|
|
CHECK_MAIL_AUTO_DOMAIN_BLOCK=true
|
|
|
|
# --- Suspicious-signup sweep (app/suspicious_sweep.py, run on a schedule) ---
|
|
# Watches every account flagged by EITHER IP-scrutiny or email-domain
|
|
# scrutiny at signup. Once the account goes live (account.created if open/
|
|
# auto-approved, account.approved if this instance requires moderator
|
|
# approval — same dual handling the welcome flow already uses), a baseline
|
|
# post/follow count is recorded. A scheduled sweep (cron on admin.yttrx.com,
|
|
# `docker exec yttrx-welcomebot python -m app.suspicious_sweep`) then acts on
|
|
# any watch past SUSPICIOUS_GRACE_HOURS with zero new posts AND zero new
|
|
# follows since that baseline. Master switch:
|
|
SUSPICIOUS_SWEEP_ENABLED=true
|
|
|
|
# Hours of grace given to a flagged signup to show organic activity (a new
|
|
# post or a new follow) before it's considered dormant/bot-like. Lowered from
|
|
# the original 24 to 1 (2026-07-06) — the datacenter/hosting-IP signal alone
|
|
# is a strong enough spam indicator that a full day of grace was mostly just
|
|
# delaying an inevitable suspend; paired with a tighter sweep cadence (see
|
|
# CLAUDE.md) so real latency stays close to this value.
|
|
SUSPICIOUS_GRACE_HOURS=1
|
|
|
|
# Moderation action taken on a flagged signup with no activity in the grace
|
|
# window: "suspend" (agreed default — a stronger signal than the report-
|
|
# driven abuse-bot's default "silence", since this is zero organic activity,
|
|
# not just reports) or "silence".
|
|
SUSPICIOUS_ACTION=suspend
|
|
|
|
# Rollout safety switch, same role as ABUSE_DRY_RUN — ships "false" (live)
|
|
# here by design; flip to "true" to pause without redeploying.
|
|
SUSPICIOUS_DRY_RUN=false
|
|
|
|
# Requires ABUSE_BOT_TOKEN to additionally carry the admin:read:accounts
|
|
# scope (fetches current post/follow counts + role/suspended state via
|
|
# GET /api/v1/admin/accounts/:id) — see CLAUDE.md's moderator-token-gotcha
|
|
# section. Without it the sweep 403s per-account (logged as an error, that
|
|
# account is retried next sweep) but nothing else in the bot is affected.
|
|
# Reuses ABUSE_SKIP_PRIVILEGED and ABUSE_ALLOWLIST above — no separate vars.
|
|
|
|
# --- Combined-signal immediate suspend (app/main.py, process_signup) ---
|
|
# The sweep above acts on EITHER signal (IP or email) after
|
|
# SUSPICIOUS_GRACE_HOURS of inactivity. A signup flagged by BOTH signals at
|
|
# once is a stronger indicator, so it's auto-actioned immediately at signup
|
|
# time instead of waiting for the hourly sweep. Additive — doesn't change
|
|
# the OR-based sweep's handling of single-signal flags. Reuses
|
|
# ABUSE_ALLOWLIST above (staff can't be brand-new signups, but checked for
|
|
# defense in depth) — no separate allowlist var.
|
|
SUSPICIOUS_COMBINED_ENABLED=true
|
|
|
|
# Moderation action taken immediately on a combined-signal signup: "suspend"
|
|
# (agreed default, same reasoning as SUSPICIOUS_ACTION) or "silence".
|
|
SUSPICIOUS_COMBINED_ACTION=suspend
|
|
|
|
# Rollout safety switch — ships "true" (dry-run) here, unlike
|
|
# SUSPICIOUS_DRY_RUN's by-design "false" default above, since this is a
|
|
# brand-new action path. Flip to "false" once the moderator DMs look right.
|
|
SUSPICIOUS_COMBINED_DRY_RUN=true
|
|
|
|
# --- Blocklist API (app/main.py, GET /api/blocks) ---------------------------
|
|
# Read-only JSON of the IP ranges and email domains this bot has auto-blocked:
|
|
# GET /api/blocks both lists in one document
|
|
# GET /api/blocks/ip-ranges registered Admin::IpBlock networks
|
|
# GET /api/blocks/email-domains registered Admin::EmailDomainBlock domains
|
|
# Optional ?limit=N on any of them. Responses assert only that a range/domain
|
|
# is blocked — no account handles, account ids or email addresses, and none of
|
|
# the ipapi.is / check-mail.org classification behind the decision (publishing
|
|
# those verdicts would be redistributing a paid reputation feed's data). Note
|
|
# this reports the *bot's own* record, not Mastodon's full blocklist: blocks a
|
|
# moderator added by hand in the admin UI aren't in it.
|
|
#
|
|
# SERVED WITHOUT AUTHENTICATION, so it defaults to off and every deployment
|
|
# must opt in: publishing which ranges and domains are already blocked also
|
|
# tells a would-be spam signup which ones to avoid. With this unset the three
|
|
# paths 404 as if they didn't exist. yttrx.com sets this to true.
|
|
BLOCKS_API_ENABLED=false
|
|
|
|
# Hard ceiling on entries per response, whatever ?limit asks for — the
|
|
# endpoint is unauthenticated, so response size can't be left to the caller.
|
|
BLOCKS_API_MAX_ITEMS=5000
|
|
|
|
# Public base URL this API is reached at. Used only to print a "source:" line
|
|
# in the text format's header comments, so a downloaded list says where it
|
|
# came from; it plays no part in routing.
|
|
BLOCKS_API_PUBLIC_URL=https://hooks.yttrx.com
|
|
|
|
# Cache-Control max-age (seconds) on blocklist responses. The data only
|
|
# changes when a signup is flagged, so a few minutes of client/intermediate
|
|
# caching costs nothing.
|
|
BLOCKS_API_CACHE_SECONDS=300
|