docs: add vulgar-word/fragment-sold reserved-username lists and CSVs, reserve script
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:
parent
492bd9a934
commit
9b76ef4041
5 changed files with 1030 additions and 0 deletions
129
scripts/reserve_usernames.py
Executable file
129
scripts/reserve_usernames.py
Executable 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue