Files
abdulive/packages/shared/src/events.ts
T
ZeasyAlexandClaude Opus 5 bc2c8f6cbe Overlay: fuenf Browser-Sources fuer OBS
Einblendungen, Chat, Statusleiste, Effekte, Pausenbild. Kein Build, kein
Framework - OBS laedt die Dateien direkt, und jede Zwischenstufe waere ein
Ding, das mitten im Stream kaputtgehen kann.

Server-Sent Events statt WebSocket, entgegen der Skizze in chat-control.md:
Node bringt einen WebSocket-*Client* mit, aber keinen Server, `EventSource`
verbindet sich von allein neu, und die Richtung stimmt - das Overlay hoert
nur zu. Gegen die tote Browser-Source nach einem Neustart stehen drei Dinge:
der eingebaute Reconnect, ein Herzschlag als echtes Ereignis (ein
SSE-Kommentar haelt die Verbindung offen, ist im Browser aber unsichtbar),
und ein Zustands-Schnappschuss beim Verbinden.

Abos, Raids, Bits und Loeschungen kommen ueber IRC-USERNOTICE und CLEARCHAT
herein; das wurde bisher weggeworfen. Follows fehlen weiter, die gibt es nur
ueber EventSub.

GIFs im Chat: Twitch schickt dafuer einen Tag `gifs`, den wir nicht kannten.

  gifs=0-22|Pnd7qLFuGCOntZcNKB|https://media0.giphy.com/media/.../giphy.gif
  PRIVMSG #abdulive_ :[Idk GIF by Respective]

Der Nachrichtentext ist nur ein Platzhalter, der Bereich markiert ihn.
Ausgewertet wird er jetzt; ohne das stand woertlich "[Idk GIF by Respective]"
im Bild. Gefunden wurde der Tag durch einen Rohmitschnitt - in 3094
Nachrichten aus acht grossen Kanaelen war kein einziges GIF, weil die Adresse
eben nicht im Text steht.

Zwei Fehler, die beim Durchklicken im Browser aufgefallen sind:

- Die Aufraeumfunktion im Chat zaehlte Zeilen, die erst nach der
  Ausblend-Animation aus dem DOM verschwinden, und lief in eine
  Endlosschleife. In OBS waere die Quelle beim vierzehnten Chatbeitrag
  eingefroren.
- Der Bereichsparser der GIFs war zu nachsichtig: `-1-4` zerfaellt beim
  Trennen in ['', '1', '4'], und `Number('')` ist 0 - der Bereich saehe
  gueltig aus und wuerde den falschen Textteil ersetzen.

Auf der Uebersichtsseite steht fuer jede Einblendung, jede Chat-Variante und
jeden Effekt ein Knopf. Ohne den liesse sich eine Raid-Einblendung nur
pruefen, indem man auf einen echten Raid wartet. Was einen Zustand setzt, ist
waehrend eines Streams gesperrt; "Stand zuruecksetzen" ausdruecklich nicht,
sonst kaeme man aus einem erfundenen Zustand nicht mehr heraus.

build-package.ts nimmt jetzt auch html/css/js mit - sonst liefert das Paket
404 statt Overlay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 00:13:28 +02:00

72 lines
2.1 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 }>;
/**
* GIFs aus Twitchs Auswahlfenster. `from`/`to` zeigen auf die Zeichen im
* Text, an denen der Platzhalter steht - siehe `parseGifs` in #twitch/irc.ts.
*/
gifs: Array<{ id: string; url: string; from: number; to: 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;
}