Files
abdulive/docs/02-twitch-integration.md
T
ZeasyAlexandClaude Opus 5 d07c18441b S1: Einrichtungs-Assistent, Twitch-Auth, Datenbank und Chat-Aufnahme
Zugangsdaten kommen jetzt aus der App statt aus einer .env: ein
Einrichtungs-Assistent auf http://localhost:4792/setup nimmt Client-ID,
Secret und OBS-Passwort entgegen, der OAuth-Rueckruf laeuft auf denselben
Server. Gespeichert wird verschlueselt (AES-256-GCM) in einem Volume,
bewusst nicht in der Datenbank - die wird auf die Windows-Seite gesichert.
Siehe ADR-0005.

Technikbasis vereinfacht: Node 24 bringt node:sqlite, WebSocket, fetch und
die Ausfuehrung von TypeScript mit. Das Projekt hat damit null
Laufzeitabhaengigkeiten und keinen Build-Schritt; pnpm-Workspaces und die
Paketmanifeste sind entfallen, Importe laufen ueber die imports-Map der
Wurzel-package.json. erasableSyntaxOnly verhindert Syntax, die das Type
Stripping stillschweigend entfernen wuerde. Siehe Nachtrag 2 in ADR-0001.

Enthalten:
- packages/core: Config, Layout, Logger mit Secret-Maskierung, Retry, YAML
- packages/db: Migrationen, Repositories, alle SQL-Anweisungen an einem Ort
- packages/twitch: Secret-Speicher, OAuth, Helix mit Token-Bucket, IRC
- apps/ingest: Daemon, Einrichtungsseite, Spike-Erkennung
- 20 Tests (IRC-Parser, Spike-Detektor, Datenbankschicht)

Der Chat verbindet sich anonym, solange nichts hinterlegt ist - damit laesst
sich die Datenaufnahme pruefen, bevor die Twitch-Apps existieren.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
2026-09-09 09:56:55 +02:00

128 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Twitch-Integration
## Zwei Identitäten
Es werden zwei getrennte Twitch-Apps/Tokens gebraucht:
1. **Broadcaster-Token** (Konto `abdulive_`) — für alles, was den eigenen Kanal
betrifft: Clips erstellen, Titel ändern, Raids starten, EventSub abonnieren,
Analytics lesen.
2. **Bot-Token** (eigenes Bot-Konto, z. B. `abdulive_bot`) — für Chat-Nachrichten.
Trennung, damit Bot-Nachrichten als Bot erkennbar sind und der Broadcaster-
Token nicht im Chat-Client hängt.
Client-IDs, Client-Secrets und beide Tokens liegen verschlüsselt in
`/db/secrets.json` im Docker-Volume und werden über Refresh Tokens automatisch
erneuert (`packages/twitch/secrets.ts`). Eingegeben werden sie im
Einrichtungs-Assistenten unter `http://localhost:4792/setup`, **nicht** in einer
`.env` und nie im Repo — siehe [ADR-0005](decisions/ADR-0005-zugangsdaten.md).
## Scopes
| Scope | Wofür | Token |
|-------|-------|-------|
| `chat:read` | IRC lesen | Bot |
| `chat:edit` | IRC schreiben | Bot |
| `user:write:chat` | Chat über Helix senden (Alternative zu IRC) | Bot |
| `moderator:read:chatters` | Zuschauerliste im Chat | Broadcaster |
| `clips:edit` | Clips erstellen | Broadcaster |
| `channel:manage:broadcast` | Titel/Kategorie ändern, Marker setzen | Broadcaster |
| `channel:read:subscriptions` | Sub-Events | Broadcaster |
| `moderator:read:followers` | Follow-Events (EventSub) | Broadcaster |
| `channel:manage:raids` | Raid starten/abbrechen | Broadcaster |
| `channel:read:redemptions` | Channel-Point-Einlösungen | Broadcaster |
| `channel:manage:redemptions` | eigene Punkte-Rewards verwalten | Broadcaster |
| `channel:manage:polls` | Umfragen | Broadcaster |
| `analytics:read:games` | Game-Analytics-Report | Broadcaster |
| `bits:read` | Bits-Events | Broadcaster |
Der Autorisierungs-Flow läuft über den Einrichtungs-Assistenten:
`docker compose up -d`, dann `http://localhost:4792/setup` öffnen und je einmal
für Broadcaster- und Bot-Konto auf „Mit Twitch verbinden" klicken. Die
Redirect-URI, die in beiden Twitch-Apps hinterlegt sein muss, lautet
`http://localhost:4792/callback`.
## EventSub (WebSocket)
Abonniert werden beim Start:
| Subscription | Version | Nutzen für |
|---|---|---|
| `stream.online` | 1 | Broadcaster, Analytics, Recap |
| `stream.offline` | 1 | Recap, VOD-Kapitel |
| `channel.update` | 2 | VOD-Kapitel (Kategorie/Titel), Analytics |
| `channel.follow` | 2 | Community, Recap |
| `channel.subscribe` / `.message` / `.gift` | 1 | Recap, Overlay |
| `channel.cheer` | 1 | Recap, Overlay |
| `channel.raid` (to + from) | 1 | Raid-Tool |
| `channel.channel_points_custom_reward_redemption.add` | 1 | Loyalty, Overlay |
| `channel.poll.begin/progress/end` | 1 | Overlay |
| `channel.chat.message` | 1 | **optional**, siehe unten |
| `channel.chat.notification` | 1 | Community (Erstzuschauer-Marker) |
**Chat: IRC oder EventSub?** Beides funktioniert. Wir lesen den Chat über
**IRC (`irc-ws.chat.twitch.tv:443`)**, weil die Tags dort (`first-msg`,
`returning-chatter`, `emotes`, `badges`, `msg-id`) reichhaltiger und stabiler
sind als im EventSub-Payload, und weil Rate Limits beim Senden dort klar
dokumentiert sind. EventSub übernimmt alles Nicht-Chat.
**Verbindungsregeln:** Eine WS-Session hält max. 300 Subscriptions. Twitch
sendet alle ~10 s ein Keepalive; bleibt es aus, neu verbinden.
`session_reconnect` liefert eine neue URL — dort verbinden, *dann* erst die
alte Verbindung schließen. Nach jedem Reconnect: Lücke per Helix-Polling füllen.
## Helix — die relevanten Endpunkte
| Endpunkt | Tool |
|---|---|
| `GET /helix/streams?user_id=` | Ingest (Viewer-Sampling, alle 60 s) |
| `GET /helix/streams?game_id=&first=100` | Raid-Scout |
| `GET /helix/channels` / `PATCH /helix/channels` | Loyalty (Titel-Shop), VOD-Kapitel |
| `POST /helix/clips` | Clipper |
| `GET /helix/clips?id=` | Clipper (Metadaten + Thumbnail-URL) |
| `GET /helix/videos?user_id=&type=archive` | VOD-Kapitel |
| `POST /helix/streams/markers` | VOD-Kapitel, Clipper |
| `GET /helix/chat/chatters` | Community (Anwesenheit, alle 5 min) |
| `GET /helix/channels/followers` | Recap, Community |
| `POST /helix/raids` | Raid-Tool |
| `GET /helix/users?login=` | überall (Login → ID) |
**Rate Limits:** 800 Punkte/Minute pro Client-ID (Standard: 1 Punkt pro Request).
Der Helix-Client in `packages/twitch` führt einen Token-Bucket und drosselt
selbst; `Ratelimit-Remaining` aus dem Response-Header wird ausgewertet.
Bei `429` → Backoff nach `Ratelimit-Reset`.
## Clips: die Fallstricke
Das ist der Teil, an dem die Clip-Pipeline scheitert, wenn man ihn nicht kennt:
1. `POST /helix/clips` schneidet **rückwirkend ~30 s** ab dem Aufrufzeitpunkt
(minus Stream-Delay). Man kann keinen Zeitbereich wählen.
2. Der Clip ist **nicht sofort verfügbar**. Nach dem POST bekommt man `id` und
`edit_url`; die Verarbeitung dauert typischerweise 5–20 s. Danach liefert
`GET /helix/clips?id=` erst Metadaten.
3. Die MP4-URL steht nirgends in der API. Sie wird aus `thumbnail_url`
abgeleitet: alles ab `-preview-` abschneiden, `.mp4` anhängen.
```
https://clips-media-assets2.twitch.tv/AT-cm%7C123-preview-480x272.jpg
→ https://clips-media-assets2.twitch.tv/AT-cm%7C123.mp4
```
Das ist undokumentiert und kann sich ändern — deshalb im Code an genau einer
Stelle (`packages/twitch/clips.ts`) gekapselt und mit Fallback versehen.
4. Der Clip enthält das **fertig komposite Bild** (1920×1080), nicht die
Einzelquellen. Facecam und Gameplay lassen sich nur per Crop trennen, und nur
wenn das OBS-Layout fest ist.
5. Bitrate/Qualität entspricht dem Transcode, nicht dem Original.
**Deshalb: OBS-Replay-Buffer ist der bessere Primärweg.** Details in
[`docs/projects/clip-shorts.md`](projects/clip-shorts.md).
## OBS-WebSocket
obs-websocket v5 auf `ws://127.0.0.1:4455`. Genutzt für:
- `CurrentProgramSceneChanged` → VOD-Kapitel
- `SaveReplayBuffer` → Clipper
- `TriggerHotkeyByName`, `SetInputSettings` → Chat-Steuerung, Loyalty-Shop
- `GetSourceScreenshot` → Go-Live-Broadcaster (Screenshot für den Post)