Files
yttrx-welcomebot/dormant-sweep/README.md
T

143 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```bash
# 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)
```bash
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:
```bash
# 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.