diff --git a/src/bot/commands/radio.js b/src/bot/commands/radio.js index 43f741b..56da0c6 100644 --- a/src/bot/commands/radio.js +++ b/src/bot/commands/radio.js @@ -51,6 +51,17 @@ export async function execute(interaction) { }); return; } + // Ein voller Kanal sieht aus wie ein Rechteproblem: Discord nimmt die + // Beitrittsanfrage entgegen und antwortet einfach nicht mehr. Nur wer + // Mitglieder verschieben darf, kommt am Limit vorbei. + if (kanal.userLimit > 0 && kanal.members.size >= kanal.userLimit && !rechte.has('MoveMembers')) { + await interaction.reply({ + content: `❌ <#${kanal.id}> ist voll (${kanal.members.size}/${kanal.userLimit}). ` + + 'Entweder Platz machen oder mir das Recht **Mitglieder verschieben** geben.', + flags: MessageFlags.Ephemeral, + }); + return; + } } // Ohne ffmpeg wuerde der Bot sich hinsetzen und schweigen. Lieber gleich diff --git a/src/bot/radio.js b/src/bot/radio.js index 2bad541..1b6e1dd 100644 --- a/src/bot/radio.js +++ b/src/bot/radio.js @@ -136,6 +136,70 @@ function tonquelle(url) { return ff; } +/** + * Der Adapter zwischen Discord-Verbindung und Sprachbibliothek — mit Protokoll. + * + * Ein Beitritt zum Sprachkanal ist ein Dreischritt, und `signalling` heisst + * nur „einer davon fehlt", ohne zu sagen welcher: + * + * 1. Der Bot bittet Discord über die bestehende Verbindung um den Beitritt. + * 2. Discord antwortet mit dem eigenen Sprachstatus (darin die Sitzungs-ID). + * 3. Discord nennt den Sprachserver (Adresse und Zugangswort). + * + * Fehlt 1, ist die Verbindung zu Discord nicht bereit. Fehlt 2, kommen die + * Sprachstatus-Meldungen nicht an — dann fehlt der Intent oder der eigene + * Mitgliedseintrag. Fehlt 3, hat Discord den Beitritt abgelehnt, praktisch + * immer wegen fehlender Rechte im Kanal. + * + * Ohne diese drei Häkchen bleibt nur Raten, und Raten hat hier schon zweimal + * nicht gereicht. + */ +function beobachteterAdapter(guild, schritte) { + const echt = guild.voiceAdapterCreator; + return (methods) => { + const gepatcht = { + ...methods, + onVoiceStateUpdate: (daten) => { + schritte.eigenerStatus = true; + return methods.onVoiceStateUpdate(daten); + }, + onVoiceServerUpdate: (daten) => { + schritte.sprachserver = true; + return methods.onVoiceServerUpdate(daten); + }, + }; + const adapter = echt(gepatcht); + return { + ...adapter, + sendPayload: (daten) => { + const ok = adapter.sendPayload(daten); + schritte.angefragt = ok; + return ok; + }, + }; + }; +} + +/** Aus den drei Häkchen wird ein Satz, der sagt, was zu tun ist */ +export function beitrittsDiagnose(schritte) { + if (schritte.angefragt === false) { + return 'Die Verbindung zu Discord war nicht bereit — der Beitritt ging gar nicht erst raus.'; + } + if (!schritte.eigenerStatus && !schritte.sprachserver) { + return 'Discord hat auf den Beitritt nicht geantwortet. Das ist fast immer eine ' + + 'fehlende Berechtigung **Verbinden** im Sprachkanal selbst.'; + } + if (!schritte.sprachserver) { + return 'Discord kennt den Beitritt, nennt aber keinen Sprachserver — ' + + 'darf der Bot in diesem Kanal sprechen?'; + } + if (!schritte.eigenerStatus) { + return 'Der Sprachserver steht, aber der eigene Sprachstatus kam nie an — ' + + 'dem Bot fehlt der Intent **GuildVoiceStates**.'; + } + return 'Der Austausch mit dem Sprachserver kam nicht zu Ende — ausgehendes UDP dicht?'; +} + /** Läuft gerade etwas auf diesem Server? */ export const laeuft = (guildId) => laufend.get(guildId) ?? null; @@ -161,13 +225,16 @@ export async function spielen(kanal, station) { const guildId = kanal.guild.id; const vorher = laufend.get(guildId); + // Wird nur gefüllt, wenn wirklich neu verbunden wird + const schritte = { angefragt: null, eigenerStatus: false, sprachserver: false }; + let verbindung = getVoiceConnection(guildId); if (!verbindung || verbindung.joinConfig.channelId !== kanal.id) { verbindung?.destroy(); verbindung = joinVoiceChannel({ channelId: kanal.id, guildId, - adapterCreator: kanal.guild.voiceAdapterCreator, + adapterCreator: beobachteterAdapter(kanal.guild, schritte), selfDeaf: true, // der Bot muss nicht hören, nur senden }); // Discord verschiebt Sprachserver im Betrieb; ohne dieses Zurückholen @@ -213,12 +280,10 @@ export async function spielen(kanal, station) { const steckt = verbindung.state.status; verbindung.destroy(); laufend.delete(guildId); - 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}`); + const grund = beitrittsDiagnose(schritte); + console.error(`[radio] Beitritt steckt bei "${steckt}" — ` + + `angefragt: ${schritte.angefragt}, eigener Status: ${schritte.eigenerStatus}, ` + + `Sprachserver: ${schritte.sprachserver}`); throw new Error(`Sprachverbindung kam nicht zustande. ${grund}`); }