# Matrix Chat Widget – Dokumentation **Projekt:** Öffentliche Leseanzeige eines Matrix-Raums auf einer WordPress-Seite **Homeserver:** `matrix.rpi-virtuell.de` **Element-Client:** `element.rpi-virtuell.de` **Raum-ID:** `!NQPmoqtLSjGzdtLaXO:rpi-virtuell.de` **Stand:** März 2026 --- ## Kontext und Problemlage Ziel war es, einen öffentlichen Matrix-Raum als Leseanzeige (kein Login für Besucher:innen erforderlich) in eine WordPress-Seite einzubetten. Dabei wurden folgende Wege geprüft und verworfen: **Matrix Public Archive / Matrix Viewer** – wurde eingestellt. **matrix-live (live.hello-matrix.net)** – funktioniert nur mit Homeservern, die Guest Access aktiviert haben. **Guest Access** – auf `matrix.rpi-virtuell.de` nicht aktiviert; auf `matrix.org` seit Januar 2025 abgeschaltet. **Element Web als iFrame** – `app.element.io` und die meisten selbst gehosteten Instanzen setzen `X-Frame-Options: DENY` und können nicht eingebettet werden. **Gewählte Lösung:** Ein eigenständiges HTML/JS-Widget, das direkt die Matrix Client-Server API abfragt. Voraussetzung ist ein dedizierter Bot-Account mit Lesezugriff, dessen Token fest im Widget hinterlegt ist. Das ist unkritisch, da der Token nur Lesezugriff auf einen öffentlichen Raum hat. --- ## Voraussetzungen ### 1. Raum auf „world_readable" setzen In Element: **Raumeinstellungen → Sicherheit & Datenschutz → „Wer kann den Verlauf lesen?" → „Jeder"** Ohne diese Einstellung antwortet die API mit HTTP 403. ### 2. Bot-Account anlegen und Token holen Einen dedizierten Account (z. B. `@openrelibot:rpi-virtuell.de`) anlegen und einmalig per API einloggen: ```bash curl -X POST https://matrix.rpi-virtuell.de/_matrix/client/v3/login \ -H "Content-Type: application/json" \ -d '{"type":"m.login.password","user":"openrelibot","password":"PASSWORT"}' ``` Die Antwort enthält `access_token` – dieser wird im Widget als `TOKEN` eingetragen. > **Hinweis:** Da der Token in einem Chatprotokoll sichtbar war, sollte er bei nächster Gelegenheit rotiert werden (siehe Abschnitt Token-Rotation). --- ## Einbindung in WordPress Gutenberg-Editor → Block **„Benutzerdefiniertes HTML"** → vollständigen Widget-Code einfügen → Speichern. Kein Plugin erforderlich. Der Code ist vollständig eigenständig (kein externes CDN, keine Abhängigkeiten). --- ## Vollständiger Code ```html
Chat wird geladen…
``` --- ## Technische Dokumentation ### Architektur Das Widget ist eine einzelne, vollständig eigenständige HTML-Datei ohne externe Abhängigkeiten. Es kommuniziert direkt mit der Matrix Client-Server API (v3) des Homeservers per `fetch()`. ``` Browser → fetch() → matrix.rpi-virtuell.de/_matrix/client/v3/... → /_matrix/media/v3/download/... (Bilder) ``` ### Verwendete API-Endpunkte | Endpunkt | Zweck | |---|---| | `GET /rooms/{roomId}/state/m.room.name/` | Raumname laden | | `GET /rooms/{roomId}/state/m.room.avatar/` | Raumlogo laden (mxc-URL) | | `GET /rooms/{roomId}/messages?dir=b&limit=N` | Letzte Nachrichten abrufen | | `GET /profile/{userId}/avatar_url` | Benutzer-Avatar laden (mxc-URL) | | `GET /_matrix/media/v3/download/{server}/{id}` | Mediendatei (Bild) abrufen | Alle Requests senden den `Authorization: Bearer {TOKEN}` Header, da der Homeserver auch für world-readable Räume Authentifizierung verlangt (HTTP 401 ohne Token). ### Konfigurationsparameter ```javascript const HOMESERVER = 'https://matrix.rpi-virtuell.de'; // Matrix-Homeserver const ROOM_ID = '!NQPmoqtLSjGzdtLaXO:rpi-virtuell.de'; // Raum-ID const LIMIT = 40; // Anzahl angezeigter Top-Level-Nachrichten const POLL_SEC = 30; // Polling-Intervall in Sekunden const TOKEN = '...'; // Access Token des Bot-Accounts ``` ### Nachrichten-Typen und Darstellung Das Widget unterscheidet vier Typen von Events anhand des `m.relates_to`-Feldes: **Normale Nachricht** – kein `m.relates_to` → wird auf Top-Level gerendert. **Bearbeitete Nachricht** (`rel_type: "m.replace"`) → wird nicht als separate Nachricht angezeigt. Stattdessen überschreibt das neueste Replace-Event den Inhalt der Originalnachricht. In der Zeitzeile erscheint kursiv *(bearbeitet)*. **Klassische Reply** (`m.in_reply_to`, kein `rel_type: m.thread`) → wird um 46 px eingerückt dargestellt, mit grauer linker Borderlinie und einer grünen Zitiervorschau des Originaltexts. Element bettet den zitierten Text als `> Zitat\n\nAntwort` in den `body` ein – das Widget extrahiert dieses Muster per `split('\n\n')`. **Thread-Reply** (`rel_type: "m.thread"`, `event_id` = Root-Event-ID) → wird unter der Root-Nachricht gesammelt und erst nach Klick auf den Toggle-Button sichtbar. Der aufgeklappte Thread-Container hat eine grüne linke Borderlinie und kleinere Avatare (26 px statt 36 px). ### Rendering-Pipeline ``` fetchMessages() → API: /messages?dir=b&limit=120 → events.reverse() // chronologisch sortieren → Edit-Deduplizierung: latestEdit{} sammeln (m.replace) Replace-Events herausfiltern Originale mit m.new_content patchen + _edited:true → patchedEvents.slice(-40) // letzte 40 behalten → getUserAvatarUrl() parallel // Avatare vorladen → Sortierung: byId{}, threads{}, topLevel[] → topLevel.forEach → buildMsgEl() → threads[e.event_id] → Toggle + threadEl → Orphan-Thread-Kinder anhängen ``` Das Ladefenster (`limit = LIMIT * 3 = 120`) ist größer als die angezeigten 40 Nachrichten, um sicherzustellen, dass zu den angezeigten Top-Level-Nachrichten auch ihre zugehörigen Edit- und Thread-Events im selben API-Response enthalten sind. ### mxc-URLs Matrix speichert Mediendateien intern als `mxc://server/id`. Die Funktion `mxcToHttp()` konvertiert diese in reguläre HTTPS-URLs: ``` mxc://rpi-virtuell.de/AbcXyz → https://matrix.rpi-virtuell.de/_matrix/media/v3/download/rpi-virtuell.de/AbcXyz ``` ### Avatar-Caching Um bei jedem Polling-Zyklus unnötige API-Aufrufe zu vermeiden, werden User-Avatare in einem In-Memory-Objekt `avatarCache` gespeichert (`mxid → URL | null`). Pro Rendering-Durchlauf werden alle einzigartigen Absender parallel vorgeladen (`Promise.all`), bevor die Nachrichten gerendert werden. ### Fallback-Logik für Avatare Ist kein Profilbild hinterlegt oder schlägt das Laden fehl (`onerror`), zeigt das Widget einen farbigen Kreis mit den ersten zwei Buchstaben des Localparts. Die Farbe wird deterministisch per Hash-Funktion aus der vollständigen MXID berechnet – dieselbe Person hat immer dieselbe Farbe. ### XSS-Schutz Alle aus der API stammenden Texte (Absendername, Nachrichtentext, Zitiervorschau) werden durch `escapeHtml()` bereinigt, bevor sie per `innerHTML` eingefügt werden. Bilder werden nur als `` eingebettet, nie als HTML aus dem Event-Content übernommen. ### Statusanzeige Der kleine Punkt oben rechts im Header zeigt den Verbindungsstatus: - 🟡 pulsierend – Ladevorgang - 🟢 grün – letzter API-Aufruf erfolgreich - 🔴 rot – Fehler (403 Forbidden oder Netzwerkfehler) ### Polling statt WebSocket Das Widget verwendet Polling alle 30 Sekunden statt einer persistenten WebSocket-Verbindung (`/sync`). Begründung: WordPress-Seiten haben oft viele gleichzeitige Besucher:innen. Ein dauerhafter Sync-Kanal pro Seitenaufruf würde den Homeserver stark belasten. Polling ist hier ausreichend und deutlich ressourcenschonender. --- ## Anpassungsmöglichkeiten **Höhe des Chat-Bereichs** – CSS: `#matrix-messages { height: 420px; }` **Anzahl der Nachrichten** – `const LIMIT = 40;` **Polling-Frequenz** – `const POLL_SEC = 30;` (nicht unter 10 setzen) **Farbe des Headers** – `#matrix-chat-header { background: #0dbd8b; }` (Matrix-Grün) **„Mitschreiben"-Link** – URL im `` im HTML-Teil anpassen --- ## Token-Rotation Da der Token im Klartext im Code steht, sollte er bei Bedarf rotiert werden: ```bash # Alten Token invalidieren curl -X POST https://matrix.rpi-virtuell.de/_matrix/client/v3/logout \ -H "Authorization: Bearer ALTERTOKEN" # Neuen Token holen curl -X POST https://matrix.rpi-virtuell.de/_matrix/client/v3/login \ -H "Content-Type: application/json" \ -d '{"type":"m.login.password","user":"openrelibot","password":"PASSWORT"}' ``` Neuen `access_token` im Code als `TOKEN` einsetzen und WordPress-Seite aktualisieren.