# 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