Go-Live-Ping: Discord meldet den Streamstart

Der billigste Hebel im Repo, und bis jetzt nur Vorarbeit: die Webhooks lagen
seit der Discord-Einrichtung bereit, gepostet hat nie jemand.

Ablauf: neuer Stream erkannt, eine Minute warten (Twitch setzt Titel und
Kategorie erst kurz nach dem Start, wer sofort postet kuendigt den Titel des
letzten Streams an), Daten frisch holen, Webhook posten, Rolle Live-Ping
anpingen.

Der Ping funktioniert nur mit `allowed_mentions` im Nachrichtenkoerper. `<@&ID>`
allein faerbt die Erwaehnung lila ein und benachrichtigt niemanden - der
Fehler, den man erst nach drei leeren Streams bemerkt. Dagegen steht ein
eigener Test.

Gegen den doppelten Post stehen drei Dinge, denn wer zweimal in zehn Minuten
angepingt wird, nimmt sich die Rolle wieder ab und ist damit fuer immer stumm:

- Tabelle `posts` mit eindeutigem Index je Stream und Plattform. Nur
  *gesendete* Posts sperren; ein fehlgeschlagener Versuch darf wiederholt
  werden, sonst kostet ein Netzwerkaussetzer die ganze Ankuendigung.
- Abklingzeit von sechs Stunden, gegen die neue Stream-ID nach einem
  Verbindungsabbruch.
- Ausgeloest wird nur beim erkannten Neustart, nie im Fortsetzungszweig.

`stream_id` ist bewusst kein Fremdschluessel: ein Post ist die Quittung fuer
etwas, das den Rechner verlassen hat, und muss auch dann schreibbar sein,
wenn es die Sitzung nicht gibt - beim Probelauf etwa.

Dazu ein vierter Webhook in #technik-log. Der Entwurfsmodus postet dieselbe
Nachricht dorthin, ohne Ping und nur fuer Mods sichtbar. So sieht man das
Ergebnis, bevor es der Server sieht.

Die Rollenaufloesung aus selfroles.ts liegt jetzt in roles.ts, weil der Ping
sie auch braucht: ein Webhook kann eine Rolle nicht ueber ihren Namen
anpingen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZeasyAlexandClaude Opus 5 committed 2026-09-18 00:12:35 +02:00
1 parent e87879de9e
commit c4dc6ee04f
12 files changed
+1098 -18

No files matched your search

+13 -1
View File
@@ -150,8 +150,20 @@ test('alle internen Kanaele liegen wirklich in der internen Kategorie', () => {
});
test('jeder Webhook-Platz kommt genau einmal vor', () => {
// Zweimal derselbe Platz hiesse: der zweite Kanal ueberschreibt die URL des
// ersten im Zugangsdaten-Speicher, und einer der beiden postet ins Leere.
const slots = CHANNELS.flatMap((c) => (c.webhooks ?? []).map((w) => w.slot));
assert.deepEqual([...slots].sort(), ['clipsWebhook', 'goLiveWebhook', 'recapWebhook']);
assert.deepEqual([...slots].sort(),
['clipsWebhook', 'goLiveWebhook', 'recapWebhook', 'technikWebhook']);
assert.equal(new Set(slots).size, slots.length);
});
test('der Entwurfsmodus postet in einen Kanal, den Mitglieder nicht sehen', () => {
// `broadcaster.mode: draft` schickt dieselbe Nachricht wie im Betrieb,
// nur ohne Ping. Laege der Kanal oeffentlich, waere jeder Probelauf eine
// falsche Ankuendigung im Server.
const kanal = CHANNELS.find((c) => (c.webhooks ?? []).some((w) => w.slot === 'technikWebhook'));
assert.equal(kanal?.category, 'intern');
});
test('Webhooks haengen nur an Kanaelen, in denen Mitglieder nicht dazwischenfunken', () => {