From dca01199ce45079c34261fd4a0d83cc7b37b2e01 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Thu, 1 Jan 2026 16:29:11 -0300 Subject: [PATCH] comms: reset othrys room --- .../communication/atlasbot-deployment.yaml | 4 +- services/communication/guest-name-job.yaml | 18 +- services/communication/kustomization.yaml | 1 + services/communication/pin-othrys-job.yaml | 2 +- .../communication/reset-othrys-room-job.yaml | 231 ++++++++++++++++++ services/communication/seed-othrys-room.yaml | 4 +- 6 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 services/communication/reset-othrys-room-job.yaml diff --git a/services/communication/atlasbot-deployment.yaml b/services/communication/atlasbot-deployment.yaml index fbb9b3d..9778005 100644 --- a/services/communication/atlasbot-deployment.yaml +++ b/services/communication/atlasbot-deployment.yaml @@ -33,7 +33,9 @@ spec: - name: AUTH_BASE value: http://matrix-authentication-service:8080 - name: BOT_USER - value: atlasbot + value: atlas + - name: BOT_MENTIONS + value: atlas - name: BOT_PASS valueFrom: secretKeyRef: diff --git a/services/communication/guest-name-job.yaml b/services/communication/guest-name-job.yaml index 8d8149e..6bd0761 100644 --- a/services/communication/guest-name-job.yaml +++ b/services/communication/guest-name-job.yaml @@ -3,7 +3,7 @@ apiVersion: batch/v1 kind: CronJob metadata: name: guest-name-randomizer - namespace: communication + namespace: comms spec: schedule: "*/1 * * * *" suspend: true @@ -42,7 +42,7 @@ spec: BASE = os.environ["SYNAPSE_BASE"] AUTH_BASE = os.environ.get("AUTH_BASE", BASE) - OTHRYS = "!orejZnVfvbAmwQDYba:live.bstein.dev" + ROOM_ALIAS = "#othrys:live.bstein.dev" def login(user, password): r = requests.post(f"{AUTH_BASE}/_matrix/client/v3/login", json={ @@ -53,6 +53,13 @@ spec: r.raise_for_status() return r.json()["access_token"] + 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 = [] @@ -73,22 +80,23 @@ spec: break return users - def set_displayname(token, user_id, name): + 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(OTHRYS)}/state/m.room.member/{urllib.parse.quote(user_id)}" + 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) 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, g, new) + set_displayname(token, room_id, g, new) PY diff --git a/services/communication/kustomization.yaml b/services/communication/kustomization.yaml index 1b8f17a..b4d5eb4 100644 --- a/services/communication/kustomization.yaml +++ b/services/communication/kustomization.yaml @@ -17,6 +17,7 @@ resources: - livekit-middlewares.yaml - element-call-config.yaml - element-call-deployment.yaml + - reset-othrys-room-job.yaml - pin-othrys-job.yaml - guest-name-job.yaml - atlasbot-configmap.yaml diff --git a/services/communication/pin-othrys-job.yaml b/services/communication/pin-othrys-job.yaml index a45f37a..b0a4c4d 100644 --- a/services/communication/pin-othrys-job.yaml +++ b/services/communication/pin-othrys-job.yaml @@ -3,7 +3,7 @@ apiVersion: batch/v1 kind: CronJob metadata: name: pin-othrys-invite - namespace: communication + namespace: comms spec: schedule: "*/30 * * * *" suspend: true diff --git a/services/communication/reset-othrys-room-job.yaml b/services/communication/reset-othrys-room-job.yaml new file mode 100644 index 0000000..ddcc0a7 --- /dev/null +++ b/services/communication/reset-othrys-room-job.yaml @@ -0,0 +1,231 @@ +# services/communication/reset-othrys-room-job.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: othrys-room-reset-1 + namespace: comms +spec: + backoffLimit: 0 + template: + spec: + restartPolicy: Never + containers: + - name: reset + image: python:3.11-slim + env: + - name: SYNAPSE_BASE + value: http://othrys-synapse-matrix-synapse:8008 + - name: AUTH_BASE + value: http://matrix-authentication-service:8080 + - name: SERVER_NAME + value: live.bstein.dev + - name: ROOM_ALIAS + value: "#othrys:live.bstein.dev" + - name: ROOM_NAME + value: Othrys + - name: PIN_MESSAGE + value: "Invite guests: share https://live.bstein.dev/#/room/#othrys:live.bstein.dev?action=join and choose 'Continue' -> 'Join as guest'." + - name: SEEDER_USER + value: othrys-seeder + - name: SEEDER_PASS + valueFrom: + secretKeyRef: + name: atlasbot-credentials-runtime + key: seeder-password + - name: BOT_USER + value: atlas + - name: BOT_PASS + valueFrom: + secretKeyRef: + name: atlasbot-credentials-runtime + key: bot-password + command: + - /bin/sh + - -c + - | + set -euo pipefail + pip install --no-cache-dir requests >/dev/null + python - <<'PY' + import os, sys, urllib.parse, requests + + BASE = os.environ["SYNAPSE_BASE"] + AUTH_BASE = os.environ.get("AUTH_BASE", BASE) + SERVER_NAME = os.environ.get("SERVER_NAME", "live.bstein.dev") + ROOM_ALIAS = os.environ.get("ROOM_ALIAS", "#othrys:live.bstein.dev") + ROOM_NAME = os.environ.get("ROOM_NAME", "Othrys") + PIN_MESSAGE = os.environ["PIN_MESSAGE"] + SEEDER_USER = os.environ["SEEDER_USER"] + SEEDER_PASS = os.environ["SEEDER_PASS"] + BOT_USER = os.environ["BOT_USER"] + BOT_PASS = os.environ["BOT_PASS"] + + POWER_LEVELS = { + "ban": 50, + "events": { + "m.room.avatar": 50, + "m.room.canonical_alias": 50, + "m.room.encryption": 100, + "m.room.history_visibility": 100, + "m.room.name": 50, + "m.room.power_levels": 100, + "m.room.server_acl": 100, + "m.room.tombstone": 100, + }, + "events_default": 0, + "historical": 100, + "invite": 50, + "kick": 50, + "m.call.invite": 50, + "redact": 50, + "state_default": 50, + "users": {f"@{SEEDER_USER}:{SERVER_NAME}": 100}, + "users_default": 0, + } + + def auth(token): return {"Authorization": f"Bearer {token}"} + + 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, + }) + if r.status_code != 200: + raise SystemExit(f"login failed: {r.status_code} {r.text}") + return r.json()["access_token"] + + def resolve_alias(token, alias): + enc = urllib.parse.quote(alias) + r = requests.get(f"{BASE}/_matrix/client/v3/directory/room/{enc}", headers=auth(token)) + if r.status_code == 404: + return None + r.raise_for_status() + return r.json()["room_id"] + + def ensure_user(token, localpart, password, admin): + user_id = f"@{localpart}:{SERVER_NAME}" + url = f"{BASE}/_synapse/admin/v2/users/{urllib.parse.quote(user_id)}" + payload = {"password": password, "admin": admin, "deactivated": False} + r = requests.put(url, headers=auth(token), json=payload) + if r.status_code not in (200, 201): + raise SystemExit(f"ensure user {user_id} failed: {r.status_code} {r.text}") + return user_id + + def create_room(token): + r = requests.post(f"{BASE}/_matrix/client/v3/createRoom", headers=auth(token), json={ + "preset": "public_chat", + "name": ROOM_NAME, + "room_version": "11", + }) + r.raise_for_status() + return r.json()["room_id"] + + def put_state(token, room_id, ev_type, content): + r = requests.put( + f"{BASE}/_matrix/client/v3/rooms/{urllib.parse.quote(room_id)}/state/{ev_type}", + headers=auth(token), + json=content, + ) + r.raise_for_status() + + def set_directory_visibility(token, room_id, visibility): + r = requests.put( + f"{BASE}/_matrix/client/v3/directory/list/room/{urllib.parse.quote(room_id)}", + headers=auth(token), + json={"visibility": visibility}, + ) + r.raise_for_status() + + def delete_alias(token, alias): + enc = urllib.parse.quote(alias) + r = requests.delete(f"{BASE}/_matrix/client/v3/directory/room/{enc}", headers=auth(token)) + if r.status_code in (200, 202, 404): + return + r.raise_for_status() + + def put_alias(token, alias, room_id): + enc = urllib.parse.quote(alias) + r = requests.put( + f"{BASE}/_matrix/client/v3/directory/room/{enc}", + headers=auth(token), + json={"room_id": room_id}, + ) + r.raise_for_status() + + def admin_join(token, room_id, user_id): + r = requests.post( + f"{BASE}/_synapse/admin/v1/join/{urllib.parse.quote(room_id)}", + headers=auth(token), + json={"user_id": user_id}, + ) + r.raise_for_status() + + def join_all_locals(token, room_id): + users = [] + 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=auth(token)) + res.raise_for_status() + data = res.json() + for u in data.get("users", []): + if u.get("is_guest"): + continue + users.append(u["name"]) + from_token = data.get("next_token") + if not from_token: + break + for uid in users: + admin_join(token, room_id, uid) + + def send_message(token, room_id, body): + r = requests.post( + f"{BASE}/_matrix/client/v3/rooms/{urllib.parse.quote(room_id)}/send/m.room.message", + headers=auth(token), + json={"msgtype": "m.text", "body": body}, + ) + r.raise_for_status() + return r.json()["event_id"] + + token = login(SEEDER_USER, SEEDER_PASS) + + old_room_id = resolve_alias(token, ROOM_ALIAS) + if not old_room_id: + raise SystemExit(f"alias {ROOM_ALIAS} not found; refusing to proceed") + + bot_user_id = ensure_user(token, BOT_USER, BOT_PASS, admin=False) + + new_room_id = create_room(token) + + # Configure the new room. + put_state(token, new_room_id, "m.room.join_rules", {"join_rule": "public"}) + put_state(token, new_room_id, "m.room.guest_access", {"guest_access": "can_join"}) + put_state(token, new_room_id, "m.room.history_visibility", {"history_visibility": "shared"}) + put_state(token, new_room_id, "m.room.power_levels", POWER_LEVELS) + + # Move the alias. + delete_alias(token, ROOM_ALIAS) + put_alias(token, ROOM_ALIAS, new_room_id) + put_state(token, new_room_id, "m.room.canonical_alias", {"alias": ROOM_ALIAS}) + + set_directory_visibility(token, new_room_id, "public") + + # Join the bot and all local non-guest users. + admin_join(token, new_room_id, bot_user_id) + join_all_locals(token, new_room_id) + + # Pin the guest invite message in the new room. + event_id = send_message(token, new_room_id, PIN_MESSAGE) + put_state(token, new_room_id, "m.room.pinned_events", {"pinned": [event_id]}) + + # De-list and tombstone the old room. + set_directory_visibility(token, old_room_id, "private") + put_state(token, old_room_id, "m.room.join_rules", {"join_rule": "invite"}) + put_state(token, old_room_id, "m.room.guest_access", {"guest_access": "forbidden"}) + put_state(token, old_room_id, "m.room.tombstone", {"body": "Othrys has been reset. Please join the new room.", "replacement_room": new_room_id}) + + print(f"old_room_id={old_room_id}") + print(f"new_room_id={new_room_id}") + PY diff --git a/services/communication/seed-othrys-room.yaml b/services/communication/seed-othrys-room.yaml index 06be4fb..a54f0de 100644 --- a/services/communication/seed-othrys-room.yaml +++ b/services/communication/seed-othrys-room.yaml @@ -3,7 +3,7 @@ apiVersion: batch/v1 kind: CronJob metadata: name: seed-othrys-room - namespace: communication + namespace: comms spec: schedule: "*/10 * * * *" suspend: true @@ -30,7 +30,7 @@ spec: name: atlasbot-credentials-runtime key: seeder-password - name: BOT_USER - value: atlasbot + value: atlas - name: BOT_PASS valueFrom: secretKeyRef: