ipapi.is free tier returns company/asn as flat strings ("Google LLC",
"AS15169 Google LLC") not nested dicts. The old code assumed nested
objects and crashed with AttributeError when trying to call .get() on
strings. This broke IP-based signup scrutiny for every new signup.
Now handles both formats (string and dict) for backward compat. Route
field handling also simplified since free tier doesn't nest asn data.
Fixes signup accounts falling through without IP classification.
datacenter/proxy/tor/abuser is a much cleaner bulk/bot-signup indicator
than vpn, which also flags plenty of privacy-conscious real users. A
strong-flagged signup now suspends immediately regardless of the email
signal; vpn-only still gets the normal held-welcome/ipblock/lowered-
threshold treatment, falling through to the existing combined-signal
suspend only if also paired with a flagged email domain.
register_ip_block always blocked the exact signup IP as a /32 (or /128
for v6), which is close to pointless for the datacenter/VPN/proxy space
these flags fire on: a repeat bad actor from the same provider almost
never reuses the exact same address, but very often reuses a different
one in the same block.
classify_signup_ip now returns a fourth value, block_cidr — ipapi.is's
asn.route CIDR when it's a valid network that actually contains the
signup IP (already being tracked in ipapi_range_cache purely for lookup
caching), falling back to the address's own /32 or /128 when no usable
route exists. register_ip_block blocks that instead of always deriving
a /32 itself.
Added range_cache_tests() covering the cache/fallback logic directly
(pure sqlite + ipaddress, no network), and updated the existing
ip_scrutiny_tests()/combined_signal_tests() mocks for the new 4-tuple
classify_signup_ip return and register_ip_block arity.
The hourly OR-based sweep (SUSPICIOUS_GRACE_HOURS) still handles a
single-signal flag as before. This adds an additional check in
process_signup(): a signup flagged by BOTH IP-scrutiny and
email-domain scrutiny at once is a stronger signal, so it's suspended
right away instead of waiting out the grace period, skipping the
hold/welcome path and the suspicious_watch entry entirely.
Gated behind new SUSPICIOUS_COMBINED_* env vars, dry-run first per the
usual rollout convention (unlike SUSPICIOUS_DRY_RUN, which shipped
live by design). Falls back to the normal held-welcome path if the
suspend API call fails, and respects ABUSE_ALLOWLIST.
Uses asn.route from ipapi.is to bucket the classification cache by
network block (IPv4 and IPv6), so signups from the same datacenter/ASN
block no longer each burn a separate lookup against the 1000/day quota.
Falls back to a single-address range when route is missing or doesn't
actually contain the queried IP.
fetch_account_counts() was hitting the public /api/v1/accounts/:id
endpoint, which 404s for any account that hasn't confirmed its email
yet. Most flagged signups haven't confirmed at the moment
account.created/approved fires, so the baseline snapshot silently
failed and the account never entered suspicious_watch -- 6 of 8
IP-flagged signups to date missed the sweep entirely. Switch to the
admin endpoint suspicious_sweep.py already uses successfully.
account.approved never fires for a signup that went through open
registration without ever being queued (no prior block on that
address). process_signup was waiting on that event forever, so the
first flagged signup on any new bad IP/email domain never got welcomed
or watched by the suspicious-sweep. Now checks Admin::Account.approved
from the account.created payload and acts immediately if it's already
true.
With the sweep now running every 10 minutes, the per-run 'N due' summary
and per-account skip/clear log lines (already-limited, staff, cleared by
activity, not found) were mostly noise drowning out the actual suspend/
silence actions. Those states are still recorded in suspicious_watch.status
for anyone querying the DB directly; only the taken action, dry-run
simulation, and error paths still log.
Watches every account flagged by IP-scrutiny or email-domain scrutiny at
signup and, once it goes live, records a baseline post/follow count. A
scheduled sweep (app/suspicious_sweep.py, run via cron on admin.yttrx.com)
suspends any watch past SUSPICIOUS_GRACE_HOURS with zero new posts and
zero new follows since that baseline; any activity clears the watch.
Works whether yttrx is open-registration or requires moderator approval,
since the watch starts at whichever event actually makes the account live
(account.created vs account.approved), same dual handling the welcome flow
already uses. Needs ABUSE_BOT_TOKEN re-minted with admin:read:accounts.
Classifies each signup's email domain (domain-only, GDPR-friendly) alongside
the existing IP scrutiny signal, with matching held-welcome and auto
email_domain_block behavior. A report against an account with a flagged
domain suspends immediately (no reporter-count threshold) and blocks the
domain, classified live from the report payload rather than any signup-time
record so it also covers pre-existing accounts. Ships CHECK_MAIL_DRY_RUN=true
by default, independent of ABUSE_DRY_RUN, so the new report-triggered suspend
path stays inert until watched.