diff --git a/apps/hub/README.md b/apps/hub/README.md new file mode 100644 index 0000000..40f18cc --- /dev/null +++ b/apps/hub/README.md @@ -0,0 +1,20 @@ +# @abdulive/hub + +Der zentrale Dienst. Alles, was nur Internet braucht: Twitch-Chat, +Zuschauerzahlen, der Discord-Rollen-Knopf, die Go-Live-Ankuendigung und die +Overlay-Seiten fuer OBS. + +Laeuft im LXC, nicht auf Abdus Rechner - damit der Ping auch dann rausgeht, +wenn der Stream-PC gerade nicht laeuft. Der Schnitt zwischen Hub und Ingest +steht in [ADR-0008](../../docs/decisions/ADR-0008-geteilter-betrieb.md). + +``` +abdulive hub +``` + +Bedienung ueber die Weboberflaeche des Overlay-Servers: Uebersicht auf `/`, +Einrichtung auf `/setup`. Beides hinter einem Passwort +(`abdulive web:passwort`). + +Nicht hier drin und auch nicht hierher zu holen: OBS, der Wiedergabepuffer, +ffmpeg und whisper. Die bleiben in `abdulive ingest`. diff --git a/config/config.example.yaml b/config/config.example.yaml index f7a763f..8b6b824 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -70,16 +70,30 @@ clipper: # Der Hotkey wird in OBS auf "Wiedergabepuffer speichern" gelegt. Es gibt # bewusst keinen eigenen Hotkey-Dienst - siehe ADR-0004, Punkt 4. +# Go-Live-Ankuendigung auf Discord. +# +# mode: live -> Post in #ankuendigungen, mit Ping an die Rolle Live-Ping +# draft -> derselbe Post in #technik-log, ohne Ping. Zum Zusehen. +# +# Zum Ausprobieren, ohne auf einen Stream zu warten: +# abdulive golive:test (interner Kanal, ohne Ping) +# abdulive golive:test --echt (echter Kanal, mit Ping) broadcaster: enabled: true - mode: draft # draft | live + mode: live + # Twitch setzt Titel und Kategorie erst kurz nach dem Start. Wer sofort + # postet, kuendigt regelmaessig den Titel des letzten Streams an. delaySecondsAfterOnline: 60 + # Innerhalb dieser Zeit wird kein zweites Mal angekuendigt. Faengt den Fall + # ab, dass Twitch nach einem Aussetzer eine neue Stream-ID vergibt. cooldownHours: 6 + pingRole: Live-Ping # leer = Post ohne Ping + obsScreenshot: true # Vorschaubild aus OBS statt von Twitch platforms: discord: true - bluesky: true + bluesky: false # noch nicht gebaut mastodon: false - instagramStoryImage: true + instagramStoryImage: false community: greetings: @@ -109,8 +123,52 @@ recap: loyalty: enabled: false # spaetere Ausbaustufe +# Kanalpunkt-Belohnungen und was sie ausloesen. +# +# Zugeordnet wird ueber den Titel der Belohnung auf Twitch (Gross-/Klein- +# schreibung egal) oder ueber die stabile ID; liegt beides vor, gewinnt die ID. +# Belohnungen, die hier nicht stehen, werden protokolliert und sonst in Ruhe +# gelassen - nicht jede Belohnung geht das Toolkit etwas an. +# +# Aktionen: konfetti | emote-regen | einblendung | tts | nichts +rewards: + enabled: false # aus, bis hier bewusst Belohnungen stehen + belohnungen: [] + # Beispiel: + # belohnungen: + # - titel: Konfetti + # aktion: konfetti + # - titel: Nachricht vorlesen + # aktion: tts + # maxZeichen: 150 + tts: + # Vorgabe ist die Freigabe von Hand, und das aus gutem Grund: Kanalpunkte + # sammeln sich beim Zugucken von allein an und sind damit kaum eine + # Huerde. Auf `false` gestellt hat man eine Vorlesefunktion fuer Fremde + # im eigenen Stream. Siehe docs/projects/chat-control.md. + freigabe: true + maxZeichen: 200 + # Zu langer Text wird abgelehnt, nicht gekuerzt - ein halb vorgelesener + # Satz ist schlimmer als gar keiner. + blockliste: [] + proPersonProStunde: 3 + +# Overlay-Server. Zugleich die einzige Tuer nach aussen, wenn der Dienst +# zentral laeuft: Uebersicht, Einrichtung und OAuth-Rueckruf haengen daran. overlay: port: 4791 + enabled: true + +# Oeffentliche Adresse, ohne Schraegstrich am Ende. +# +# Leer = der Dienst laeuft lokal und bindet an 127.0.0.1. Ist etwas gesetzt, +# bindet er an 0.0.0.0 (der Reverse-Proxy sitzt in einem anderen Container) +# und der OAuth-Rueckruf von Twitch geht an /callback - genau so +# muss er auch in der Twitch-App hinterlegt sein. +# +# Die Umgebungsvariable PUBLIC_URL gewinnt gegen diesen Wert. +publicUrl: "" + dashboard: port: 4792 ingestServer: diff --git a/docs/handoff.md b/docs/handoff.md index c5aeafd..9cc41ff 100644 --- a/docs/handoff.md +++ b/docs/handoff.md @@ -1,6 +1,6 @@ # Übergabe -Stand **2026-09-15**, letztes gebautes Paket 0.5.4. Wer hier weitermacht, +Stand **2026-09-17**, letztes gebautes Paket 0.5.4. Wer hier weitermacht, liest zuerst diese Seite, dann [`07-mvp-plan.md`](07-mvp-plan.md). Für Abdu selbst gibt es das [Handbuch](handbuch/handbuch.pdf). @@ -20,9 +20,85 @@ Auslöser bis zum fertigen Short steht im Code vollständig. Geprüft ist sie | S5 Onboarding + Sichtung | offen | | | S6 Erstchatter-Begrüßung | offen | | -162 Tests, alle grün. Was noch fehlt, steht vollständig in +193 Tests, alle grün. Was noch fehlt, steht vollständig in [`offene-punkte.md`](offene-punkte.md). +## Neu: der Dienst läuft zentral + +Seit 2026-09-17 läuft ein Teil des Toolkits nicht mehr auf Abdus Rechner, +sondern in einem LXC auf dem Proxmox-Host. Der Schnitt und seine Begründung +stehen in [ADR-0008](decisions/ADR-0008-geteilter-betrieb.md); die Kurzfassung +ist: alles, was nur Internet braucht, gehört dorthin, wo durchgehend Strom +ist. + +| | Hub (LXC 141, `192.168.2.15`) | Ingest (Abdus PC) | +|---|---|---| +| Befehl | `abdulive hub` | `abdulive ingest` | +| Chat, Zuschauerzahlen | ja | ja | +| Discord-Knopf, Go-Live-Ping | **ja** | nein | +| Overlay für OBS | **ja** | nein | +| OBS, `!clip`, Rendern | nein | **ja** | + +Erreichbar unter `https://abdu.dimension47.de`, hinter dem vorhandenen Nginx +Proxy Manager (Proxy-Eintrag 42, eigenes Let's-Encrypt-Zertifikat, Pufferung +abgeschaltet - ohne das sammelt nginx die Overlay-Ereignisse und liefert sie +gebündelt aus). Ausgerollt wird mit `scripts/deploy-hub.sh`, gestartet von +systemd (`abdulive-hub`). Der Container hat keinen Zugang zum Repo - das +Skript schiebt den Arbeitsstand als Archiv hinüber. + +Der Discord-Teil der Zugangsdaten ist übertragen, der Bot hängt am Hub und +meldet beim Start "Rollen-Knöpfe sind scharf". Die Twitch-Broadcaster-App ist +hinterlegt und geprüft (Client-ID registriert, Secret passt - Twitch stellt +darauf ein App-Token aus). **Was fehlt, ist der Klick von Abdu**, siehe unten. + +**Das kostet ADR-0005 seine Hauptbegründung.** "Bindet nur auf 127.0.0.1" gilt +für den Hub nicht mehr. An seine Stelle treten ein Passwort für alles, was ein +Mensch bedient, und ein Schlüssel in der Adresse für die Browser-Sources. + +## Der Go-Live-Ping geht raus + +War in der letzten Übergabe noch Vorarbeit, ist jetzt gebaut +(`apps/broadcaster`). Ablauf: neuer Stream erkannt, eine Minute warten, Titel +und Kategorie frisch holen, Webhook posten, Rolle `Live-Ping` anpingen. + +Der Ping funktioniert nur mit `allowed_mentions` im Nachrichtenkörper. `<@&ID>` +allein färbt die Erwähnung ein und benachrichtigt niemanden - das ist der +Fehler, den man erst nach drei leeren Streams bemerkt, und dagegen steht ein +eigener Test. + +Gegen den doppelten Ping stehen drei Dinge, denn wer zweimal in zehn Minuten +angepingt wird, nimmt sich die Rolle wieder ab: die Tabelle `posts` mit einem +eindeutigen Index je Stream, eine Abklingzeit von sechs Stunden, und der +Umstand, dass der Fortsetzungszweig im Polling gar nicht erst auslöst. + +Der Hub steht auf `mode: draft` - der Post geht vollständig, aber in +`#technik-log` und ohne Ping. Ein Probelauf ist dort gelandet, der Weg zum +Webhook ist damit nachgewiesen. Zum Scharfschalten: `mode: live` in +`/var/lib/abdulive/config/config.yaml`. + +**Ausgelöst hat er noch nie**, und das liegt nicht am Broadcaster, sondern an +Twitch: die Erkennung des Stream-Starts hängt am Helix-Polling, und das +braucht ein Broadcaster-Token. Solange das fehlt, wartet der Ping auf ein +Ereignis, das nie kommt. + +## Das Overlay steht + +Fünf Browser-Sources unter `apps/overlay-web/public`, ausgeliefert vom +Overlay-Server: Einblendungen, Chat, Statusleiste, Effekte, Pausenbild. Kein +Build, kein Framework - OBS lädt die Dateien direkt. + +Die Ereignisse kommen über Server-Sent Events statt WebSocket. Warum, und was +gegen die tote Browser-Source nach einem Neustart getan ist, steht in +[`apps/overlay-server/README.md`](../apps/overlay-server/README.md). + +Abos, Raids und Bits kommen über IRC-USERNOTICE herein, das wird jetzt +ausgewertet. **Follows fehlen** - die gibt es nur über EventSub. + +Zum Ansehen ohne Stream: auf der Übersichtsseite gibt es unter +**Ausprobieren** für jede Einblendung, jede Chat-Variante und jeden Effekt +einen Knopf. Die Ereignisse gehen nur an die Browser-Sources. Dasselbe im +Dauerbetrieb: `abdulive overlay --demo` oder der Knopf „Vorführung starten". + ## Der Discord-Server steht Dazugekommen seit der letzten Übergabe, und kein Teil des MVP: der @@ -44,10 +120,14 @@ die Kurzfassung ist, dass bei 5 bis 30 Zuschauern fünf sichtbare Kanäle besser sind als zwanzig leere. Ein Teil davon läuft dauerhaft: der Knopf unter dem Willkommenstext, mit dem -man sich die Rolle `Live-Ping` selbst gibt. Er hängt als Gateway-Verbindung im -Ingest-Daemon mit drin und meldet sich als `discordConnected` auf der -Statusseite. **Läuft der Daemon nicht, tut der Knopf nichts** - der Klick -läuft dann in Discords Zeitüberschreitung. +man sich die Rolle `Live-Ping` selbst gibt. Er hängt als Gateway-Verbindung mit +im Prozess und meldet sich als `discordConnected` auf der Statusseite. Seit dem +Umzug in den Hub läuft er durch, auch wenn Abdus Rechner aus ist. + +Dazugekommen ist ein vierter Webhook: `technikWebhook` in `#technik-log`. Dort +landet der Go-Live-Post im Entwurfsmodus - dieselbe Nachricht wie im Betrieb, +nur ohne Ping und nur für Mods sichtbar. Wer den Server vor dieser Fassung +eingerichtet hat, muss `abdulive discord:setup` einmal erneut laufen lassen. ## Was jemand wissen muss, bevor er etwas anfasst @@ -58,9 +138,10 @@ keine Parameter-Properties im Konstruktor, keine `namespace`. `erasableSyntaxOnly` in der `tsconfig.json` erzwingt das und hat schon einen echten Fehler gefangen. -**Alles läuft in einem Prozess.** Schwere Arbeit (ffmpeg, whisper) läuft als -Kindprozess und darf abstürzen. Der Ingest darf nie sterben. Was er nicht -mitschreibt, ist für immer weg. +**Alles läuft in einem Prozess** - aber es gibt jetzt zwei davon, Hub und +Ingest (siehe oben). Schwere Arbeit (ffmpeg, whisper) läuft als Kindprozess und +darf abstürzen. Der Chat-Prozess darf nie sterben. Was er nicht mitschreibt, +ist für immer weg. **Zugangsdaten kommen aus der Weboberfläche**, nicht aus einer Datei. Sie liegen verschlüsselt neben der Datenbank, aber bewusst nicht *in* ihr. Die @@ -97,9 +178,16 @@ Zum Entwickeln reicht ein Checkout und Node 24 oder neuer: ```powershell node apps/cli/main.ts check-env node --test "packages/**/*.test.ts" "apps/**/*.test.ts" +node apps/cli/main.ts overlay --demo # Overlay ohne Stream ansehen node scripts/build-package.ts --version 0.5.5 # baut dist/ ``` +Den Hub ausrollen (braucht SSH auf den Proxmox-Host): + +```bash +scripts/deploy-hub.sh +``` + Typprüfung, das Einzige wofür npm gebraucht wird: ```powershell @@ -214,12 +302,25 @@ oben. ## Die drei Dinge, die Abdu tun muss -1. **Broadcaster-Konto verbinden.** Die beiden Twitch-Apps - (dev.twitch.tv/console/apps, Redirect-URI `http://localhost:4792/callback`) - kann auch jemand anders anlegen, sie hängen nicht am Kanal. Was nur Abdu - kann: auf der Einrichtungsseite eingeloggt als er selbst auf „Mit Twitch - verbinden" klicken. Dafür muss die Anwendung auf seinem Rechner laufen, - die Seite hört nur auf `127.0.0.1`. +1. **Broadcaster-Konto verbinden.** Das ist jetzt der Flaschenhals: ohne + Broadcaster-Token gibt es keine Zuschauerzahlen, keine Stream-Sitzungen und + damit auch keinen Go-Live-Ping. + + Die beiden Twitch-Apps (dev.twitch.tv/console/apps) kann auch jemand anders + anlegen, sie hängen nicht am Kanal. Seit dem Umzug in den Hub brauchen sie + als Redirect-URI **`https://abdu.dimension47.de/callback`**; die alte + `http://localhost:4792/callback` bleibt daneben stehen, wenn auf dem + Stream-PC weiter eingerichtet werden soll. Twitch vergleicht zeichengenau. + + Die App ist inzwischen hinterlegt. Was nur Abdu kann: auf + `https://abdu.dimension47.de/setup` eingeloggt als er selbst auf „Mit + Twitch verbinden" klicken. Der Vorteil gegenüber vorher: das geht von jedem + Gerät, der Stream-PC muss nicht laufen. + + Ob die Redirect-URI in der App richtig eingetragen ist, zeigt sich erst bei + diesem Klick. Twitch prüft sie erst nach dem Login und gibt vorher auch bei + einer falschen URI ein freundliches 302 zurück - von außen ist das nicht + nachweisbar. 2. **OBS einrichten**: obs-websocket an (Port 4455), Wiedergabepuffer 90 s, Hotkey auf „Wiedergabepuffer speichern", getrennte Audiospuren. 3. **Layout umstellen** nach [`obs-layout.md`](obs-layout.md), danach @@ -248,10 +349,14 @@ in der ganzen Kette, und alles Weitere hängt daran. Seit es `abdulive sources` gibt, ist auch die Frage beantwortbar, welche OBS-Quelle fremden Text ins Bild malt. Das war vorher nicht zu erraten. -**Discord braucht jetzt nichts mehr.** Der Server steht, die Webhooks liegen -bereit. Wer als Nächstes `apps/broadcaster` oder `apps/recap` baut, findet -alles vor und muss nur noch posten. Das wäre auch ein guter Nebenweg, wenn der -Stream-PC gerade nicht verfügbar ist, denn anders als S2 hängt daran nichts. +**Den Go-Live-Ping scharf schalten.** Er steht auf `draft`. Einmal +`abdulive golive:test --echt` laufen lassen, prüfen ob das Handy klingelt, dann +`mode: live`. Das ist der kleinste verbleibende Handgriff mit der größten +Wirkung. + +**`apps/recap` bauen.** Der Webhook liegt bereit, der Hub hat die Daten, und +anders als S2 hängt daran nichts. Guter Nebenweg, wenn der Stream-PC gerade +nicht verfügbar ist. **Danach S5.** Nicht wegen des Codes, sondern weil das Werkzeug ohne Sichtungsseite für Abdu nicht bedienbar ist. Die Kette funktioniert, aber sie @@ -277,11 +382,20 @@ konkrete Verbesserungen stehen in [`offene-punkte.md`](offene-punkte.md). Solange nur Abdu installiert, reicht ein Klick. Sobald jemand Drittes es bekommt, nicht mehr. -**Der Discord-Knopf hängt am Daemon.** Läuft der Ingest nicht, passiert beim -Klick nichts, und die klickende Person sieht nur „Interaktion fehlgeschlagen". -Das ist vertretbar, solange der Daemon ohnehin durchläuft, aber es ist eine -stille Abhängigkeit zwischen zwei Dingen, die sonst nichts miteinander zu tun -haben. +**Der Discord-Knopf hängt am Daemon** - das war der Punkt aus der letzten +Übergabe und ist mit dem Hub erledigt. Dafür gibt es jetzt einen neuen: der +Bot darf nur an **einer** Stelle laufen. Steht auf dem Stream-PC ein +Discord-Bot-Token im Speicher und läuft dort `ingest`, antworten zwei Prozesse +auf denselben Klick. Auf dem Stream-PC bleibt das Discord-Token leer. + +**Der Overlay-Schlüssel steht in der Adresse.** Damit steht er in jedem +OBS-Profil und auf jedem Screenshot der Einstellungen. Neu würfeln geht auf +der Übersichtsseite, aber danach müssen alle fünf Adressen in OBS neu +eingetragen werden. + +**Das Overlay hat nie unter Last gelaufen.** Geprüft ist es gegen den +Vorführmodus und im Browser. Wie es sich bei 200 Nachrichten pro Minute +verhält, und was der Emote-Regen beim Encoder kostet, ist offen. **Der Discord-Bot ist nie unter Last gelaufen.** Getestet sind die Bausteine und ein Klickweg gegen einen nachgebauten Server; am echten Server ist die