Initial commit: welcome-bot, abuse-bot, dormant-sweep, IP signup scrutiny
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Guidance for Claude Code when working on **yttrx-welcomebot** (the welcome bot
|
||||
+ abuse auto-silence bot for yttrx.com).
|
||||
|
||||
## What this is / where it runs
|
||||
|
||||
A FastAPI webhook server. One Mastodon admin webhook delivers `account.created`,
|
||||
`account.approved`, and `report.created` to `POST /webhook`; the app dispatches:
|
||||
|
||||
- **account.created** → classify the signup IP (RDAP org lookup); flagged
|
||||
(datacenter/hosting) signups get more scrutiny (held welcome, ip_blocks
|
||||
registration, DM to moderator) before falling through to a normal welcome.
|
||||
- **account.approved** → always DM the welcome (dedup-safe; this is what
|
||||
releases a held welcome once a human clears the approval queue).
|
||||
- **report.created** → classify the reported account, count distinct reporters,
|
||||
and auto-**silence** young/dormant accounts past the threshold — lowered for
|
||||
accounts with a flagged signup IP (see README).
|
||||
|
||||
| Where | What |
|
||||
|---|---|
|
||||
| Workstation | Source of truth: `~/yttrx-welcomebot/` (this repo) |
|
||||
| `admin.yttrx.com` | Deploy target: `/root/yttrx-welcomebot/`, Docker container `yttrx-welcomebot` on `127.0.0.1:8087`, nginx site `hooks` → `hooks.yttrx.com`. `ssh admin.yttrx.com` logs in as **root**. Compose is **`docker-compose` v1.29.2** (hyphenated, not `docker compose`). |
|
||||
| `mammut` (`ssh mammut`) | The Mastodon instance. `tootctl`/Rails run inside the `live-web-1` container. Bot accounts + tokens live here. |
|
||||
|
||||
Full ops history is in secondbrain `yttrx-documentation/changelog.md`; the
|
||||
user-memory note `project_welcomebot` has the current state.
|
||||
|
||||
## HARD RULES
|
||||
|
||||
- **Never overwrite the `.env` on `admin.yttrx.com`.** It holds the live
|
||||
`WEBHOOK_SECRET`, `BOT_ACCESS_TOKEN`, and `ABUSE_BOT_TOKEN`. The local `.env`
|
||||
has these blank. **Always rsync code only (`--exclude='.env'`)** and edit the
|
||||
box's `.env` in place for new vars.
|
||||
- **Never commit secrets.** `.env` is git- and docker-ignored. Tokens/passwords
|
||||
must not land in this repo, the changelog, or CLAUDE.md.
|
||||
- The bot **silences** (reversible), it does not suspend by default
|
||||
(`ABUSE_ACTION=silence`). It applies the action **without** `report_id` so the
|
||||
report stays open for human review.
|
||||
|
||||
## Deploy procedure (workstation → admin.yttrx.com)
|
||||
|
||||
```bash
|
||||
# 1. Test locally (offline, no network)
|
||||
cd ~/yttrx-welcomebot
|
||||
python3 -m venv .venv && . .venv/bin/activate && pip install -q -r requirements.txt
|
||||
WEBHOOK_SECRET=testsecret BOT_ACCESS_TOKEN=x python test_local.py # -> ALL TESTS PASSED
|
||||
|
||||
# 2. Back up the live code + env on the box (date-stamp)
|
||||
ssh admin.yttrx.com 'cd /root/yttrx-welcomebot && \
|
||||
cp app/main.py app/main.py.bak-$(date +%Y%m%d) && cp .env .env.bak-$(date +%Y%m%d)'
|
||||
|
||||
# 3. Rsync CODE ONLY (never the .env)
|
||||
rsync -az --exclude='.env' --exclude='.venv' --exclude='__pycache__' \
|
||||
--exclude='*.pyc' --exclude='*.db' --exclude='*.bak*' --exclude='.git' \
|
||||
~/yttrx-welcomebot/ admin.yttrx.com:/root/yttrx-welcomebot/
|
||||
|
||||
# 4. Normalise ownership; keep .env root:root 600
|
||||
ssh admin.yttrx.com 'cd /root/yttrx-welcomebot && chown -R waffles:waffles . && \
|
||||
chown root:root .env .env.bak-* 2>/dev/null; chmod 600 .env .env.bak-* 2>/dev/null'
|
||||
|
||||
# 5. If new env vars were added this release, set them IN PLACE on the box, e.g.
|
||||
ssh admin.yttrx.com 'cd /root/yttrx-welcomebot && \
|
||||
grep -q "^NEW_VAR=" .env || echo "NEW_VAR=value" >> .env'
|
||||
|
||||
# 6. Rebuild + restart (named volume welcomebot-data persists the dedup db)
|
||||
ssh admin.yttrx.com 'cd /root/yttrx-welcomebot && docker-compose up -d --build'
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
ssh admin.yttrx.com 'curl -s localhost:8087/healthz' # {"ok":true}
|
||||
ssh admin.yttrx.com 'docker logs --tail 20 yttrx-welcomebot' # clean startup
|
||||
# confirm the running image + config:
|
||||
ssh admin.yttrx.com 'docker exec yttrx-welcomebot python -c \
|
||||
"import app.main as m; print(m.ABUSE_DRY_RUN, m.ABUSE_ACTION, sorted(m.ABUSE_ALLOWLIST))"'
|
||||
```
|
||||
|
||||
### Viewing logs
|
||||
|
||||
A `welcomebot-logs` CLI is installed at `/usr/local/bin/welcomebot-logs` on
|
||||
admin.yttrx.com (source: `bin/welcomebot-logs` in this repo). It wraps
|
||||
`docker logs` for the container:
|
||||
|
||||
```bash
|
||||
ssh admin.yttrx.com welcomebot-logs # last 200 lines
|
||||
ssh admin.yttrx.com welcomebot-logs -f --abuse # follow abuse activity only
|
||||
ssh admin.yttrx.com welcomebot-logs -n 1000 --since 24h
|
||||
ssh admin.yttrx.com 'welcomebot-logs --help'
|
||||
```
|
||||
|
||||
To reinstall after editing `bin/welcomebot-logs`:
|
||||
`scp bin/welcomebot-logs admin.yttrx.com:/tmp/ && ssh admin.yttrx.com 'install -m755 /tmp/welcomebot-logs /usr/local/bin/'`
|
||||
|
||||
### Rollback
|
||||
|
||||
```bash
|
||||
ssh admin.yttrx.com 'cd /root/yttrx-welcomebot && \
|
||||
cp app/main.py.bak-YYYYMMDD app/main.py && cp .env.bak-YYYYMMDD .env && \
|
||||
docker-compose up -d --build'
|
||||
```
|
||||
|
||||
## Rollout safety: dry-run
|
||||
|
||||
`ABUSE_DRY_RUN=true` makes the abuse handler log + DM what it *would* do without
|
||||
silencing anyone. The shipped `.env.example` default is `true`; **production is
|
||||
currently `false` (live and acting)**. To re-enter dry-run:
|
||||
|
||||
```bash
|
||||
ssh admin.yttrx.com 'cd /root/yttrx-welcomebot && \
|
||||
sed -i "s/^ABUSE_DRY_RUN=.*/ABUSE_DRY_RUN=true/" .env && docker-compose up -d'
|
||||
```
|
||||
|
||||
## The moderator bot token (gotcha)
|
||||
|
||||
The abuse handler posts to `POST /api/v1/admin/accounts/:id/action`, which needs
|
||||
the **`admin:write:accounts`** OAuth scope — *separate* from `admin:write:reports`.
|
||||
The bot account (`bot`) is an **Admin** (so it has the role permissions), but a
|
||||
token created in the UI may omit `admin:write:accounts` → silence returns `403`.
|
||||
|
||||
**IP-scrutiny's `register_ip_block` needs an additional scope,
|
||||
`admin:write:ip_blocks`**, to `POST /api/v1/admin/ip_blocks` — mint (or re-mint)
|
||||
the token with it included, or `IP_SCRUTINY_AUTO_IPBLOCK` writes will 403 (logged
|
||||
as an error; nothing else in the bot is affected).
|
||||
|
||||
Mint a correctly-scoped token from the Rails console on mammut:
|
||||
|
||||
```bash
|
||||
ssh mammut 'docker exec $(docker ps -f name=live-web-1 -q) bin/rails runner "
|
||||
acct = Account.find_local(%q{bot})
|
||||
scopes = %q{read:statuses write:statuses admin:read:reports admin:write:accounts admin:write:ip_blocks}
|
||||
app = Doorkeeper::Application.create!(name: %q{Abuse handler vN}, scopes: scopes, redirect_uri: %q{urn:ietf:wg:oauth:2.0:oob})
|
||||
tok = Doorkeeper::AccessToken.create!(application_id: app.id, resource_owner_id: acct.user.id, scopes: scopes)
|
||||
puts tok.token
|
||||
"'
|
||||
```
|
||||
|
||||
Put the result in `ABUSE_BOT_TOKEN` on the box's `.env`, restart, and revoke the
|
||||
old token (`Doorkeeper::AccessToken.find(<id>).revoke`).
|
||||
|
||||
**Harmless write-permission probe** (no real account touched — `403` = missing
|
||||
scope, `404` = authorized):
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
|
||||
-H "Authorization: Bearer $ABUSE_BOT_TOKEN" -d "type=none" \
|
||||
https://yttrx.com/api/v1/admin/accounts/0/action
|
||||
```
|
||||
|
||||
Same idea for the `admin:write:ip_blocks` scope IP-scrutiny needs (`403` =
|
||||
missing scope; `422` = authorized, just missing/invalid params — no block is
|
||||
created either way):
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
|
||||
-H "Authorization: Bearer $ABUSE_BOT_TOKEN" \
|
||||
https://yttrx.com/api/v1/admin/ip_blocks
|
||||
```
|
||||
|
||||
## Mastodon side (mammut)
|
||||
|
||||
- The webhook (Administration → Webhooks) is already subscribed to
|
||||
`account.created`, `account.approved`, `report.created` — no change needed for
|
||||
code redeploys.
|
||||
- Staff are never auto-silenced: `ABUSE_SKIP_PRIVILEGED=true` reads the payload
|
||||
`target_account.role` (skips any assigned staff role, id > 0), plus the
|
||||
explicit `ABUSE_ALLOWLIST`. Current staff: `waffles`/`tommertron` (Owner),
|
||||
`davis`/`bot` (Admin).
|
||||
|
||||
## Appeals page
|
||||
|
||||
Silenced users are DMed `ABUSE_HELP_URL` =
|
||||
`https://welcome.yttrx.com/posts/account-limited/`. That page is a Hugo
|
||||
(Compost) content file at `admin.yttrx.com:/var/www/html/welcome/content/posts/`,
|
||||
rebuilt with `./build.sh` in that dir (see secondbrain `misc-sites.md`).
|
||||
Reference in New Issue
Block a user