diff --git a/services/communication/guest-name-job.yaml b/services/communication/guest-name-job.yaml index 6bd0761..56df3f2 100644 --- a/services/communication/guest-name-job.yaml +++ b/services/communication/guest-name-job.yaml @@ -6,28 +6,40 @@ metadata: namespace: comms spec: schedule: "*/1 * * * *" - suspend: true + 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: AUTH_BASE - value: http://matrix-authentication-service:8080 + - 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 - - name: SEEDER_PASS - valueFrom: - secretKeyRef: - name: atlasbot-credentials-runtime - key: seeder-password command: - /bin/sh - -c @@ -35,24 +47,77 @@ spec: set -euo pipefail pip install --no-cache-dir requests >/dev/null python - <<'PY' - import os, random, requests, urllib.parse + import base64 + import os + import random + import requests + import urllib.parse - ADJ = ["brisk","calm","eager","gentle","merry","nifty","rapid","sunny","witty","zesty"] - NOUN = ["otter","falcon","comet","ember","grove","harbor","meadow","raven","river","summit"] + 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"] - AUTH_BASE = os.environ.get("AUTH_BASE", 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 login(user, password): - r = requests.post(f"{AUTH_BASE}/_matrix/client/v3/login", json={ - "type": "m.login.password", - "identifier": {"type": "m.id.user", "user": user}, - "password": password, - }) + 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) @@ -63,6 +128,7 @@ spec: 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" @@ -73,12 +139,14 @@ spec: 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 + return users, existing_names def set_displayname(token, room_id, user_id, name): headers = {"Authorization": f"Bearer {token}"} @@ -93,10 +161,23 @@ spec: content["displayname"] = name requests.put(state_url, headers=headers, json=content) - token = login(os.environ["SEEDER_USER"], os.environ["SEEDER_PASS"]) - room_id = resolve_alias(token, ROOM_ALIAS) - guests = list_guests(token) - for g in guests: - new = f"{random.choice(ADJ)}-{random.choice(NOUN)}" - set_displayname(token, room_id, g, new) + 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