/** * Userbot (selfbot) entry — runs the assistant on a single USER account token, * which is the only kind of account Discord lets Go Live (screen-share). * * Voice goes through @discordjs/voice (>=0.19 + @snazzah/davey for Discord's * DAVE E2EE) using the selfbot client's `voiceAdapterCreator`. The selfbot * library's OWN native voice connection does NOT complete the current * (DAVE/v8) handshake and times out, so we deliberately reuse the normal-bot * VoiceSession (receive -> Whisper -> reply -> TTS playback) instead, just with * a user-account channel. * * Stage 1 (this module): log in, join (auto from DISCORD_VOICE_CHANNEL_ID or via * "!자비스 join"), converse by voice. Go-Live broadcast + broadcast-coupled * search routing land in stage 2. * * ToS WARNING: automating a user account ("selfbot") violates Discord's ToS and * can get the account banned. Use a throwaway/burner account only. */ import type { VoiceBasedChannel } from "discord.js"; import { config } from "./config.ts"; import { joinChannel, leaveGuild } from "./voice.ts"; type AnyClient = any; async function loadSelfbot(): Promise { try { return await import("discord.js-selfbot-v13"); } catch (e) { throw new Error( "유저봇 의존성이 없습니다. 설치: bun add discord.js-selfbot-v13\n" + `원본 오류: ${(e as Error).message}`, ); } } async function joinAndListen(client: AnyClient, channelId: string): Promise { const channel: any = await client.channels.fetch(channelId).catch(() => null); if (!channel || channel.isVoice?.() === false) { console.error(`[userbot] voice channel ${channelId} not found / not a voice channel`); return; } // The selfbot VoiceChannel is runtime-compatible with @discordjs/voice's // joinVoiceChannel (it exposes id, guild.id and guild.voiceAdapterCreator). const session = await joinChannel(channel as unknown as VoiceBasedChannel); session.onTurn = ({ transcript, reply }) => console.log(`🗣️ ${transcript}\n🤖 ${reply}`); console.log(`🎙️ 유저봇이 '${channel.name}' 음성채널에 참여했습니다.`); } export async function runUserbot(): Promise { if (!config.selfbotToken) { throw new Error("DISCORD_SELFBOT_TOKEN이 설정되지 않았습니다 (.env). 버너 계정 토큰을 넣어주세요."); } const selfbot = await loadSelfbot(); const client = new selfbot.Client(); client.on("ready", async () => { console.log(`✓ 유저봇 로그인: ${client.user?.tag}`); if (config.autoJoinChannelId) { await joinAndListen(client, config.autoJoinChannelId).catch((e) => console.error("[userbot] auto-join failed:", e), ); } else { console.log('[userbot] DISCORD_VOICE_CHANNEL_ID 미설정 — "!자비스 join" 명령을 기다립니다.'); } }); // Text-command control (userbots cannot use slash commands): join the // caller's current voice channel, or leave. client.on("messageCreate", async (msg: any) => { const content = (msg.content || "").trim(); if (content === "!자비스 join" || content === "!jarvis join") { const ch = msg.member?.voice?.channel || msg.author?.voice?.channel; if (ch) await joinAndListen(client, ch.id).catch((e) => console.error("[userbot] join cmd:", e)); } else if (content === "!자비스 leave" || content === "!jarvis leave") { leaveGuild(config.guildId); } }); await client.login(config.selfbotToken); }