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

+110
View File
@@ -0,0 +1,110 @@
// Rollen, die man sich selbst per Knopf gibt.
//
// Discords eigenes Onboarding kann das auch, verlangt dafuer aber sieben
// Vorgabekanaele, von denen fuenf beschreibbar sind. Der Server hat fuenf und
// drei - nachgemessen, nicht geraten (Fehlercode 350000). Zwei Kanaele nur
// anzulegen, damit eine Schwelle passt, macht den Server schlechter. Also ein
// Knopf unter dem Willkommenstext.
import { logger } from '#core/logger.ts';
import type { ApiRole, RestFull } from './api.ts';
import type { Interaction } from './gateway.ts';
import { SELF_ROLES, type SelfRolePlan } from './plan.ts';
const log = logger('discord:rollen');
/** Interaktionsart: Klick auf eine Komponente. */
const MESSAGE_COMPONENT = 3;
/** Antwortart: sichtbare Antwort, hier zusaetzlich nur fuer den Klickenden. */
const CHANNEL_MESSAGE = 4;
const EPHEMERAL = 64;
/**
* Die Knopfleiste unter dem Willkommenstext.
* Style 2 ist der graue Knopf - er soll einladen, nicht schreien.
*/
export function selfRoleComponents(): Array<Record<string, unknown>> {
if (SELF_ROLES.length === 0) return [];
return [{
type: 1, // Reihe
components: SELF_ROLES.map((r) => ({
type: 2, // Knopf
style: 2,
label: r.label,
custom_id: r.customId,
emoji: { name: r.emoji },
})),
}];
}
export type RoleButtons = {
handle: (interaction: Interaction) => Promise<void>;
};
export function createRoleButtons(api: RestFull, guildId: string): RoleButtons {
// Rollennamen einmal aufloesen und merken. Kommt ein Name nicht vor, wird
// einmal neu geladen - eine Rolle kann nach dem Start angelegt worden sein.
let cache: Map<string, string> | null = null;
async function roleId(name: string, force = false): Promise<string | undefined> {
if (!cache || force) {
const roles = await api.get<ApiRole[]>(`/guilds/${guildId}/roles`);
cache = new Map(roles.map((r) => [r.name.toLowerCase(), r.id]));
}
const hit = cache.get(name.toLowerCase());
if (hit || force) return hit;
return roleId(name, true);
}
async function reply(interaction: Interaction, content: string): Promise<void> {
await api.post(`/interactions/${interaction.id}/${interaction.token}/callback`, {
type: CHANNEL_MESSAGE,
data: { content, flags: EPHEMERAL },
});
}
return {
async handle(interaction) {
if (interaction.type !== MESSAGE_COMPONENT) return;
const customId = interaction.data?.custom_id;
const plan: SelfRolePlan | undefined = SELF_ROLES.find((r) => r.customId === customId);
if (!plan) return;
const member = interaction.member;
if (!member) {
// Ausserhalb eines Servers gibt es keine Rollen zu vergeben.
await reply(interaction, 'Das geht nur im Server selbst.');
return;
}
try {
const id = await roleId(plan.role);
if (!id) {
await reply(interaction, `Die Rolle **${plan.role}** gibt es gerade nicht. Sag einem Mod Bescheid.`);
log.error(`Rolle "${plan.role}" nicht gefunden - Bauplan und Server laufen auseinander`);
return;
}
const user = member.user.id;
const path = `/guilds/${guildId}/members/${user}/roles/${id}`;
const hatSie = member.roles.includes(id);
if (hatSie) {
await api.del(path, 'Rolle per Knopf abgegeben');
await reply(interaction, plan.offText);
log.info(`${member.user.username} hat ${plan.role} abgegeben`);
} else {
await api.put(path, undefined, 'Rolle per Knopf geholt');
await reply(interaction, plan.onText);
log.info(`${member.user.username} hat ${plan.role} geholt`);
}
} catch (err) {
log.error(`Rollenwechsel fehlgeschlagen: ${(err as Error).message}`);
// Auf einen Klick gehoert eine Antwort, auch wenn es schiefging -
// sonst steht bei der Person nur "Interaktion fehlgeschlagen".
await reply(interaction, 'Hat nicht geklappt. Versuch es gleich noch einmal.')
.catch(() => undefined);
}
},
};
}