# 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
```
---
## 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.