From 863337c6eb76984d6493559f9da9b78335764bf9 Mon Sep 17 00:00:00 2001 From: javis-bot Date: Sat, 13 Jun 2026 15:30:33 +0900 Subject: [PATCH] feat(selfbot): dedicated DISCORD_STREAM_TOKEN for the broadcast account Confirmed root cause of "broadcast doesn't appear in Discord" in userbot mode: the conversation and the Go-Live broadcaster were the SAME Discord account on two sessions. Discord allows one voice presence per account, so the broadcaster's voice connection never connects (state voiceReady:false). Proven by isolating the broadcaster: alone it connects ("Go-Live WebRTC connected"); alongside the conversation it times out. The broadcaster now logs in with DISCORD_STREAM_TOKEN when set (a second burner account dedicated to streaming), falling back to DISCORD_SELFBOT_TOKEN (correct for normal-bot mode). When userbot mode shares one account it warns loudly with the fix. Documents the var in .env.example. Co-Authored-By: Claude Opus 4.7 --- .env.example | 8 ++++++++ bot/src/config.ts | 8 ++++++++ bot/src/stream/selfbot.ts | 21 ++++++++++++++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index e2f92df..0873b18 100644 --- a/.env.example +++ b/.env.example @@ -123,6 +123,14 @@ VNC_BITRATE_KBPS=8000 # A THROWAWAY/burner Discord user account token. NEVER your main account. # Using a selfbot violates Discord ToS and can get the account banned. DISCORD_SELFBOT_TOKEN= +# Dedicated burner account for the Go-Live BROADCAST, separate from the +# conversation account above. REQUIRED in userbot mode for the broadcast to +# work: Discord allows only one voice presence per account, so the conversation +# and the broadcast cannot share one account (the broadcaster's voice connection +# never connects). Leave empty in normal-bot mode (the conversation runs on the +# bot account, so the selfbot account is already broadcast-only). Both burner +# accounts must be in the server. Use a second throwaway account, never a main. +DISCORD_STREAM_TOKEN= # Hardware (NVENC) encode for the stream. 1 = use the GPU (recommended for # 1080p60), 0 = software x264. Requires an NVIDIA GPU + ffmpeg built with nvenc. STREAM_HW=1 diff --git a/bot/src/config.ts b/bot/src/config.ts index 1b02dfb..691ba96 100644 --- a/bot/src/config.ts +++ b/bot/src/config.ts @@ -46,6 +46,14 @@ export const config = { // selfbot backend (ToS-risk; use a throwaway account token, never your main) selfbotToken: opt("DISCORD_SELFBOT_TOKEN"), + // Account used for the Go-Live broadcast. In userbot mode the conversation and + // the broadcast CANNOT share one account: Discord allows a single voice + // presence per account, so the broadcaster's second session never connects + // (confirmed: alone it goes Live, alongside the conversation it times out). + // Set DISCORD_STREAM_TOKEN to a SECOND burner account dedicated to the + // broadcast. Empty = reuse DISCORD_SELFBOT_TOKEN (correct for normal-bot mode, + // where the conversation runs on the bot account, not the selfbot account). + streamToken: opt("DISCORD_STREAM_TOKEN"), // How long to wait for the Go-Live stream WebRTC to actually reach // "connected" before declaring the broadcast failed (and tearing the local // ffmpeg pipeline down). Guards against reporting "live" when nothing reaches diff --git a/bot/src/stream/selfbot.ts b/bot/src/stream/selfbot.ts index d950ca1..ec5e387 100644 --- a/bot/src/stream/selfbot.ts +++ b/bot/src/stream/selfbot.ts @@ -130,8 +130,23 @@ export class SelfbotStreamer implements ScreenStreamer { if (this.config.screenBrowser === false) { return "화면 공유(브라우저) 모드가 꺼져 있습니다 (STREAM_BROWSER=false). 음성 + API/MCP 모드로만 동작합니다."; } - if (!this.config.selfbotToken) { - return "DISCORD_SELFBOT_TOKEN이 설정되지 않았습니다 (.env). 버너 계정 토큰을 넣어주세요."; + // Broadcast account: a dedicated DISCORD_STREAM_TOKEN if provided, otherwise + // reuse the conversation's selfbot token. In userbot mode (no normal-bot + // token) sharing one account fails — the broadcaster's voice connection + // can't establish alongside the conversation's, so the Go-Live never + // connects. Warn loudly in that case so the failure is self-explanatory. + const streamToken = this.config.streamToken || this.config.selfbotToken; + if (!streamToken) { + return "DISCORD_SELFBOT_TOKEN(또는 DISCORD_STREAM_TOKEN)이 설정되지 않았습니다 (.env). 버너 계정 토큰을 넣어주세요."; + } + const dedicatedStreamAccount = + !!this.config.streamToken && this.config.streamToken !== this.config.selfbotToken; + if (!dedicatedStreamAccount && !this.config.botToken) { + console.warn( + "[selfbot] ⚠️ 방송과 대화가 같은 계정(DISCORD_SELFBOT_TOKEN)을 사용합니다. " + + "유저봇 모드에서는 음성 채널을 동시에 점유할 수 없어 Go-Live가 연결되지 않습니다. " + + "방송 전용 DISCORD_STREAM_TOKEN(별도 버너 계정)을 설정하세요.", + ); } if (!ctx.voiceChannelId) { return "셀프봇 송출은 음성 채널 안에서 호출해야 합니다."; @@ -158,7 +173,7 @@ export class SelfbotStreamer implements ScreenStreamer { signal.throwIfAborted(); streamer = this.streamer = new Streamer(new selfbot.Client()); - await streamer.client.login(this.config.selfbotToken); + await streamer.client.login(streamToken); signal.throwIfAborted(); // Act like a person, not a bot: take a breath after coming online before