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.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "Why was my account limited for inactivity?"
|
||||
date: 2026-06-18
|
||||
description: "yttrx limits accounts that have been inactive for a long time. Here's what that means and how to get yours back."
|
||||
---
|
||||
|
||||
If you've landed here, your yttrx account was **automatically limited because it
|
||||
looked inactive** — no logins, app activity, or posts for well over a year.
|
||||
This is routine housekeeping, **not** a punishment, and **nothing has been
|
||||
deleted**. Getting it back takes one short email.
|
||||
|
||||
## What "limited" means
|
||||
|
||||
A limited (also called *silenced*) account is **not deleted and not banned**.
|
||||
Your posts, follows, and data are all still there. While the limit is in place,
|
||||
your account is just held back from public and federated timelines so a parked,
|
||||
forgotten account can't quietly be used for spam. It's fully reversible.
|
||||
|
||||
## Why it happens
|
||||
|
||||
yttrx is a small, human-run community. Over the years a lot of accounts sign up
|
||||
and then go quiet for good. To keep the instance tidy and to shrink the surface
|
||||
that spammers can hijack, we periodically limit accounts that show **no sign of
|
||||
life for well over a year** — meaning all of:
|
||||
|
||||
- no web login,
|
||||
- no activity from a mobile app or API client, and
|
||||
- no posts.
|
||||
|
||||
If you were active by *any* of those, you would not have been caught. If you
|
||||
were, it just means you've been away for a while — welcome back.
|
||||
|
||||
## How to get your account back
|
||||
|
||||
**The reliable way: email [admin@yttrx.com](mailto:admin@yttrx.com)** from, or
|
||||
mentioning, the address on your account. Include:
|
||||
|
||||
1. **Your full handle** (e.g. `@you@yttrx.com`).
|
||||
2. A line letting us know you're back and want the limit lifted.
|
||||
|
||||
A real person reads that inbox and will restore you, usually quickly.
|
||||
|
||||
**In-app appeal.** When you log back in, your account settings list this as a
|
||||
moderation strike with an **Appeal** option — submitting it sends your request
|
||||
straight to our moderators and works just as well.
|
||||
|
||||
**A note on DMs.** You *can* still log in and post while limited, but because a
|
||||
limited account's mentions are held back from people who don't already follow
|
||||
you, a direct message to [@waffles](https://yttrx.com/@waffles) may not reach
|
||||
them reliably until the limit is lifted. **Email is the dependable route** —
|
||||
once you're un-limited, messaging `@waffles` works normally again.
|
||||
|
||||
Thanks for being part of yttrx. We kept your account safe for you. 💜
|
||||
@@ -0,0 +1,44 @@
|
||||
# Copy to dormant-sweep.env on mammut and tune. No secrets live here — the sweep
|
||||
# runs inside Rails as the bot account, so there is no token to store.
|
||||
|
||||
# Inactivity window, in months. Starting policy: 18 months.
|
||||
DORMANT_MONTHS=18
|
||||
|
||||
# SAFETY: start true. Logs what WOULD happen and DMs a [DRY-RUN] mod summary,
|
||||
# but limits nobody. Flip to false only after reviewing a dry run.
|
||||
DORMANT_DRY_RUN=true
|
||||
|
||||
# Max accounts to act on per run, so a large backlog drains gradually instead of
|
||||
# limiting everyone at once. (At launch ~906 accounts qualify at 18mo — drain in steps.)
|
||||
DORMANT_BATCH_CAP=50
|
||||
|
||||
# Actor account (must hold an Admin role: Manage Users). yttrx uses `bot`.
|
||||
DORMANT_BOT_ACCT=bot
|
||||
|
||||
# Handle (no @) to DM a per-run batch summary. Empty disables it.
|
||||
DORMANT_MOD_ALERT=waffles
|
||||
|
||||
# Appeal / reactivation help page (DMed + referenced in the strike note).
|
||||
DORMANT_HELP_URL=https://welcome.yttrx.com/posts/account-inactive/
|
||||
|
||||
# Never limited. Includes all current staff as an authoritative backstop on top
|
||||
# of the automatic "any assigned role = staff" skip.
|
||||
DORMANT_ALLOWLIST=waffles,tommertron,davis,bot
|
||||
|
||||
# Keep "dormant" honest: also require no API/app token use and no recent post
|
||||
# within the window. Disabling either widens the net to login-only (NOT advised:
|
||||
# current_sign_in_at only updates on WEB login, so it flags active app users).
|
||||
DORMANT_REQUIRE_TOKEN_IDLE=true
|
||||
DORMANT_REQUIRE_POST_IDLE=true
|
||||
|
||||
# Notification channels.
|
||||
# EMAIL_NOTIFY -> native Mastodon strike email to the registered address.
|
||||
# OFF by default: many dormant addresses are dead, and a bounce
|
||||
# spike from admin@yttrx.com would damage yttrx's own mail
|
||||
# reputation (Mastodon has no bounce suppression). The strike
|
||||
# stays appealable in-app without it. If you turn this ON, keep
|
||||
# DORMANT_BATCH_CAP small so bounces trickle, and watch the
|
||||
# admin@yttrx.com inbox (bounces return there).
|
||||
# SEND_DM -> a direct message from the bot (no bounce risk). Primary notice.
|
||||
DORMANT_EMAIL_NOTIFY=false
|
||||
DORMANT_SEND_DM=true
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
# dormant-sweep.sh — cron entrypoint on mammut.
|
||||
#
|
||||
# Pipes dormant_sweep.rb into the Mastodon web container's `rails runner`,
|
||||
# passing config from an env file. Selection AND actions run inside Rails as the
|
||||
# `bot` account, so NO API token or secret is needed on the host.
|
||||
#
|
||||
# Install (on mammut): /root/dormant-sweep/{dormant-sweep.sh,dormant_sweep.rb,dormant-sweep.env}
|
||||
# Cron (mammut root): 30 4 * * 0 /root/dormant-sweep/dormant-sweep.sh >> /var/log/dormant-sweep.log 2>&1
|
||||
set -eu
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="${DORMANT_ENV_FILE:-$HERE/dormant-sweep.env}"
|
||||
|
||||
# Load KEY=VALUE config and export it so the loop below can forward it.
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
fi
|
||||
|
||||
CID="$(docker ps -f name=live-web-1 -q | head -n1)"
|
||||
[ -n "$CID" ] || { echo "dormant-sweep: live-web-1 container not found" >&2; exit 1; }
|
||||
|
||||
# Forward only DORMANT_* vars into the container.
|
||||
DOCKER_ENV=""
|
||||
for v in $(env | sed -n 's/^\(DORMANT_[A-Z_]*\)=.*/\1/p'); do
|
||||
DOCKER_ENV="$DOCKER_ENV -e $v"
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
docker exec -i $DOCKER_ENV "$CID" bin/rails runner - < "$HERE/dormant_sweep.rb"
|
||||
@@ -0,0 +1,193 @@
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# dormant_sweep.rb — proactively put long-dormant local yttrx accounts into a
|
||||
# reversible "limited" (silenced) state, notify each one (native strike email +
|
||||
# optional DM), and point them at an appeal/reactivation page.
|
||||
#
|
||||
# Runs INSIDE the Mastodon web container on mammut, as the `bot` account:
|
||||
# bin/rails runner - < dormant_sweep.rb (config comes from ENV)
|
||||
# Normally invoked by dormant-sweep.sh from cron. See README.md.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# "Dormant" = ALL of these are older than the cutoff (default 18 months):
|
||||
# * web/Devise login (users.current_sign_in_at; a NULL login counts as
|
||||
# dormant only if the account was created before cutoff)
|
||||
# * API/app token use (Doorkeeper::AccessToken.last_used_at)
|
||||
# * last authored post (account_stats.last_status_at)
|
||||
#
|
||||
# Requiring all three avoids limiting people who are active through a mobile
|
||||
# app (stale web login but a live OAuth token) or who lurk-and-post without ever
|
||||
# web-logging-in. current_sign_in_at ALONE is a bad signal: it only updates on
|
||||
# web login, so on a real instance it flags ~65% of accounts.
|
||||
#
|
||||
# Safety:
|
||||
# * dry-run by default (DORMANT_DRY_RUN=true) — logs what it WOULD do
|
||||
# * batch-capped per run (DORMANT_BATCH_CAP) so a backlog drains gradually
|
||||
# * never touches staff (any assigned role), allowlisted handles, or accounts
|
||||
# that are already suspended/silenced
|
||||
# * silence is REVERSIBLE and is applied with NO report attached
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
require 'time'
|
||||
require 'set'
|
||||
|
||||
def env(key, default = nil)
|
||||
v = ENV[key]
|
||||
v.nil? || v.strip.empty? ? default : v
|
||||
end
|
||||
|
||||
def env_bool(key, default)
|
||||
v = ENV[key]
|
||||
return default if v.nil? || v.strip.empty?
|
||||
%w[1 true yes on].include?(v.strip.downcase)
|
||||
end
|
||||
|
||||
MONTHS = (env('DORMANT_MONTHS', '18')).to_i
|
||||
DRY_RUN = env_bool('DORMANT_DRY_RUN', true) # SAFE DEFAULT
|
||||
BATCH_CAP = (env('DORMANT_BATCH_CAP', '50')).to_i # max accounts actioned per run
|
||||
BOT_ACCT = env('DORMANT_BOT_ACCT', 'bot') # actor; must hold an Admin role
|
||||
MOD_ALERT = (env('DORMANT_MOD_ALERT', '') || '').sub(/\A@/, '')
|
||||
HELP_URL = env('DORMANT_HELP_URL', 'https://welcome.yttrx.com/posts/account-inactive/')
|
||||
ALLOWLIST = (env('DORMANT_ALLOWLIST', 'waffles,tommertron,davis,bot') || '')
|
||||
.split(',').map { |s| s.strip.sub(/\A@/, '').downcase }.reject(&:empty?).to_set
|
||||
|
||||
# Cross-checks that keep "dormant" honest (leave on unless you really mean it).
|
||||
REQUIRE_TOKEN_IDLE = env_bool('DORMANT_REQUIRE_TOKEN_IDLE', true)
|
||||
REQUIRE_POST_IDLE = env_bool('DORMANT_REQUIRE_POST_IDLE', true)
|
||||
|
||||
# Notification channels.
|
||||
# EMAIL_NOTIFY -> Mastodon's native strike email to the user's registered
|
||||
# address (and the in-app notification ping). OFF BY DEFAULT:
|
||||
# dormant addresses are often dead, and a bounce spike from
|
||||
# admin@yttrx.com would hurt yttrx's own sending reputation
|
||||
# (Mastodon has no bounce suppression). The strike itself is
|
||||
# still created and appealable in settings without the email.
|
||||
# SEND_DM -> a direct message from the bot (a Mastodon DM, no bounce
|
||||
# risk; seen if they log back in — silence doesn't block
|
||||
# INCOMING DMs). This is the primary, safe notice.
|
||||
EMAIL_NOTIFY = env_bool('DORMANT_EMAIL_NOTIFY', false)
|
||||
SEND_DM = env_bool('DORMANT_SEND_DM', true)
|
||||
|
||||
# Moderation note stored on the strike. The "dormant-sweep" tag makes the batch
|
||||
# findable/reversible later (see README rollback).
|
||||
NOTE = env('DORMANT_NOTE',
|
||||
"[dormant-sweep] Auto-limited: no login, app/API use, or posts in " \
|
||||
"#{MONTHS}+ months. Reversible, no report attached. Reactivate by emailing " \
|
||||
"admin@yttrx.com — see #{HELP_URL}")
|
||||
|
||||
# Secondary DM template ({acct} + {help_url} substituted; must mention @{acct}).
|
||||
DM_TEMPLATE = env('DORMANT_DM',
|
||||
"Hi @{acct} — your yttrx account has been put into a limited state because it " \
|
||||
"looks inactive (no login or posts in #{MONTHS}+ months). Nothing is deleted " \
|
||||
"and it's easy to undo: email admin@yttrx.com (or use the in-app appeal) and " \
|
||||
"we'll lift it. Details: {help_url}")
|
||||
|
||||
def log(msg)
|
||||
puts "#{Time.now.utc.iso8601} #{msg}"
|
||||
end
|
||||
|
||||
cutoff = MONTHS.months.ago
|
||||
actor = Account.find_local(BOT_ACCT)
|
||||
abort("dormant_sweep: actor account '#{BOT_ACCT}' not found") unless actor
|
||||
|
||||
log("start dry_run=#{DRY_RUN} months=#{MONTHS} cutoff=#{cutoff.iso8601} " \
|
||||
"batch_cap=#{BATCH_CAP} email=#{EMAIL_NOTIFY} dm=#{SEND_DM} " \
|
||||
"token_idle=#{REQUIRE_TOKEN_IDLE} post_idle=#{REQUIRE_POST_IDLE}")
|
||||
|
||||
# --- selection -------------------------------------------------------------
|
||||
# User rows exist only for LOCAL accounts, so no domain filter is needed.
|
||||
base = User.confirmed.where(approved: true).joins(:account)
|
||||
.where(accounts: { suspended_at: nil, silenced_at: nil })
|
||||
candidates = base.where(
|
||||
'users.current_sign_in_at < :c OR (users.current_sign_in_at IS NULL AND users.created_at < :c)',
|
||||
c: cutoff
|
||||
)
|
||||
|
||||
# Accounts with API/app token activity since the cutoff are NOT dormant.
|
||||
active_token_uids =
|
||||
if REQUIRE_TOKEN_IDLE
|
||||
Doorkeeper::AccessToken.where(resource_owner_id: candidates.pluck(:id))
|
||||
.where('last_used_at > ?', cutoff).distinct.pluck(:resource_owner_id).to_set
|
||||
else
|
||||
Set.new
|
||||
end
|
||||
|
||||
acted = 0
|
||||
skipped = Hash.new(0)
|
||||
|
||||
candidates.includes(account: :account_stat).find_each do |user|
|
||||
break if acted >= BATCH_CAP
|
||||
account = user.account
|
||||
handle = account.username.downcase
|
||||
|
||||
# --- guards --------------------------------------------------------------
|
||||
if ALLOWLIST.include?(handle)
|
||||
skipped[:allowlist] += 1; next
|
||||
end
|
||||
# Normal users have a NULL role_id; any assigned role means staff. The
|
||||
# ALLOWLIST above is an authoritative backstop for known staff.
|
||||
if user.role_id.present?
|
||||
skipped[:staff] += 1; next
|
||||
end
|
||||
if REQUIRE_TOKEN_IDLE && active_token_uids.include?(user.id)
|
||||
skipped[:token_active] += 1; next
|
||||
end
|
||||
if REQUIRE_POST_IDLE
|
||||
last_status = account.account_stat&.last_status_at
|
||||
if last_status && last_status > cutoff
|
||||
skipped[:recent_post] += 1; next
|
||||
end
|
||||
end
|
||||
|
||||
last_login = user.current_sign_in_at ? user.current_sign_in_at.strftime('%Y-%m-%d') : 'never'
|
||||
|
||||
if DRY_RUN
|
||||
log("[DRY-RUN] would limit @#{account.username} (last_login=#{last_login})")
|
||||
acted += 1
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
Admin::AccountAction.new(
|
||||
type: 'silence',
|
||||
current_account: actor,
|
||||
target_account: account,
|
||||
text: NOTE,
|
||||
send_email_notification: EMAIL_NOTIFY,
|
||||
include_statuses: false
|
||||
).save!
|
||||
rescue => e
|
||||
log("ERROR limiting @#{account.username}: #{e.class}: #{e.message}")
|
||||
next
|
||||
end
|
||||
|
||||
if SEND_DM
|
||||
begin
|
||||
text = DM_TEMPLATE.gsub('{acct}', account.username).gsub('{help_url}', HELP_URL)
|
||||
PostStatusService.new.call(actor, text: text, visibility: 'direct')
|
||||
rescue => e
|
||||
log("WARN limited @#{account.username} but DM failed: #{e.class}: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
acted += 1
|
||||
log("limited @#{account.username} (last_login=#{last_login})" \
|
||||
"#{EMAIL_NOTIFY ? '; emailed' : ''}#{SEND_DM ? '; DMed' : ''}")
|
||||
end
|
||||
|
||||
log("done dry_run=#{DRY_RUN} acted=#{acted} batch_cap=#{BATCH_CAP} " \
|
||||
"skipped=#{skipped.map { |k, v| "#{k}=#{v}" }.join(' ')}")
|
||||
|
||||
# Batch summary to the moderator (best-effort).
|
||||
if !MOD_ALERT.empty? && acted.positive?
|
||||
begin
|
||||
verb = DRY_RUN ? 'would limit' : 'limited'
|
||||
PostStatusService.new.call(actor,
|
||||
text: "@#{MOD_ALERT} 🧹 Dormant sweep: #{verb} #{acted} inactive account(s) " \
|
||||
"(no login/app/posts in #{MONTHS}+ mo, cap #{BATCH_CAP}/run)" \
|
||||
"#{DRY_RUN ? ' [DRY-RUN]' : ''}.",
|
||||
visibility: 'direct')
|
||||
rescue => e
|
||||
log("WARN mod-alert DM failed: #{e.class}: #{e.message}")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user