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
67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
// 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;
|
|
}
|