diff --git a/dave/join.mjs b/dave/join.mjs index 566748f..00ff134 100644 --- a/dave/join.mjs +++ b/dave/join.mjs @@ -46,7 +46,10 @@ const ssrcToUser = new Map(); // audio ssrc -> user id const rtpCount = new Map(); // ssrc -> packet count const voiceState = { session_id: null, token: null, endpoint: null, ssrc: null, ip: null, port: null, mode: null }; +let leaving = false; function leaveAndExit(code = 0) { + if (leaving) return; // idempotent: hard ceiling + ready timer + signals must not double-fire + leaving = true; try { mainWs?.send(JSON.stringify({ op: 4, d: { guild_id: GUILD_ID, channel_id: null, self_mute: true, self_deaf: true } })); } catch {} setTimeout(() => { try { voiceWs?.close(); } catch {} @@ -59,6 +62,11 @@ function leaveAndExit(code = 0) { process.on('SIGINT', () => { log('SIGINT'); leaveAndExit(0); }); process.on('SIGTERM', () => { log('SIGTERM'); leaveAndExit(0); }); +// Hard time-box ceiling, armed at startup regardless of handshake state. Without +// this, a partial join (e.g. DAVE/MLS never completes op29/op30 so announceReady +// never fires) would run the selfbot forever — a guardrail hole for a live test. +if (RUN_MS > 0) setTimeout(() => { log(`RUN_MS=${RUN_MS} hard ceiling elapsed — leaving`); leaveAndExit(0); }, RUN_MS); + // ---------- MAIN GATEWAY ---------- mainWs = new WebSocket('wss://gateway.discord.gg/?v=10&encoding=json'); let mainHb; @@ -190,7 +198,7 @@ function maybeConnectVoice() { announced = true; log(`✅ JOINED & READY. channel=${CHANNEL_ID} dave=${voiceState.daveVer} mlsReady=${mlsReady} privacyCode=${daveSession?.voicePrivacyCode || 'n/a'}`); log(' staying connected, listening for speakers…'); - if (RUN_MS > 0) setTimeout(() => { log(`RUN_MS=${RUN_MS} elapsed`); leaveAndExit(0); }, RUN_MS); + // time-box is owned by the startup hard-ceiling timer (armed regardless of ready state) } }