Discord: Knopf fuer die Live-Ping-Rolle statt Onboarding

Live-Ping nuetzt nur, wenn man sie sich selbst geben kann. Discords
Onboarding waere der vorgesehene Weg, laesst sich hier aber nicht
einschalten - gemessen, nicht vermutet:

  HTTP 400  Cannot enable onboarding, requirements are not met  (350000)
  sichtbar fuer @everyone: 5   davon beschreibbar: 3
  Discord verlangt:        7   davon beschreibbar: 5

Zwei Kanaele anzulegen, damit eine Schwelle passt, macht den Server leerer
statt besser. Stattdessen ein Knopf unter dem Willkommenstext: ein Druck
holt die Rolle, der naechste gibt sie ab, die Antwort sieht nur, wer
geklickt hat.

Der Knopf braucht eine dauerhafte Verbindung, weil Discord Klicks ans
Gateway oder an eine oeffentliche HTTPS-Adresse liefert und der Stream-PC
keine hat:

- gateway.ts   WebSocket mit Herzschlag, Fortsetzen und Wiederverbinden.
               Bei 4004 und Verwandten wird aufgegeben statt im Kreis zu
               laufen - dagegen hilft nur eine Aenderung an der App.
               intents: 0, der Bot sieht weder Nachrichten noch Mitglieder.
- selfroles.ts Knopfleiste und Rollenwechsel.
- bot.ts       beides zusammen, laeuft im Ingest-Daemon mit. Ohne
               hinterlegtes Token entfaellt er wortlos.

Neuer Befehl `abdulive discord:bot` zum Ausprobieren; im Normalbetrieb
haengt der Bot im Daemon und meldet sich als discordConnected auf der
Statusseite.

Die Knopfleiste gehoert zum Soll-Zustand des Willkommenstextes: fehlt sie,
wird sie nachgereicht, ohne die Nachricht neu zu posten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZeasyAlexandClaude Opus 5 committed 2026-09-15 21:59:08 +02:00
1 parent 3b744caf6a
commit 994ce4faf0
12 files changed
+787 -11

No files matched your search

+31 -2
View File
@@ -19,7 +19,7 @@ class FakeDiscord implements Rest {
channels: ApiChannel[] = [];
webhooks: ApiWebhook[] = [];
automod: Array<{ id: string; name: string; actions?: unknown[] }> = [];
messages = new Map<string, Array<{ id: string; content: string; author: { id: string } }>>();
messages = new Map<string, Array<{ id: string; content: string; author: { id: string }; components?: unknown[] }>>();
calls: string[] = [];
private next = 1000;
@@ -97,6 +97,7 @@ class FakeDiscord implements Rest {
id: this.id(),
content: String((b as Record<string, unknown>)['content'] ?? ''),
author: { id: this.botUserId },
components: ((b as Record<string, unknown>)['components'] ?? []) as unknown[],
};
const list = this.messages.get(msg[1]!) ?? [];
list.push(made);
@@ -133,7 +134,11 @@ class FakeDiscord implements Rest {
const msg = /^\/channels\/(\d+)\/messages\/(\d+)$/.exec(path);
if (msg) {
const m = (this.messages.get(msg[1]!) ?? []).find((x) => x.id === msg[2]);
if (m) m.content = String((body as Record<string, unknown>)['content'] ?? '');
if (m) {
const src = body as Record<string, unknown>;
m.content = String(src['content'] ?? '');
m.components = (src['components'] ?? []) as unknown[];
}
return structuredClone(m) as T;
}
const ch = /^\/channels\/(\d+)$/.exec(path);
@@ -448,3 +453,27 @@ test('der Meldekanal einer bestehenden Regel bleibt, wie er gesetzt wurde', asyn
const danach = (api.automod[0]!.actions as typeof actions).find((a) => a.type === 2);
assert.equal(danach?.metadata['channel_id'], '424242', 'die eigene Wahl darf nicht ueberschrieben werden');
});
test('der Willkommenstext bekommt die Knopfleiste', async () => {
const api = new FakeDiscord();
await run(api);
const post = api.messages.get(live(api, 'willkommen')!.id)![0]!;
const reihen = post.components as Array<{ type: number; components: Array<{ custom_id: string }> }>;
assert.equal(reihen.length, 1);
assert.equal(reihen[0]!.components[0]!.custom_id, 'rolle:live-ping');
});
test('eine Nachricht ohne Knopfleiste bekommt sie nachgereicht', async () => {
// So sieht der Willkommenstext aus, der vor den Knoepfen gepostet wurde.
const api = new FakeDiscord();
await run(api);
const kanal = live(api, 'willkommen')!.id;
api.messages.get(kanal)![0]!.components = [];
const res = await run(api);
const step = res.steps.find((s) => s.name === 'Willkommenstext');
assert.equal(step?.change, 'angepasst');
assert.equal((api.messages.get(kanal)![0]!.components as unknown[]).length, 1);
assert.equal(api.messages.get(kanal)!.length, 1, 'nicht neu posten');
});