From e246dadbec88588e1c23d1a36c85dff86b479cd3 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Fri, 14 Aug 2026 12:00:14 +0200 Subject: [PATCH] Zwei Befunde aus dem Betriebslog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. /api/auditlog antwortete mit 500. `displayAvatarURL({ size: 48 })` — 48 ist keine Zweierpotenz, Discord nimmt nur 16 bis 4096. Ueberall sonst im Code stehen 64 oder 128, nur hier nicht. Der Wurf lief durch das map, also riss ein einziges Avatar die ganze Liste mit. Jetzt 64, und die Bildsuche faengt ihre Fehler selbst ab. Dazu eine Pruefung, die verbotene Groessen im Quelltext findet, bevor sie jemand im Log findet. 2. "Sprachverbindung kam nicht zustande" sagt jetzt, WO sie stehen blieb. Das entscheidet naemlich die Ursache: bleibt sie bei `signalling`, hat Discord nie einen Sprachserver genannt (Rechte, Gateway). Bleibt sie bei `connecting`, ist der Sprachserver bekannt und der UDP-Austausch mit ihm scheitert — praktisch immer gesperrtes ausgehendes UDP. Beides steht jetzt im Log und in der Antwort auf /radio. Dazu der dokumentierte Umgang mit Close-Code 4014: rausgeworfen oder verschoben heisst nicht wiederverbinden, sondern aufhoeren. Co-Authored-By: Claude Opus 5 --- src/bot/radio.js | 27 +++++++++++++++++++++++++-- src/web/api.js | 13 +++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/bot/radio.js b/src/bot/radio.js index e1a2d99..2bad541 100644 --- a/src/bot/radio.js +++ b/src/bot/radio.js @@ -172,7 +172,15 @@ export async function spielen(kanal, station) { }); // Discord verschiebt Sprachserver im Betrieb; ohne dieses Zurückholen // bleibt die Verbindung dann still stehen, statt sich zu erholen. - verbindung.on(VoiceConnectionStatus.Disconnected, async () => { + // + // 4014 heisst „rausgeworfen oder verschoben" — da hilft kein + // Wiederverbinden, das wäre nur ein Ringen mit dem Moderator. + verbindung.on(VoiceConnectionStatus.Disconnected, async (_, stand) => { + if (stand.closeCode === 4014) { + stoppen(guildId); + clearRadioState(guildId); + return; + } try { await Promise.race([ entersState(verbindung, VoiceConnectionStatus.Signalling, 5000), @@ -194,9 +202,24 @@ export async function spielen(kanal, station) { try { await entersState(verbindung, VoiceConnectionStatus.Ready, 20_000); } catch { + // Wo genau sie hängen blieb, entscheidet die Ursache — deshalb steht + // der Zustand in der Meldung und nicht nur „ging nicht": + // + // signalling → Discord hat den Sprachserver nie genannt. Das ist ein + // Rechte- oder Gateway-Problem, kein Netzproblem. + // connecting → der Sprachserver ist bekannt, aber der UDP-Austausch + // mit ihm scheitert. Praktisch immer: ausgehendes UDP + // ist dicht (Container, Firewall, Hoster). + const steckt = verbindung.state.status; verbindung.destroy(); laufend.delete(guildId); - throw new Error('Die Sprachverbindung kam nicht zustande.'); + const grund = steckt === VoiceConnectionStatus.Connecting + ? 'Der Sprachserver antwortet nicht auf UDP — ausgehendes UDP ist vermutlich gesperrt.' + : steckt === VoiceConnectionStatus.Signalling + ? 'Discord nennt keinen Sprachserver — fehlen dem Bot Rechte im Kanal?' + : `Die Verbindung blieb bei „${steckt}" stehen.`; + console.error(`[radio] Verbindung steckt bei "${steckt}": ${grund}`); + throw new Error(`Sprachverbindung kam nicht zustande. ${grund}`); } const player = vorher?.player ?? createAudioPlayer({ diff --git a/src/web/api.js b/src/web/api.js index 6cd84ea..e54a911 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -2471,11 +2471,16 @@ ${rssItems} // Audit-Log: die letzten Team-/Owner-Aktionen im Webinterface (nur Owner) app.get('/api/auditlog', async (request, reply) => { if (requireAdmin(request, reply)) return; + // 64 und nicht 48: Discord nimmt nur Zweierpotenzen von 16 bis 4096. + // Mit 48 warf die Bibliothek, und weil der Wurf durch das map lief, + // riss ein einziges Avatar das ganze Protokoll mit — 500 statt Liste. const bild = (id) => { - for (const guild of client.guilds.cache.values()) { - const m = guild.members?.cache?.get(id); - if (m) return m.displayAvatarURL?.({ size: 48 }) ?? null; - } + try { + for (const guild of client.guilds.cache.values()) { + const m = guild.members?.cache?.get(id); + if (m) return m.displayAvatarURL?.({ size: 64 }) ?? null; + } + } catch { /* ein fehlendes Bild ist kein Grund, die Liste zu verlieren */ } return null; }; return { entries: listAudit(100).map((e) => ({ ...e, avatar: bild(e.user_id) })) };