From 6759817518c68bb5f0298e7170ede2f736b689aa Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 31 Dec 2025 12:15:18 -0300 Subject: [PATCH] communication: stage guest-helper for prune --- services/communication/guest-helper.yaml | 239 ++++++++++++++++++ services/communication/kustomization.yaml | 2 + .../synapse-federation-service.yaml | 15 ++ 3 files changed, 256 insertions(+) create mode 100644 services/communication/guest-helper.yaml create mode 100644 services/communication/synapse-federation-service.yaml diff --git a/services/communication/guest-helper.yaml b/services/communication/guest-helper.yaml new file mode 100644 index 0000000..a390b75 --- /dev/null +++ b/services/communication/guest-helper.yaml @@ -0,0 +1,239 @@ +# services/communication/guest-helper.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: guest-helper +data: + app.py: | + import os, uuid, string, random + import requests + from fastapi import FastAPI, HTTPException, Header + from pydantic import BaseModel + import uvicorn + + BASE = os.environ.get("SYNAPSE_BASE", "http://othrys-synapse-matrix-synapse:8008") + SEED_USER = os.environ["SEEDER_USER"] + SEED_PASS = os.environ["SEEDER_PASS"] + SERVER_NAME = os.environ.get("SERVER_NAME", "live.bstein.dev") + ELEMENT_URL = os.environ.get("ELEMENT_URL", "https://live.bstein.dev") + ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN") + + app = FastAPI(title="Guest Helper", version="0.1.0") + + + class InviteRequest(BaseModel): + room: str # room_id or alias + display_name: str | None = None + + + def login(user, password): + res = requests.post( + f"{BASE}/_matrix/client/v3/login", + json={ + "type": "m.login.password", + "identifier": {"type": "m.id.user", "user": user}, + "password": password, + }, + timeout=10, + ) + if res.status_code != 200: + raise HTTPException(status_code=500, detail="seeder login failed") + return res.json()["access_token"] + + + def resolve_room(token, room): + headers = {"Authorization": f"Bearer {token}"} + if room.startswith("!"): + return room + if room.startswith("#"): + alias_enc = requests.utils.requote_uri(room) + r = requests.get(f"{BASE}/_matrix/client/v3/directory/room/{alias_enc}", headers=headers, timeout=10) + if r.status_code != 200: + raise HTTPException(status_code=400, detail="room alias not found") + return r.json()["room_id"] + raise HTTPException(status_code=400, detail="room must be room_id or alias") + + + def random_pwd(): + alphabet = string.ascii_letters + string.digits + return "".join(random.choice(alphabet) for _ in range(20)) + + + def create_guest(token, display): + uid = f"@guest-{uuid.uuid4().hex[:8]}:{SERVER_NAME}" + pwd = random_pwd() + headers = {"Authorization": f"Bearer {token}"} + body = { + "password": pwd, + "displayname": display or "Guest", + "admin": False, + "deactivated": False, + } + r = requests.put( + f"{BASE}/_synapse/admin/v2/users/{requests.utils.requote_uri(uid)}", + headers=headers, + json=body, + timeout=10, + ) + if r.status_code not in (200, 201): + raise HTTPException(status_code=500, detail=f"user create failed: {r.text}") + return uid, pwd + + + def join_room_as(token, room_id, user_id): + headers = {"Authorization": f"Bearer {token}"} + r = requests.post( + f"{BASE}/_synapse/admin/v1/join/{requests.utils.requote_uri(room_id)}", + headers=headers, + json={"user_id": user_id}, + timeout=10, + ) + if r.status_code not in (200, 202): + raise HTTPException(status_code=500, detail=f"join failed: {r.text}") + + + def login_token(user, password): + r = requests.post( + f"{BASE}/_matrix/client/v3/login", + json={ + "type": "m.login.password", + "identifier": {"type": "m.id.user", "user": user}, + "password": password, + }, + timeout=10, + ) + if r.status_code != 200: + raise HTTPException(status_code=500, detail="guest login failed") + data = r.json() + return data["access_token"] + + + @app.post("/invite") + def invite(req: InviteRequest, x_admin_token: str | None = Header(default=None)): + if ADMIN_TOKEN and x_admin_token != ADMIN_TOKEN: + raise HTTPException(status_code=401, detail="unauthorized") + admin_token = login(SEED_USER, SEED_PASS) + room_id = resolve_room(admin_token, req.room) + guest_id, pwd = create_guest(admin_token, req.display_name) + join_room_as(admin_token, room_id, guest_id) + guest_token = login_token(guest_id, pwd) + join_url = f"{ELEMENT_URL}/#/room/{room_id}?access_token={guest_token}&user_id={guest_id}" + return { + "user_id": guest_id, + "password": pwd, + "room_id": room_id, + "access_token": guest_token, + "join_url": join_url, + } + + + def main(): + uvicorn.run(app, host="0.0.0.0", port=8081) + + + if __name__ == "__main__": + main() +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: guest-helper + labels: + app: guest-helper +spec: + replicas: 1 + selector: + matchLabels: + app: guest-helper + template: + metadata: + labels: + app: guest-helper + spec: + nodeSelector: + hardware: rpi5 + containers: + - name: api + image: python:3.11-slim + command: + - /bin/sh + - -c + - | + pip install --no-cache-dir fastapi uvicorn requests && \ + python /app/app.py + env: + - name: SYNAPSE_BASE + value: http://othrys-synapse-matrix-synapse:8008 + - name: SEEDER_USER + value: othrys-seeder + - name: SEEDER_PASS + valueFrom: + secretKeyRef: + name: atlasbot-credentials + key: seeder-password + - name: SERVER_NAME + value: live.bstein.dev + - name: ELEMENT_URL + value: https://live.bstein.dev + - name: ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: guest-helper-admin + key: ADMIN_TOKEN + optional: true + ports: + - name: http + containerPort: 8081 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 300m + memory: 256Mi + volumeMounts: + - name: code + mountPath: /app/app.py + subPath: app.py + volumes: + - name: code + configMap: + name: guest-helper +--- +apiVersion: v1 +kind: Service +metadata: + name: guest-helper +spec: + selector: + app: guest-helper + ports: + - name: http + port: 8081 + targetPort: 8081 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: guest-helper + annotations: + kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + cert-manager.io/cluster-issuer: letsencrypt +spec: + tls: + - hosts: + - live.bstein.dev + secretName: live-othrys-tls + rules: + - host: live.bstein.dev + http: + paths: + - path: /guest-helper + pathType: Prefix + backend: + service: + name: guest-helper + port: + number: 8081 diff --git a/services/communication/kustomization.yaml b/services/communication/kustomization.yaml index 9cd7f38..3c5f115 100644 --- a/services/communication/kustomization.yaml +++ b/services/communication/kustomization.yaml @@ -22,3 +22,5 @@ resources: - atlasbot-deployment.yaml - seed-othrys-room.yaml - wellknown.yaml + - synapse-federation-service.yaml + - guest-helper.yaml diff --git a/services/communication/synapse-federation-service.yaml b/services/communication/synapse-federation-service.yaml new file mode 100644 index 0000000..5417c9e --- /dev/null +++ b/services/communication/synapse-federation-service.yaml @@ -0,0 +1,15 @@ +# services/communication/synapse-federation-service.yaml +apiVersion: v1 +kind: Service +metadata: + name: synapse-federation +spec: + clusterIP: 10.43.60.60 + selector: + app.kubernetes.io/name: matrix-synapse + app.kubernetes.io/instance: othrys-synapse + ports: + - name: federation + port: 8448 + targetPort: 8008 + protocol: TCP