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.
129 lines
4.5 KiB
Python
Executable file
129 lines
4.5 KiB
Python
Executable file
#!/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()
|