Files

6.9 KiB
Raw Permalink Blame History

dormant-sweep

A scheduled sweep that reversibly limits (silences) long-dormant local yttrx accounts and notifies each one with an appeal/reactivation path. This is the proactive counterpart to the report-driven abuse-bot in this repo — roadmap item A in secondbrain yttrx-documentation/anti-abuse.md.

Unlike the rest of the welcomebot (which runs on admin over the HTTP API), this sweep runs on mammut, inside the Mastodon live-web-1 container, because the "last login" signal (users.current_sign_in_at) only exists in the database — the admin REST API does not expose it. Running inside Rails also means it acts as the bot account directly, so there is no API token or secret to manage on the host.

What counts as "dormant"

ALL of the following must be older than the cutoff (default 18 months):

Signal Source Why
web login users.current_sign_in_at the headline "hasn't logged in" signal (NULL = never logged in; only counts if the account predates the cutoff)
app / API token use Doorkeeper::AccessToken.last_used_at so we don't limit people active via a mobile app (stale web login, live token)
last post account_stats.last_status_at so we don't limit lurkers who post without web-logging-in

Why all three? current_sign_in_at alone is a trap: it only updates on web login, so at a 6-month cutoff it flagged 1004 / 1548 accounts (~65%) on the live instance. The three-signal definition flagged the same 1004 (the instance is just very quiet), but it will not produce false positives as app usage grows. Starting policy is an 18-month cutoff → 906 candidates.

Always excluded: staff (any assigned role), DORMANT_ALLOWLIST handles, and accounts already suspended or silenced.

Notifications

DM only by default. We do NOT email dormant users. Blasting a native strike email to ~900 years-stale addresses would bounce heavily, and a bounce spike from admin@yttrx.com (which has no Mastodon-side suppression) would damage yttrx's own outbound mail reputation — the same channel signup confirmations use. So:

  • Bot DM (DORMANT_SEND_DM=true, default) — the primary, bounce-free notice. A Mastodon DM from the bot; seen if they log back in. (Silence blocks outgoing reach, not incoming, so the DM is delivered fine.)
  • Strike record — every action still creates an AccountWarning (strike) that is appealable in the user's account settings, independent of email.
  • Native strike email (DORMANT_EMAIL_NOTIFY=false, default) — OFF. Turning it on emails the registered address AND fires the in-app notification ping (both are gated behind the same warnable? check). Only enable with a small DORMANT_BATCH_CAP so bounces trickle, and watch the admin@yttrx.com inbox (bounces return there; Mastodon won't auto-suppress).
  • Mod summary — a per-run batch summary DM to DORMANT_MOD_ALERT.

The user-facing reactivation path is email admin@yttrx.com (and/or the native in-app appeal on the strike) — not "DM @waffles", because a silenced account's mentions to non-followers are filtered and unreliable until the limit is lifted. The help page (account-inactive.md) explains this.

Files

File Role
dormant_sweep.rb the sweep itself; run via rails runner - inside live-web-1
dormant-sweep.sh host cron wrapper (pipes the rb into the container)
dormant-sweep.env.example config template → copy to dormant-sweep.env on mammut
account-inactive.md Hugo content for welcome.yttrx.com/posts/account-inactive/

Config

All via env (see dormant-sweep.env.example). Key knobs: DORMANT_MONTHS (18), DORMANT_DRY_RUN (true by default), DORMANT_BATCH_CAP (50/run), DORMANT_ALLOWLIST, DORMANT_EMAIL_NOTIFY (false — DM only), DORMANT_SEND_DM, DORMANT_MOD_ALERT.

Deploy (workstation → mammut)

# 1. Copy the runner + wrapper + config to mammut
ssh mammut 'mkdir -p /root/dormant-sweep'
scp dormant_sweep.rb dormant-sweep.sh dormant-sweep.env.example mammut:/root/dormant-sweep/
ssh mammut 'cd /root/dormant-sweep && cp -n dormant-sweep.env.example dormant-sweep.env && \
            chmod +x dormant-sweep.sh'
# 2. Edit /root/dormant-sweep/dormant-sweep.env on mammut (keep DORMANT_DRY_RUN=true)

# 3. DRY RUN — review what it would do (cap raised for a full preview)
ssh mammut 'DORMANT_BATCH_CAP=2000 /root/dormant-sweep/dormant-sweep.sh' | tee /tmp/dormant-dryrun.log

# 4. When happy, go live in controlled batches:
#    set DORMANT_DRY_RUN=false in the env, then either run by hand a few times
#    or let cron drain it at DORMANT_BATCH_CAP per run.
ssh mammut 'crontab -l; echo "30 4 * * 0 /root/dormant-sweep/dormant-sweep.sh >> /var/log/dormant-sweep.log 2>&1" | crontab -'

Deploy the help page (welcome.yttrx.com, on admin)

scp account-inactive.md admin.yttrx.com:/var/www/html/welcome/content/posts/
ssh admin.yttrx.com 'cd /var/www/html/welcome && ./build.sh && \
                     git add -A && git commit -m "welcome: add account-inactive (dormant) page"'

Initial backlog (~906 accounts at the 18-month cutoff)

This is a large one-time batch. Recommended rollout:

  1. Dry-run (step 3) and read the list — confirm no surprises.
  2. Set DORMANT_DRY_RUN=false, keep DORMANT_BATCH_CAP modest (e.g. 50100).
  3. Run a few batches by hand, spot-check the strike emails / appeals queue, then let cron drain the rest. New accounts crossing 6mo afterward are a trickle.

Rollback / un-limit

Every action is logged: limited @handle ... lines in /var/log/dormant-sweep.log, and the strike note is tagged [dormant-sweep]. To lift the whole batch:

# from the log (exact set this script limited)
ssh mammut 'grep -oP "(?<=limited @)\S+" /var/log/dormant-sweep.log | sort -u' > /tmp/swept.txt
ssh mammut 'docker exec -i $(docker ps -f name=live-web-1 -q) bin/rails runner "
  STDIN.read.split.each { |u| a = Account.find_local(u); a.unsilence! if a&.silenced? }
"' < /tmp/swept.txt

# or by the strike tag, regardless of the log:
ssh mammut 'docker exec $(docker ps -f name=live-web-1 -q) bin/rails runner "
  AccountWarning.where(\"text LIKE ?\", \"%[dormant-sweep]%\").find_each do |w|
    w.target_account.unsilence! if w.target_account&.silenced?
  end
"'

A single account: in the admin UI (Moderation → the account → Undo limit), or Account.find_local(\"handle\").unsilence!.

Notes / gotchas

  • Runs as bot (Admin). Admin::AccountAction(type: 'silence') is the exact call the abuse-bot makes via the API — same reversible, no-report_id behaviour, just invoked in-process.
  • mammut uses the docker compose plugin; tootctl/rails run inside live-web-1 (docker exec $(docker ps -f name=live-web-1 -q) ...).
  • Verified against Mastodon 4.6.0 (2026-06-18): Admin::AccountAction, PostStatusService, User.confirmed, and the three signal columns all exist.