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) })) };