# services/communication/guest-name-job.yaml apiVersion: batch/v1 kind: CronJob metadata: name: guest-name-randomizer namespace: comms spec: schedule: "*/1 * * * *" suspend: false jobTemplate: spec: backoffLimit: 0 template: spec: restartPolicy: Never volumes: - name: mas-admin-client secret: secretName: mas-admin-client-runtime items: - key: client_secret path: client_secret containers: - name: rename image: python:3.11-slim volumeMounts: - name: mas-admin-client mountPath: /etc/mas-admin-client readOnly: true env: - name: SYNAPSE_BASE value: http://othrys-synapse-matrix-synapse:8008 - name: MAS_ADMIN_CLIENT_ID value: 01KDXMVQBQ5JNY6SEJPZW6Z8BM - name: MAS_ADMIN_CLIENT_SECRET_FILE value: /etc/mas-admin-client/client_secret - name: MAS_ADMIN_API_BASE value: http://matrix-authentication-service:8081/api/admin/v1 - name: MAS_TOKEN_URL value: http://matrix-authentication-service:8080/oauth2/token - name: SEEDER_USER value: othrys-seeder command: - /bin/sh - -c - | set -euo pipefail pip install --no-cache-dir requests >/dev/null python - <<'PY' import base64 import os import random import requests import urllib.parse ADJ = [ "brisk","calm","eager","gentle","merry","nifty","rapid","sunny","witty","zesty", "amber","bold","bright","crisp","daring","frosty","glad","jolly","lively","mellow", "quiet","ripe","serene","spry","tidy","vivid","warm","wild","clever","kind", ] NOUN = [ "otter","falcon","comet","ember","grove","harbor","meadow","raven","river","summit", "breeze","cedar","cinder","cove","delta","forest","glade","lark","marsh","peak", "pine","quartz","reef","ridge","sable","sage","shore","thunder","vale","zephyr", ] BASE = os.environ["SYNAPSE_BASE"] MAS_ADMIN_CLIENT_ID = os.environ["MAS_ADMIN_CLIENT_ID"] MAS_ADMIN_CLIENT_SECRET_FILE = os.environ["MAS_ADMIN_CLIENT_SECRET_FILE"] MAS_ADMIN_API_BASE = os.environ["MAS_ADMIN_API_BASE"].rstrip("/") MAS_TOKEN_URL = os.environ["MAS_TOKEN_URL"] SEEDER_USER = os.environ["SEEDER_USER"] ROOM_ALIAS = "#othrys:live.bstein.dev" def mas_admin_token(): with open(MAS_ADMIN_CLIENT_SECRET_FILE, "r", encoding="utf-8") as f: secret = f.read().strip() basic = base64.b64encode(f"{MAS_ADMIN_CLIENT_ID}:{secret}".encode()).decode() r = requests.post( MAS_TOKEN_URL, headers={"Authorization": f"Basic {basic}"}, data={"grant_type": "client_credentials", "scope": "urn:mas:admin"}, timeout=30, ) r.raise_for_status() return r.json()["access_token"] def mas_user_id(token, username): r = requests.get( f"{MAS_ADMIN_API_BASE}/users/by-username/{urllib.parse.quote(username)}", headers={"Authorization": f"Bearer {token}"}, timeout=30, ) r.raise_for_status() return r.json()["data"]["id"] def mas_personal_session(token, user_id): r = requests.post( f"{MAS_ADMIN_API_BASE}/personal-sessions", headers={"Authorization": f"Bearer {token}"}, json={ "actor_user_id": user_id, "human_name": "guest-name-randomizer", "scope": "urn:matrix:client:api:*", "expires_in": 300, }, timeout=30, ) r.raise_for_status() data = r.json().get("data", {}).get("attributes", {}) or {} return data["access_token"], r.json()["data"]["id"] def mas_revoke_session(token, session_id): requests.post( f"{MAS_ADMIN_API_BASE}/personal-sessions/{urllib.parse.quote(session_id)}/revoke", headers={"Authorization": f"Bearer {token}"}, json={}, timeout=30, ) def resolve_alias(token, alias): headers = {"Authorization": f"Bearer {token}"} enc = urllib.parse.quote(alias) r = requests.get(f"{BASE}/_matrix/client/v3/directory/room/{enc}", headers=headers) r.raise_for_status() return r.json()["room_id"] def list_guests(token): headers = {"Authorization": f"Bearer {token}"} users = [] existing_names = set() from_token = None while True: url = f"{BASE}/_synapse/admin/v2/users?local=true&deactivated=false&limit=100" if from_token: url += f"&from={from_token}" res = requests.get(url, headers=headers) res.raise_for_status() data = res.json() for u in data.get("users", []): disp = u.get("displayname", "") if disp: existing_names.add(disp) if u.get("is_guest") and (not disp or disp.isdigit()): users.append(u["name"]) from_token = data.get("next_token") if not from_token: break return users, existing_names def set_displayname(token, room_id, user_id, name): headers = {"Authorization": f"Bearer {token}"} payload = {"displayname": name} # Update global profile r = requests.put(f"{BASE}/_matrix/client/v3/profile/{urllib.parse.quote(user_id)}/displayname", headers=headers, json=payload) r.raise_for_status() # Update Othrys member event so clients see the change quickly state_url = f"{BASE}/_matrix/client/v3/rooms/{urllib.parse.quote(room_id)}/state/m.room.member/{urllib.parse.quote(user_id)}" r2 = requests.get(state_url, headers=headers) content = r2.json() if r2.status_code == 200 else {"membership": "join"} content["displayname"] = name requests.put(state_url, headers=headers, json=content) admin_token = mas_admin_token() seeder_id = mas_user_id(admin_token, SEEDER_USER) token, session_id = mas_personal_session(admin_token, seeder_id) try: room_id = resolve_alias(token, ROOM_ALIAS) guests, existing = list_guests(token) for g in guests: new = None for _ in range(30): candidate = f"{random.choice(ADJ)}-{random.choice(NOUN)}" if candidate not in existing: new = candidate existing.add(candidate) break if not new: continue set_displayname(token, room_id, g, new) finally: mas_revoke_session(admin_token, session_id) PY