#!/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()