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 <noreply@anthropic.com>
This commit is contained in:
javis-bot
2026-06-13 15:30:33 +09:00
parent 5961faed38
commit 863337c6eb
3 changed files with 34 additions and 3 deletions

View File

@@ -123,6 +123,14 @@ VNC_BITRATE_KBPS=8000
# A THROWAWAY/burner Discord user account token. NEVER your main account. # A THROWAWAY/burner Discord user account token. NEVER your main account.
# Using a selfbot violates Discord ToS and can get the account banned. # Using a selfbot violates Discord ToS and can get the account banned.
DISCORD_SELFBOT_TOKEN= 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 # Hardware (NVENC) encode for the stream. 1 = use the GPU (recommended for
# 1080p60), 0 = software x264. Requires an NVIDIA GPU + ffmpeg built with nvenc. # 1080p60), 0 = software x264. Requires an NVIDIA GPU + ffmpeg built with nvenc.
STREAM_HW=1 STREAM_HW=1

View File

@@ -46,6 +46,14 @@ export const config = {
// selfbot backend (ToS-risk; use a throwaway account token, never your main) // selfbot backend (ToS-risk; use a throwaway account token, never your main)
selfbotToken: opt("DISCORD_SELFBOT_TOKEN"), 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 // How long to wait for the Go-Live stream WebRTC to actually reach
// "connected" before declaring the broadcast failed (and tearing the local // "connected" before declaring the broadcast failed (and tearing the local
// ffmpeg pipeline down). Guards against reporting "live" when nothing reaches // ffmpeg pipeline down). Guards against reporting "live" when nothing reaches

View File

@@ -130,8 +130,23 @@ export class SelfbotStreamer implements ScreenStreamer {
if (this.config.screenBrowser === false) { if (this.config.screenBrowser === false) {
return "화면 공유(브라우저) 모드가 꺼져 있습니다 (STREAM_BROWSER=false). 음성 + API/MCP 모드로만 동작합니다."; return "화면 공유(브라우저) 모드가 꺼져 있습니다 (STREAM_BROWSER=false). 음성 + API/MCP 모드로만 동작합니다.";
} }
if (!this.config.selfbotToken) { // Broadcast account: a dedicated DISCORD_STREAM_TOKEN if provided, otherwise
return "DISCORD_SELFBOT_TOKEN이 설정되지 않았습니다 (.env). 버너 계정 토큰을 넣어주세요."; // 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) { if (!ctx.voiceChannelId) {
return "셀프봇 송출은 음성 채널 안에서 호출해야 합니다."; return "셀프봇 송출은 음성 채널 안에서 호출해야 합니다.";
@@ -158,7 +173,7 @@ export class SelfbotStreamer implements ScreenStreamer {
signal.throwIfAborted(); signal.throwIfAborted();
streamer = this.streamer = new Streamer(new selfbot.Client()); streamer = this.streamer = new Streamer(new selfbot.Client());
await streamer.client.login(this.config.selfbotToken); await streamer.client.login(streamToken);
signal.throwIfAborted(); signal.throwIfAborted();
// Act like a person, not a bot: take a breath after coming online before // Act like a person, not a bot: take a breath after coming online before