Initial commit: welcome-bot, abuse-bot, dormant-sweep, IP signup scrutiny
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# 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. 50–100).
|
||||
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.
|
||||
Reference in New Issue
Block a user