Created
January 24, 2026 13:41
-
-
Save Sayrix/5c8ac9d05051f82c5e8af222f7d1aa39 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==================== CONFIGURATION ==================== | |
| const CONFIG = { | |
| DISCORD_TOKEN: "YOUR_DISCORD_TOKEN_HERE", | |
| GUILD_ID: "YOUR_GUILD_ID_HERE", // Discord server ID | |
| CHANNEL_ID: "YOUR_CHANNEL_ID_HERE", // Voice channel ID to join | |
| STREAM_URL: | |
| "https://archive.org/download/kevin-mc-leod-monkeys-spinning-monkeys/Kevin%20McLeod%20-%20Monkeys%20Spinning%20Monkeys.ogg" // Ogg Opus stream | |
| }; | |
| // ==================== IMPORTS ==================== | |
| const { Client, GatewayIntentBits } = require("@discordjs/core"); | |
| const { REST } = require("@discordjs/rest"); | |
| const { WebSocketManager } = require("@discordjs/ws"); | |
| const { | |
| joinVoiceChannel, | |
| createAudioPlayer, | |
| createAudioResource, | |
| NoSubscriberBehavior, | |
| VoiceConnectionStatus, | |
| AudioPlayerStatus, | |
| StreamType, | |
| entersState | |
| } = require("@discordjs/voice"); | |
| const { LRUCache } = require("lru-cache"); | |
| const { GatewayDispatchEvents } = require("discord-api-types/v10"); | |
| // ==================== VOICE ADAPTER CREATOR ==================== | |
| /** | |
| * Creates a voice adapter creator that bridges @discordjs/core with @discordjs/voice | |
| * Simplified version assuming single shard (shard 0) | |
| */ | |
| function createVoiceAdapterCreator(gateway, client) { | |
| const adapters = new Map(); // guildId -> adapter | |
| const sharedState = { | |
| adapters, | |
| trackedBotId: null | |
| }; | |
| // Set up event handlers once | |
| client.on(GatewayDispatchEvents.VoiceStateUpdate, (data) => { | |
| const voiceState = data.data; | |
| const userId = voiceState.user_id; | |
| const guildId = voiceState.guild_id; | |
| // Track bot's user ID from the first voice state update | |
| if (sharedState.trackedBotId === null && voiceState.member?.user?.id) { | |
| sharedState.trackedBotId = voiceState.member.user.id; | |
| } | |
| // Only process updates for the bot itself | |
| if (userId !== sharedState.trackedBotId) return; | |
| const adapter = adapters.get(guildId); | |
| if (!adapter) return; | |
| // If bot was disconnected, destroy the adapter | |
| if (voiceState.channel_id === null) { | |
| adapter.destroy(); | |
| return; | |
| } | |
| // Call the adapter's onVoiceStateUpdate if it's been set by the voice library | |
| if (typeof adapter.onVoiceStateUpdate === "function") { | |
| adapter.onVoiceStateUpdate(voiceState); | |
| } | |
| }); | |
| client.on(GatewayDispatchEvents.VoiceServerUpdate, (data) => { | |
| const voiceServer = data.data; | |
| const guildId = voiceServer.guild_id; | |
| const adapter = adapters.get(guildId); | |
| if (!adapter) return; | |
| // Call the adapter's onVoiceServerUpdate if it's been set by the voice library | |
| if (typeof adapter.onVoiceServerUpdate === "function") { | |
| adapter.onVoiceServerUpdate(voiceServer); | |
| } | |
| }); | |
| return (methods) => { | |
| // Extract guild ID from the first method call or use a temporary approach | |
| // The voice library will call these methods, and we'll figure out the guild from the data | |
| const guildId = CONFIG.GUILD_ID; // For single-guild benchmark, use the configured guild | |
| const adapter = { | |
| destroyed: false, | |
| sendPayload(data) { | |
| if (this.destroyed) return false; | |
| const shardId = 0; // Always use shard 0 for single shard | |
| try { | |
| if (typeof gateway.send === "function") { | |
| gateway.send(shardId, data); | |
| return true; | |
| } | |
| } catch (err) { | |
| console.error("[Adapter] Error sending payload:", err); | |
| } | |
| return false; | |
| }, | |
| destroy() { | |
| if (this.destroyed) return; | |
| this.destroyed = true; | |
| adapters.delete(guildId); | |
| console.log(`[Adapter] Destroyed for guild ${guildId}`); | |
| } | |
| }; | |
| // Store the methods from the voice library | |
| adapter.onVoiceStateUpdate = methods.onVoiceStateUpdate; | |
| adapter.onVoiceServerUpdate = methods.onVoiceServerUpdate; | |
| adapters.set(guildId, adapter); | |
| return adapter; | |
| }; | |
| } | |
| // ==================== LRU CACHE WITH CHANNEL INDEX ==================== | |
| /** | |
| * Voice state cache with O(1) member counting per channel | |
| * Maintains a secondary index: guildId -> channelId -> Set<userId> | |
| */ | |
| const channelIndex = new Map(); // guildId -> Map<channelId, Set<userId>> | |
| function removeFromIndex(guildId, channelId, userId) { | |
| if (!channelId) return; | |
| const guildChannels = channelIndex.get(guildId); | |
| if (!guildChannels) return; | |
| const channelMembers = guildChannels.get(channelId); | |
| if (!channelMembers) return; | |
| channelMembers.delete(userId); | |
| if (channelMembers.size === 0) { | |
| guildChannels.delete(channelId); | |
| if (guildChannels.size === 0) { | |
| channelIndex.delete(guildId); | |
| } | |
| } | |
| } | |
| function addToIndex(guildId, channelId, userId, isBot) { | |
| if (!channelId || isBot) return; | |
| if (!channelIndex.has(guildId)) { | |
| channelIndex.set(guildId, new Map()); | |
| } | |
| const guildChannels = channelIndex.get(guildId); | |
| if (!guildChannels.has(channelId)) { | |
| guildChannels.set(channelId, new Set()); | |
| } | |
| guildChannels.get(channelId).add(userId); | |
| } | |
| const voiceStateCache = new LRUCache({ | |
| maxSize: 999999999, | |
| sizeCalculation: () => 1, | |
| dispose: (value, key) => { | |
| const parts = key.split(":"); | |
| const guildId = parts[0]; | |
| const userId = parts.slice(1).join(":"); | |
| removeFromIndex(guildId, value.data.channel_id, userId); | |
| } | |
| }); | |
| const voiceStateUtils = { | |
| setSnapshot(guildId, userId, data) { | |
| const key = `${guildId}:${userId}`; | |
| const old = voiceStateCache.peek(key); | |
| // Remove from old channel index | |
| if (old?.data.channel_id) { | |
| removeFromIndex(guildId, old.data.channel_id, userId); | |
| } | |
| // Add to new channel index | |
| if (data.channel_id) { | |
| const isBot = data.member?.user?.bot || false; | |
| addToIndex(guildId, data.channel_id, userId, isBot); | |
| } | |
| voiceStateCache.set(key, { | |
| data, | |
| ts: Date.now() | |
| }); | |
| }, | |
| getSnapshot(guildId, userId) { | |
| const key = `${guildId}:${userId}`; | |
| return voiceStateCache.get(key); | |
| }, | |
| peekSnapshot(guildId, userId) { | |
| const key = `${guildId}:${userId}`; | |
| return voiceStateCache.peek(key); | |
| }, | |
| deleteSnapshot(guildId, userId) { | |
| const key = `${guildId}:${userId}`; | |
| voiceStateCache.delete(key); | |
| }, | |
| countMembersInChannel(guildId, channelId) { | |
| const guildChannels = channelIndex.get(guildId); | |
| if (!guildChannels) return 0; | |
| const channelMembers = guildChannels.get(channelId); | |
| if (!channelMembers) return 0; | |
| return channelMembers.size; | |
| }, | |
| getMembersInChannel(guildId, channelId) { | |
| const guildChannels = channelIndex.get(guildId); | |
| if (!guildChannels) return []; | |
| const channelMembers = guildChannels.get(channelId); | |
| if (!channelMembers) return []; | |
| return Array.from(channelMembers) | |
| .map((userId) => { | |
| const snapshot = this.peekSnapshot(guildId, userId); | |
| return snapshot?.data; | |
| }) | |
| .filter(Boolean); | |
| }, | |
| clear() { | |
| voiceStateCache.clear(); | |
| channelIndex.clear(); | |
| } | |
| }; | |
| // ==================== CONNECTION MANAGER & AUDIO PLAYER ==================== | |
| const activeConnections = new Map(); // guildId -> VoiceConnection | |
| // Create shared audio player | |
| const player = createAudioPlayer({ | |
| behaviors: { | |
| maxMissedFrames: 9999, | |
| noSubscriber: NoSubscriberBehavior.Stop | |
| } | |
| }); | |
| player.on("stateChange", (oldState, newState) => { | |
| console.log(`[Player] ${oldState.status} -> ${newState.status}`); | |
| }); | |
| player.on(AudioPlayerStatus.Idle, () => { | |
| if (player.subscribers && player.subscribers.length > 0) { | |
| console.log("[Player] Idle - attempting to restart stream"); | |
| playStream(); | |
| } else { | |
| console.log("[Player] Idle - no subscribers, not restarting"); | |
| } | |
| }); | |
| player.on("error", (error) => { | |
| console.error("[Player] Error:", error); | |
| }); | |
| function playStream() { | |
| try { | |
| const resource = createAudioResource(CONFIG.STREAM_URL, { | |
| inputType: StreamType.OggOpus | |
| }); | |
| player.play(resource); | |
| console.log("[Player] Started playing stream"); | |
| } catch (error) { | |
| console.error("[Player] Failed to create audio resource:", error); | |
| } | |
| } | |
| function getConnection(guildId) { | |
| return activeConnections.get(guildId); | |
| } | |
| function destroyConnection(guildId) { | |
| const connection = activeConnections.get(guildId); | |
| if (!connection) return false; | |
| if (connection.state.status !== VoiceConnectionStatus.Destroyed) { | |
| try { | |
| connection.state.subscription?.unsubscribe(); | |
| } catch (err) { | |
| console.error("[Connection] Error unsubscribing:", err); | |
| } | |
| try { | |
| connection.destroy(); | |
| console.log(`[Connection] Destroyed for guild ${guildId}`); | |
| } catch (err) { | |
| console.error("[Connection] Error destroying:", err); | |
| } | |
| } | |
| activeConnections.delete(guildId); | |
| return true; | |
| } | |
| function subscribeToPlayer(connection) { | |
| if (!connection) return; | |
| // Unsubscribe first if already subscribed | |
| if (connection.state.subscription) { | |
| connection.state.subscription.unsubscribe(); | |
| } | |
| connection.subscribe(player); | |
| console.log("[Connection] Subscribed to player"); | |
| // Start playing if not already playing | |
| if (player.state.status === AudioPlayerStatus.Idle) { | |
| playStream(); | |
| } | |
| } | |
| async function connectToVoiceChannel(adapterCreator, guildId, channelId, shouldSubscribe = false) { | |
| return new Promise((resolve, reject) => { | |
| console.log(`[Connection] Connecting to guild ${guildId}, channel ${channelId}, subscribe: ${shouldSubscribe}`); | |
| const connection = joinVoiceChannel({ | |
| guildId, | |
| channelId, | |
| adapterCreator, | |
| selfDeaf: true, | |
| selfMute: false | |
| }); | |
| activeConnections.set(guildId, connection); | |
| connection.on(VoiceConnectionStatus.Signalling, () => { | |
| console.log(`[Connection] Signalling (${guildId})`); | |
| }); | |
| connection.on(VoiceConnectionStatus.Ready, () => { | |
| console.log(`[Connection] Ready (${guildId})`); | |
| resolve(); | |
| }); | |
| connection.once(VoiceConnectionStatus.Destroyed, () => { | |
| console.log(`[Connection] Destroyed (${guildId})`); | |
| if (activeConnections.get(guildId) === connection) { | |
| activeConnections.delete(guildId); | |
| } | |
| }); | |
| if (shouldSubscribe) { | |
| subscribeToPlayer(connection); | |
| } | |
| connection.on(VoiceConnectionStatus.Disconnected, async () => { | |
| console.log(`[Connection] Disconnected (${guildId})`); | |
| if (connection.state?.status === VoiceConnectionStatus.Destroyed) { | |
| console.log("[Connection] Already destroyed, ignoring disconnect"); | |
| return; | |
| } | |
| try { | |
| await Promise.race([ | |
| entersState(connection, VoiceConnectionStatus.Signalling, 5000), | |
| entersState(connection, VoiceConnectionStatus.Connecting, 5000) | |
| ]); | |
| console.log("[Connection] Reconnecting..."); | |
| } catch { | |
| console.log("[Connection] Failed to reconnect, destroying"); | |
| try { | |
| connection.destroy(); | |
| } catch (err) { | |
| console.error("[Connection] Error destroying:", err); | |
| } | |
| } | |
| }); | |
| // Timeout after 10 seconds | |
| setTimeout(() => { | |
| if (connection.state.status !== VoiceConnectionStatus.Ready) { | |
| reject(new Error("Connection timeout")); | |
| } | |
| }, 10000); | |
| }); | |
| } | |
| // ==================== EVENT HANDLERS ==================== | |
| let client = null; | |
| let adapterCreator = null; | |
| // GuildCreate Handler - Populate voice state cache | |
| function onGuildCreate(event) { | |
| const guild = event.data; | |
| if (guild.unavailable) { | |
| console.log(`[GuildCreate] Guild ${guild.id} unavailable, skipping`); | |
| return; | |
| } | |
| console.log(`[GuildCreate] Guild ${guild.id} loaded, populating cache`); | |
| if (guild.voice_states && guild.voice_states.length > 0) { | |
| for (const voiceState of guild.voice_states) { | |
| const userId = voiceState.user_id; | |
| const guildId = guild.id; | |
| if (userId && guildId) { | |
| const fullVoiceState = { | |
| ...voiceState, | |
| member: guild.members?.find((m) => m.user.id === userId), | |
| guild_id: guildId | |
| }; | |
| voiceStateUtils.setSnapshot(guildId, userId, fullVoiceState); | |
| } | |
| } | |
| console.log(`[GuildCreate] Populated ${guild.voice_states.length} voice states for guild ${guild.id}`); | |
| } | |
| } | |
| // VoiceStateUpdate Handler - Auto subscribe/unsubscribe | |
| function onVoiceStateUpdate(event) { | |
| const newState = event.data; | |
| const userId = newState.user_id; | |
| const guildId = newState.guild_id; | |
| // Skip if not our configured guild | |
| if (guildId !== CONFIG.GUILD_ID) return; | |
| // Skip all bots | |
| if (newState.member?.user?.bot) return; | |
| const oldState = voiceStateUtils.peekSnapshot(guildId, userId); | |
| voiceStateUtils.setSnapshot(guildId, userId, newState); | |
| // No channel change | |
| if (oldState?.data.channel_id === newState.channel_id) return; | |
| console.log( | |
| `[VoiceState] User ${userId} changed channels: ${oldState?.data.channel_id || "none"} -> ${newState.channel_id || "none"}` | |
| ); | |
| // Handle leave | |
| if (oldState?.data.channel_id === CONFIG.CHANNEL_ID) { | |
| handleLeave(guildId, oldState.data.channel_id); | |
| } | |
| // Handle join | |
| if (newState.channel_id === CONFIG.CHANNEL_ID) { | |
| handleJoin(guildId, newState.channel_id); | |
| } | |
| } | |
| function handleLeave(guildId, channelId) { | |
| const memberCount = voiceStateUtils.countMembersInChannel(guildId, channelId); | |
| console.log(`[VoiceState] User left, ${memberCount} members remaining in channel`); | |
| if (memberCount === 0) { | |
| const connection = getConnection(guildId); | |
| if (connection?.state.subscription) { | |
| connection.state.subscription.unsubscribe(); | |
| console.log("[VoiceState] Unsubscribed from player - channel empty"); | |
| } | |
| } | |
| } | |
| function handleJoin(guildId, channelId) { | |
| const memberCount = voiceStateUtils.countMembersInChannel(guildId, channelId); | |
| console.log(`[VoiceState] User joined, ${memberCount} members in channel`); | |
| const connection = getConnection(guildId); | |
| if (!connection) { | |
| console.log("[VoiceState] No connection exists, ignoring join"); | |
| return; | |
| } | |
| if (connection.state.status === VoiceConnectionStatus.Destroyed) { | |
| console.log("[VoiceState] Connection destroyed, ignoring join"); | |
| return; | |
| } | |
| if (!connection.state.subscription) { | |
| subscribeToPlayer(connection); | |
| console.log("[VoiceState] Subscribed to player - users present"); | |
| } | |
| } | |
| // Ready Handler - Auto join voice channel | |
| function onReady(event) { | |
| console.log(`[Ready] Logged in as ${event.data.user.username}#${event.data.user.discriminator}`); | |
| console.log(`[Ready] Bot ID: ${event.data.user.id}`); | |
| console.log(`[Ready] Guilds: ${event.data.guilds.length}`); | |
| // Wait a bit for guild data to be populated | |
| setTimeout(async () => { | |
| try { | |
| console.log(`[Ready] Joining voice channel ${CONFIG.CHANNEL_ID} in guild ${CONFIG.GUILD_ID}`); | |
| await connectToVoiceChannel(adapterCreator, CONFIG.GUILD_ID, CONFIG.CHANNEL_ID, false); | |
| console.log("[Ready] Successfully joined voice channel (not subscribed)"); | |
| } catch (error) { | |
| console.error("[Ready] Failed to join voice channel:", error); | |
| } | |
| }, 2000); | |
| } | |
| // ==================== INITIALIZATION ==================== | |
| async function main() { | |
| // Validate configuration | |
| if (CONFIG.DISCORD_TOKEN === "YOUR_DISCORD_TOKEN_HERE") { | |
| console.error("ERROR: Please set DISCORD_TOKEN in the configuration"); | |
| process.exit(1); | |
| } | |
| // Initialize REST client | |
| const rest = new REST({ version: "10" }).setToken(CONFIG.DISCORD_TOKEN); | |
| // Initialize WebSocket Gateway (single shard) | |
| const gateway = new WebSocketManager({ | |
| token: CONFIG.DISCORD_TOKEN, | |
| intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildVoiceStates, | |
| shardCount: 1, | |
| shardIds: [0], | |
| rest | |
| }); | |
| // Create client | |
| client = new Client({ rest, gateway }); | |
| // Create voice adapter | |
| adapterCreator = createVoiceAdapterCreator(gateway, client); | |
| // Register event handlers | |
| client.on(GatewayDispatchEvents.Ready, onReady); | |
| client.on(GatewayDispatchEvents.GuildCreate, onGuildCreate); | |
| client.on(GatewayDispatchEvents.VoiceStateUpdate, onVoiceStateUpdate); | |
| // Handle process termination | |
| process.on("SIGINT", () => { | |
| console.log("\n[Shutdown] Cleaning up..."); | |
| for (const [guildId] of activeConnections) { | |
| destroyConnection(guildId); | |
| } | |
| voiceStateUtils.clear(); | |
| gateway.destroy(); | |
| process.exit(0); | |
| }); | |
| // Connect to Discord | |
| console.log("[Init] Connecting to Discord..."); | |
| await gateway.connect(); | |
| } | |
| // Start the bot | |
| main().catch((error) => { | |
| console.error("[Fatal] Startup error:", error); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment