From dd9f2f6bbcd15f72e2b6159f5885bbe29fd29c5e Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 2 Sep 2026 00:30:57 +0700 Subject: [PATCH] fix(voice): self-undeafen/self-unmute bot instantly on server mute/deafen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously forceSelfServerUnmuteUndeafen only ran on video-watch attempts and after voice reconnects — so when an admin server-muted or server-deafened the bot, it stayed muted/deafened for minutes (or forever if no streamer came on). Add registerSelfVoiceStateGuard: a voiceStateUpdate listener that detects the bot's own serverMute/serverDeaf transition to true and immediately re-issues mute:false,deaf:false. Wired at client-ready in bootstrap.ts. Best-effort, idempotent, never blocks the gateway. --- services/discord-gateway/src/app/bootstrap.ts | 7 +++ .../src/modules/voice-recording/recorder.ts | 63 ++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/services/discord-gateway/src/app/bootstrap.ts b/services/discord-gateway/src/app/bootstrap.ts index b49e8f57..8913bc51 100644 --- a/services/discord-gateway/src/app/bootstrap.ts +++ b/services/discord-gateway/src/app/bootstrap.ts @@ -36,6 +36,7 @@ import { registerPresenceCapture } from "../modules/user-presence/index.js"; import { VoicePcmWsClient } from "../modules/voice-pcm-ws/index.js"; import { startMuxerWorker } from "../modules/voice-recording/muxer.js"; import { + registerSelfVoiceStateGuard, setPcmWsClient, setEventBroadcaster as setRecorderEventBroadcaster, } from "../modules/voice-recording/recorder.js"; @@ -298,6 +299,12 @@ export async function initializeDiscordGateway() { ), ); + // Attach the immediate self-undeafen/self-unmute guard so the bot reacts + // INSTANTLY when an admin server-mutes or server-deafens it (previously + // only re-asserted on video-watch/reconnect, leaving the bot muted for + // minutes). + registerSelfVoiceStateGuard(client); + // Start retention cleanup scheduler startRetentionCleanup(); // Start weekly moderation digest (public, automated) diff --git a/services/discord-gateway/src/modules/voice-recording/recorder.ts b/services/discord-gateway/src/modules/voice-recording/recorder.ts index dad89323..9202ae65 100644 --- a/services/discord-gateway/src/modules/voice-recording/recorder.ts +++ b/services/discord-gateway/src/modules/voice-recording/recorder.ts @@ -7,7 +7,12 @@ import { type VoiceConnection, VoiceConnectionStatus, } from "@discordjs/voice"; -import type { Client, Guild, VoiceChannel } from "discord.js-selfbot-v13"; +import type { + Client, + Guild, + VoiceChannel, + VoiceState, +} from "discord.js-selfbot-v13"; import { createChildLogger } from "@/shared/logger/index"; import { retryWithBackoff } from "@/shared/utils/index"; import { config } from "../../shared/config/config.js"; @@ -80,6 +85,62 @@ export async function forceSelfServerUnmuteUndeafen( } } +/** + * Attach an IMMEDIATE self-undeafen / self-unmute guard on `voiceStateUpdate`. + * + * Previous behaviour only re-asserted unmute/undeafen on video-watch attempts + * and after voice reconnects — so when an admin server-muted or + * server-deafened the bot the bot stayed muted/deafened until the *next* video + * watch or reconnect cycle (could be minutes or never if no streamer came on). + * + * This listener fires on EVERY guild voice-state change. When it detects that + * the bot's OWN voice state transitioned to serverDeaf||serverMute === true, + * it immediately re-issues `mute:false, deaf:false` so the bot is unmuted/ + * undeafened instantly (well within Discord's rate limit window for the bot's + * own member). Idempotent and best-effort — failures are logged, never fatal. + * + * Idempotent: safe to call multiple times; the listener is attached once. + */ +let _selfVoiceGuardAttached = false; +export function registerSelfVoiceStateGuard(client: Client): void { + if (_selfVoiceGuardAttached) return; + _selfVoiceGuardAttached = true; + client.on( + "voiceStateUpdate", + (oldState: VoiceState, newState: VoiceState) => { + void handleSelfVoiceStateUpdate(client, oldState, newState); + }, + ); +} + +async function handleSelfVoiceStateUpdate( + client: Client, + oldState: VoiceState, + newState: VoiceState, +): Promise { + // Only react to the bot's own voice state transitions. + const selfId = client.user?.id; + if (!selfId || newState.id !== selfId) return; + + const justServerMuted = !oldState.serverMute && newState.serverMute === true; + const justServerDeafed = !oldState.serverDeaf && newState.serverDeaf === true; + if (!justServerMuted && !justServerDeafed) return; + + const guild = newState.guild; + if (!guild) return; + + logger.info( + { + guildId: guild.id, + serverMuted: justServerMuted, + serverDeafed: justServerDeafed, + }, + "Bot was server-muted/deafened — immediately self-undeafening + unmuting", + ); + // Fire-and-forget the corrective edit; forceSelfServerUnmuteUndeafen logs. + void forceSelfServerUnmuteUndeafen(client, guild); +} + let _pcmWsClient: VoicePcmWsClient | undefined; export function setPcmWsClient(client: VoicePcmWsClient | undefined) {