From cc5e21fe1c30098ad00df8921a3f890041cad3be Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 6 Mar 2026 01:27:50 +0100 Subject: [PATCH] debug: add voice adapter wrapper to trace gateway communication Logs sendPayload calls (op code, result), onVoiceServerUpdate and onVoiceStateUpdate to identify why connection stays at signalling. Co-Authored-By: Claude Opus 4.6 --- server/src/plugins/soundboard/index.ts | 41 +++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/server/src/plugins/soundboard/index.ts b/server/src/plugins/soundboard/index.ts index d7ef5b9..ca395da 100644 --- a/server/src/plugins/soundboard/index.ts +++ b/server/src/plugins/soundboard/index.ts @@ -238,7 +238,7 @@ async function ensureConnectionReady(connection: VoiceConnection, channelId: str try { connection.destroy(); } catch {} guildAudioState.delete(guildId); console.log(`${SB} Creating fresh connection (attempt 3)...`); - const newConn = joinVoiceChannel({ channelId, guildId, adapterCreator: guild.voiceAdapterCreator as any, selfMute: false, selfDeaf: false }); + const newConn = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); try { await entersState(newConn, VoiceConnectionStatus.Ready, 15_000); console.log(`${SB} Connection ready (fresh)`); return newConn; } catch (e) { console.error(`${SB} All 3 connection attempts failed: ${(e as Error)?.message ?? e}`); try { newConn.destroy(); } catch {} guildAudioState.delete(guildId); throw new Error('Voice connection failed after 3 attempts'); } } @@ -267,13 +267,13 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { catch { if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; console.log(`${SB} Rejoin attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`); connection.rejoin({ channelId: state.channelId, selfDeaf: false, selfMute: false }); } else { reconnectAttempts = 0; console.log(`${SB} Max reconnect attempts reached, creating fresh connection`); try { connection.destroy(); } catch {} - const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: guild.voiceAdapterCreator as any, selfMute: false, selfDeaf: false }); + const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); state.connection = nc; nc.subscribe(state.player); attachVoiceLifecycle(state, guild); } } } else if (newS.status === VoiceConnectionStatus.Destroyed) { console.warn(`${SB} Connection destroyed, recreating...`); connectedSince.delete(state.guildId); - const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: guild.voiceAdapterCreator as any, selfMute: false, selfDeaf: false }); + const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); state.connection = nc; nc.subscribe(state.player); attachVoiceLifecycle(state, guild); } else if (newS.status === VoiceConnectionStatus.Connecting || newS.status === VoiceConnectionStatus.Signalling) { isReconnecting = true; @@ -284,7 +284,7 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { console.warn(`${SB} Timeout waiting for Ready from ${newS.status} (attempt ${reconnectAttempts}): ${(e as Error)?.message ?? e}`); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { await new Promise(r => setTimeout(r, reconnectAttempts * 2000)); isReconnecting = false; connection.rejoin({ channelId: state.channelId, selfDeaf: false, selfMute: false }); } else { reconnectAttempts = 0; isReconnecting = false; console.error(`${SB} Max attempts from ${newS.status}, fresh connection`); try { connection.destroy(); } catch {} - const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: guild.voiceAdapterCreator as any, selfMute: false, selfDeaf: false }); + const nc = joinVoiceChannel({ channelId: state.channelId, guildId: state.guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); state.connection = nc; nc.subscribe(state.player); attachVoiceLifecycle(state, guild); } } } @@ -293,6 +293,33 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { (connection as any).__lifecycleAttached = true; } +// ── Debug adapter wrapper ── +function debugAdapterCreator(guild: any): any { + const original = guild.voiceAdapterCreator; + return (methods: any) => { + const wrappedMethods = { + ...methods, + sendPayload(payload: any) { + const result = methods.sendPayload(payload); + console.log(`${SB} adapter.sendPayload op=${payload?.op ?? '?'} d.channel_id=${payload?.d?.channel_id ?? '?'} → ${result}`); + return result; + }, + }; + const adapter = original(wrappedMethods); + const origVSU = adapter.onVoiceServerUpdate; + const origVStU = adapter.onVoiceStateUpdate; + adapter.onVoiceServerUpdate = (data: any) => { + console.log(`${SB} adapter.onVoiceServerUpdate: token=${data?.token ? 'yes' : 'no'} endpoint=${data?.endpoint ?? 'none'}`); + return origVSU(data); + }; + adapter.onVoiceStateUpdate = (data: any) => { + console.log(`${SB} adapter.onVoiceStateUpdate: session_id=${data?.session_id ? 'yes' : 'no'} channel_id=${data?.channel_id ?? 'none'}`); + return origVStU(data); + }; + return adapter; + }; +} + // ── Playback ── let _pluginCtx: PluginContext | null = null; @@ -305,7 +332,7 @@ async function playFilePath(guildId: string, channelId: string, filePath: string let state = guildAudioState.get(guildId); if (!state) { console.log(`${SB} No existing audio state, creating new connection...`); - const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: guild.voiceAdapterCreator as any, selfMute: false, selfDeaf: false }); + const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); connection.subscribe(player); state = { connection, player, guildId, channelId, currentVolume: getPersistedVolume(guildId) }; @@ -322,7 +349,7 @@ async function playFilePath(guildId: string, channelId: string, filePath: string const current = getVoiceConnection(guildId); if (current && current.joinConfig?.channelId !== channelId) { current.destroy(); - const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: guild.voiceAdapterCreator as any, selfMute: false, selfDeaf: false }); + const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); const player = state.player ?? createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); connection.subscribe(player); state = { connection, player, guildId, channelId, currentVolume: state.currentVolume ?? getPersistedVolume(guildId) }; @@ -333,7 +360,7 @@ async function playFilePath(guildId: string, channelId: string, filePath: string } catch {} if (!getVoiceConnection(guildId)) { - const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: guild.voiceAdapterCreator as any, selfMute: false, selfDeaf: false }); + const connection = joinVoiceChannel({ channelId, guildId, adapterCreator: debugAdapterCreator(guild), selfMute: false, selfDeaf: false }); const player = state?.player ?? createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); connection.subscribe(player); state = { connection, player, guildId, channelId, currentVolume: state?.currentVolume ?? getPersistedVolume(guildId) };