Radio: den Beitritt in drei Schritte zerlegen statt "signalling" zu melden
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s

"signalling" sagt nur, dass etwas fehlt, nicht was. Ein Beitritt zum
Sprachkanal ist ein Dreischritt:

  1. Der Bot bittet Discord ueber die bestehende Verbindung um den Beitritt.
  2. Discord antwortet mit dem eigenen Sprachstatus (darin die Sitzungs-ID).
  3. Discord nennt den Sprachserver.

Der Adapter zwischen Verbindung und Sprachbibliothek haelt jetzt fest,
welcher davon stattgefunden hat. Aus den drei Haekchen wird ein Satz, der
sagt, was zu tun ist — fehlende Berechtigung, fehlender Intent, oder ein
Netz, das UDP nicht rauslaesst. Fuenf Faelle, fuenf verschiedene Antworten;
der Test besteht darauf, dass keine zwei gleich lauten.

Dazu eine Pruefung vorweg, die genau dasselbe Bild erzeugt haette: ein
voller Sprachkanal. Discord nimmt die Anfrage entgegen und antwortet dann
einfach nicht mehr — das sieht aus wie ein Rechteproblem. Wer Mitglieder
verschieben darf, kommt am Limit vorbei, sonst nicht.

Nachgesehen und ausgeschlossen: discord.js 14.27 verdrahtet beide
Sprachereignisse (VOICE_STATE_UPDATE ueber die Action, VOICE_SERVER_UPDATE
ueber den Handler), und GuildVoiceStates steht in den Intents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 12:09:59 +02:00
co-authored by Claude Opus 5
parent e246dadbec
commit bbb56e1276
2 changed files with 83 additions and 7 deletions
+72 -7
View File
@@ -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}`);
}