#!/usr/bin/env python3 """One-shot backfill of signup_ip.block_cidr, run once on admin.yttrx.com 2026-09-13 when the blocklist API shipped. block_cidr records the network register_ip_block actually blocked. It didn't exist before the API did, so every pre-existing blocked row had nothing to report and /api/blocks fell back to its signup IP's /32 — narrower than what is really blocked, and not what the endpoint is for. The range is recoverable from the bot's own ipapi range cache, which stores the CIDR each classification was cached under (the same value that was handed to register_ip_block). Run inside the container, dry-run first: docker cp bin/backfill-block-cidr.py yttrx-welcomebot:/tmp/ docker exec -e PYTHONPATH=/app yttrx-welcomebot python /tmp/backfill-block-cidr.py docker exec -e PYTHONPATH=/app -e APPLY=1 yttrx-welcomebot python /tmp/backfill-block-cidr.py Result of the 2026-09-13 run: 154 blocked rows, 65 backfilled to a real range, 79 that genuinely were single-address blocks (ipapi.is returned no asn.route for them), 10 with no cache entry at all whose range can't be reconstructed and stay as a /32. """ import os import app.main as m # Route-CIDR blocking landed 2026-07-15 (commit b39528a); the ipapi range # cache landed 2026-07-13. Only signups blocked after the former had a network # CIDR handed to register_ip_block — for anything earlier only the /32 was # ever registered, so backfilling a wider range from the cache would publish a # block that doesn't exist. Under-reporting is safe; over-reporting isn't. CUTOFF = "2026-07-16" APPLY = os.environ.get("APPLY") == "1" # NB: _db_lock is not reentrant — never call cached_ip_intel() inside a _db() # block, it opens its own. with m._db() as conn: rows = conn.execute( "SELECT account_id, ip, classified_at FROM signup_ip " "WHERE ipblock_registered = 1 AND block_cidr IS NULL" ).fetchall() updates, skipped_old, no_cache = [], 0, 0 for account_id, ip, classified_at in rows: intel = m.cached_ip_intel(ip or "") cidr = (intel or {}).get("cidr") or "" if not cidr or cidr.endswith(("/32", "/128")): no_cache += 1 continue if (classified_at or "") < CUTOFF: skipped_old += 1 continue updates.append((cidr, account_id)) print(f"rows missing block_cidr : {len(rows)}") print(f"no wider cached CIDR : {no_cache} (stay as their /32 or /128)") print(f"pre-{CUTOFF} rows : {skipped_old} (only a /32 was ever blocked)") print(f"to backfill : {len(updates)}") for cidr, account_id in updates[:5]: print(f" e.g. account {account_id} -> {cidr}") if APPLY: with m._db() as conn: conn.executemany("UPDATE signup_ip SET block_cidr = ? WHERE account_id = ?", updates) print(f"applied {len(updates)} updates") else: print("DRY RUN — set APPLY=1 to write")