import re from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse from .. import audit from ..chat import MATRIX_ROOM_ID_RE, MATRIX_USER_ID_RE, MatrixAdapter, mask_secret from ..constants import (FLAG_OFF, FLAG_ON, MATRIX_ALLOWLIST, MATRIX_CONTENT_MODE, MATRIX_ENABLED, MATRIX_HOMESERVER, MATRIX_ROOM_ID, MATRIX_TOKEN, MATRIX_USER_ID) from ..users import User from .content_mode import content_mode_field, validate_content_mode from .deps import require_admin from .sections import URL_RE, Action, Field, Section, check, collect, effective router = APIRouter() SLUG = "matrix" MESSAGE_PATH = f"/settings/{SLUG}/message" TEST_MESSAGE_TEXT = "Testnachricht von checkbot" ERROR_DETAIL_MAX = 300 WHITESPACE_RE = re.compile(r"\s") def validate(values: dict) -> list[str]: errors = [] homeserver, user_id = values.get(MATRIX_HOMESERVER, ""), values.get(MATRIX_USER_ID, "") token, room_id = values.get(MATRIX_TOKEN, ""), values.get(MATRIX_ROOM_ID, "") if values.get(MATRIX_ENABLED) == FLAG_ON: for value, label in ((homeserver, "Homeserver-URL"), (user_id, "Bot-Benutzer-ID"), (token, "Access-Token"), (room_id, "Standard-Raum-ID")): if not value: errors.append(f"{label} fehlt.") if homeserver and not URL_RE.match(homeserver): errors.append("Homeserver-URL muss mit http:// oder https:// beginnen.") if user_id and not MATRIX_USER_ID_RE.match(user_id): errors.append("Benutzer-ID muss die Form @name:server haben.") if token and WHITESPACE_RE.search(token): errors.append("Access-Token darf keine Leerzeichen enthalten.") if room_id and not MATRIX_ROOM_ID_RE.match(room_id): errors.append("Raum-ID muss mit ! beginnen (nicht der Alias).") allowlist = values.get(MATRIX_ALLOWLIST, "") if allowlist: entries = [e.strip() for e in allowlist.split(",")] if not all(MATRIX_USER_ID_RE.match(e) for e in entries): errors.append("Allowlist darf nur Matrix-IDs (@name:server), durch Kommas getrennt, enthalten.") return errors + validate_content_mode(values, MATRIX_CONTENT_MODE) def _adapter(values: dict) -> MatrixAdapter: for key, label in ((MATRIX_HOMESERVER, "Homeserver-URL"), (MATRIX_USER_ID, "Bot-Benutzer-ID"), (MATRIX_TOKEN, "Access-Token")): if not values.get(key): raise ValueError(f"{label} fehlt.") return MatrixAdapter(values[MATRIX_HOMESERVER], values[MATRIX_USER_ID], values[MATRIX_TOKEN], values.get(MATRIX_ROOM_ID, "")) async def test_connection(values: dict) -> str: adapter = _adapter(values) try: return await adapter.check() finally: await adapter.aclose() SECTION = Section( slug=SLUG, title="Matrix", fields=( Field(MATRIX_ENABLED, "Matrix aktivieren", kind="checkbox", default=FLAG_OFF), Field(MATRIX_HOMESERVER, "Homeserver-URL", placeholder="https://matrix.example.de"), Field(MATRIX_USER_ID, "Bot-Benutzer-ID", placeholder="@checkbot:example.de"), Field(MATRIX_TOKEN, "Access-Token des Bot-Users", kind="password", secret=True), Field(MATRIX_ROOM_ID, "Standard-Raum-ID (unverschlüsselt)", placeholder="!abcdef:example.de"), Field(MATRIX_ALLOWLIST, "Erlaubte Matrix-IDs (kommagetrennt)", placeholder="@admin:example.de"), content_mode_field(MATRIX_CONTENT_MODE, "Mit \"kurz\" enthalten Nachrichten keinen Plugin-Output und keine KI-Texte; " "die KI-Analyse wird dann nicht nachgereicht."), ), validate=validate, test=test_connection, optional=True, enabled_key=MATRIX_ENABLED, actions=(Action("Testnachricht senden", MESSAGE_PATH),), ) @router.post(MESSAGE_PATH) async def send_test_message(request: Request, user: User = Depends(require_admin)): store = request.app.state.store values = effective(store, collect(await request.form(), SECTION)) errors = check(SECTION, values) if errors: return JSONResponse({"ok": False, "detail": " ".join(errors)}) if not values.get(MATRIX_ROOM_ID): return JSONResponse({"ok": False, "detail": "Standard-Raum-ID fehlt."}) try: adapter = _adapter(values) except ValueError as exc: return JSONResponse({"ok": False, "detail": str(exc)}) try: await adapter.send(adapter.default_target, TEST_MESSAGE_TEXT) except Exception as exc: detail = mask_secret(f"{type(exc).__name__}: {exc}", values[MATRIX_TOKEN]) return JSONResponse({"ok": False, "detail": detail[:ERROR_DETAIL_MAX]}) finally: await adapter.aclose() audit.record(request.app.state.db, user.username, "matrix.test_message", values[MATRIX_ROOM_ID]) return JSONResponse({"ok": True, "detail": "Testnachricht gesendet."})