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:
1 parent
4ceb61f095
commit
d07c18441b
82 files changed
+2775
-838
No files matched your search
@@ -0,0 +1,66 @@
|
||||
// Einheitliches Ereignisformat. Alles, was aus IRC, EventSub, Helix oder OBS
|
||||
// hereinkommt, wird darauf normalisiert und in `events` geschrieben.
|
||||
// Siehe docs/03-data-model.md.
|
||||
|
||||
export type EventType =
|
||||
| 'stream.online' | 'stream.offline' | 'stream.segment_changed'
|
||||
| 'chat.message' | 'chat.first_message' | 'chat.spike'
|
||||
| 'user.arrived' | 'user.returning'
|
||||
| 'follow' | 'subscribe' | 'subgift' | 'cheer'
|
||||
| 'raid.in' | 'raid.out'
|
||||
| 'points.redeemed' | 'shop.redeemed'
|
||||
| 'poll.begin' | 'poll.end'
|
||||
| 'obs.scene_changed' | 'obs.replay_saved'
|
||||
| 'clip.requested' | 'clip.created' | 'clip.rendered';
|
||||
|
||||
export type StreamEvent = {
|
||||
id: string;
|
||||
ts: string; // ISO-8601, UTC
|
||||
type: EventType;
|
||||
streamId: string | null;
|
||||
userId: string | null;
|
||||
payload: unknown;
|
||||
};
|
||||
|
||||
export type ChatMessage = {
|
||||
id: string; // msg-id aus IRC
|
||||
ts: string;
|
||||
userId: string;
|
||||
login: string;
|
||||
displayName: string;
|
||||
text: string;
|
||||
emotes: Array<{ id: string; name: string; count: number }>;
|
||||
isFirst: boolean; // first-msg Tag
|
||||
isReturning: boolean; // returning-chatter Tag
|
||||
isCommand: boolean;
|
||||
isMod: boolean;
|
||||
isSub: boolean;
|
||||
isBroadcaster: boolean;
|
||||
bits: number;
|
||||
color: string | null;
|
||||
};
|
||||
|
||||
export type SpikeEvent = {
|
||||
score: number;
|
||||
rate: number;
|
||||
baseline: number;
|
||||
chatters: number;
|
||||
emoteHits: number;
|
||||
clipMentions: number;
|
||||
windowFrom: string;
|
||||
windowTo: string;
|
||||
overThreshold: boolean;
|
||||
};
|
||||
|
||||
/** ULID-artige, zeitlich sortierbare ID ohne Abhaengigkeit. */
|
||||
const B32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
||||
export function ulid(now = Date.now()): string {
|
||||
let time = '';
|
||||
let t = now;
|
||||
for (let i = 0; i < 10; i++) { time = B32[t % 32]! + time; t = Math.floor(t / 32); }
|
||||
const rnd = new Uint8Array(16);
|
||||
crypto.getRandomValues(rnd);
|
||||
let tail = '';
|
||||
for (const b of rnd) tail += B32[b % 32]!;
|
||||
return time + tail;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// @abdulive/shared — siehe docs/01-architecture.md
|
||||
export {};
|
||||
Reference in new issue
Block a user