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
This commit is contained in:
ZeasyAlexandClaude Opus 5 committed 2026-09-09 09:56:55 +02:00
1 parent 4ceb61f095
commit d07c18441b
82 files changed
+2775 -838

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseLine, toChatMessage } from './irc.ts';
const LINE =
'@badge-info=subscriber/8;badges=broadcaster/1,subscriber/6;color=#1E90FF;' +
'display-name=Nic_OOo;emotes=25:0-4/1902:6-10;first-msg=0;id=abc-123;mod=0;' +
'returning-chatter=0;subscriber=1;tmi-sent-ts=1757404800000;user-id=98765 ' +
':nic_ooo!nic_ooo@nic_ooo.tmi.twitch.tv PRIVMSG #abdulive_ :Kappa Keepo mehr Rost als Farbe';
test('zerlegt eine PRIVMSG mit Tags', () => {
const m = parseLine(LINE);
assert.ok(m);
assert.equal(m.command, 'PRIVMSG');
assert.equal(m.params[0], '#abdulive_');
assert.equal(m.params[1], 'Kappa Keepo mehr Rost als Farbe');
assert.equal(m.tags['user-id'], '98765');
assert.equal(m.tags['display-name'], 'Nic_OOo');
});
test('entschluesselt escapte Tag-Werte', () => {
const m = parseLine('@system-msg=Ein\\sTest\\szur\\sSache;id=x :tmi.twitch.tv USERNOTICE #k');
assert.equal(m?.tags['system-msg'], 'Ein Test zur Sache');
});
test('uebersetzt in eine ChatMessage', () => {
const msg = toChatMessage(parseLine(LINE)!);
assert.ok(msg);
assert.equal(msg.userId, '98765');
assert.equal(msg.login, 'nic_ooo');
assert.equal(msg.displayName, 'Nic_OOo');
assert.equal(msg.isBroadcaster, true);
assert.equal(msg.isSub, true);
assert.equal(msg.isFirst, false);
assert.equal(msg.isCommand, false);
assert.equal(msg.ts, new Date(1757404800000).toISOString());
// Emote-Namen werden aus den Positionsangaben zurueckgelesen
assert.deepEqual(msg.emotes.map((e) => e.name).sort(), ['Kappa', 'Keepo']);
});
test('erkennt Befehle und Erstchatter', () => {
const line =
'@first-msg=1;id=y;tmi-sent-ts=1757404800000;user-id=42;display-name=Neu ' +
':neu!neu@neu.tmi.twitch.tv PRIVMSG #abdulive_ :!clip';
const msg = toChatMessage(parseLine(line)!)!;
assert.equal(msg.isFirst, true);
assert.equal(msg.isCommand, true);
});
test('ignoriert Nachrichten ohne user-id', () => {
const line = ':tmi.twitch.tv PRIVMSG #abdulive_ :ohne Tags';
assert.equal(toChatMessage(parseLine(line)!), null);
});
test('PING wird als solches erkannt', () => {
const m = parseLine('PING :tmi.twitch.tv');
assert.equal(m?.command, 'PING');
assert.equal(m?.params[0], 'tmi.twitch.tv');
});