docs: add vulgar-word/fragment-sold reserved-username lists and CSVs, reserve script
Some checks are pending
CI / Grammy store bot (push) Waiting to run
CI / Docker main topology smoke (push) Waiting to run
CI / Go tests (push) Waiting to run
CI / Admin web build (push) Waiting to run

Extends the reserved-usernames doc with a vulgar/offensive section, adds
CSV exports (username,reason) for both the categorized reserved-usernames
list and a filtered snapshot of fragment.com's sold usernames, and adds a
scripts/reserve_usernames.py CLI to bulk-feed a CSV into the admin panel's
reserved-usernames blocklist via the real login -> CSRF -> reserve-username
API flow.
This commit is contained in:
Astra 2026-09-21 15:57:36 +01:00
parent 492bd9a934
commit 9b76ef4041
5 changed files with 1030 additions and 0 deletions

129
scripts/reserve_usernames.py Executable file
View file

@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Bulk-reserve usernames through the telesrv-admin panel API.
Reads a CSV file of `username,reason` lines and calls
POST /api/actions/reserve-username for each one, using the same
login -> cookie session -> CSRF token flow the panel frontend uses
(see docs/admin-panel-api.en.md).
Usage:
python3 scripts/reserve_usernames.py \
--host https://admin.example.com \
--username operator_name \
--file usernames.csv
`--username` is your own named admin_console_users account - use the same
username/password you log into the panel with. (The panel also has an
"owpengram" break-glass login backed by TELESRV_ADMIN_UI_PASSWORD / _TOKEN,
but that's a fallback for when the database or named accounts aren't
available, not something to use day-to-day.) The script prompts for your
password either way - it's never passed as a CLI argument or stored.
CSV format, one entry per line, no header:
somename,squatting on a brand name
othername,profanity
"""
import argparse
import csv
import getpass
import http.cookiejar
import json
import sys
import urllib.error
import urllib.request
def build_opener():
jar = http.cookiejar.CookieJar()
return urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar)), jar
def post_json(opener, url, payload, extra_headers=None):
data = json.dumps(payload).encode("utf-8")
headers = {"Content-Type": "application/json"}
if extra_headers:
headers.update(extra_headers)
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
with opener.open(req) as resp:
body = resp.read()
return resp.status, json.loads(body) if body else {}
except urllib.error.HTTPError as e:
body = e.read()
try:
return e.code, json.loads(body) if body else {}
except json.JSONDecodeError:
return e.code, {"error": body.decode("utf-8", "replace")}
def login(opener, host, username, secret):
status, resp = post_json(opener, f"{host}/api/login", {"username": username, "secret": secret})
if status != 200:
raise SystemExit(f"login failed: HTTP {status} {resp}")
return resp["csrf_token"]
def reserve_username(opener, host, csrf_token, username, reason):
status, resp = post_json(
opener,
f"{host}/api/actions/reserve-username",
{"username": username, "reason": reason, "confirm": True},
extra_headers={"X-CSRF-Token": csrf_token},
)
return status, resp
def read_entries(path):
entries = []
with open(path, newline="") as f:
for lineno, row in enumerate(csv.reader(f), start=1):
if not row or not row[0].strip():
continue
if len(row) < 2:
raise SystemExit(f"{path}:{lineno}: expected 'username,reason', got {row!r}")
username = row[0].strip()
reason = ",".join(row[1:]).strip()
if not username or not reason:
raise SystemExit(f"{path}:{lineno}: empty username or reason")
entries.append((username, reason))
return entries
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--host", required=True, help="panel base URL, e.g. https://admin.example.com")
parser.add_argument("--username", required=True, help="your named admin_console_users account")
parser.add_argument("--file", required=True, help="CSV file of username,reason lines")
args = parser.parse_args()
host = args.host.rstrip("/")
entries = read_entries(args.file)
if not entries:
raise SystemExit("no entries found in " + args.file)
secret = getpass.getpass("Password/secret: ")
opener, _jar = build_opener()
csrf_token = login(opener, host, args.username, secret)
ok, failed = 0, []
for username, reason in entries:
status, resp = reserve_username(opener, host, csrf_token, username, reason)
if status == 200:
ok += 1
print(f"reserved {username!r} ({reason})")
else:
failed.append((username, reason, status, resp))
print(f"FAILED {username!r} HTTP {status} {resp.get('error') or resp.get('message') or resp}")
print(f"\n{ok}/{len(entries)} reserved")
if failed:
print(f"{len(failed)} failed:")
for username, reason, status, resp in failed:
print(f" {username}: HTTP {status} {resp}")
sys.exit(1)
if __name__ == "__main__":
main()