UNPKG

lavacord

Version:

A simple by design lavalink wrapper made for any discord library.

403 lines (401 loc) 12.4 kB
import { __name } from '../chunk-PAWJFY3S.mjs'; import { EventEmitter } from 'events'; import { LavalinkNode } from './LavalinkNode.mjs'; import { Player } from './Player.mjs'; var Manager = class extends EventEmitter { static { __name(this, "Manager"); } /** * A Map of Lavalink Nodes indexed by their IDs. */ nodes = /* @__PURE__ */ new Map(); /** * A Map of all active players indexed by guild ID. */ players = /* @__PURE__ */ new Map(); /** * A Map of voice server update states indexed by guild ID. */ voiceServers = /* @__PURE__ */ new Map(); /** * A Map of voice state update states indexed by guild ID. */ voiceStates = /* @__PURE__ */ new Map(); /** * The user ID of the bot this Manager is managing. */ userId = null; /** * Function to send voice state update packets to Discord. * * @remarks * This can be implemented by the user in two ways: * 1. Via constructor options: `new Manager(nodes, { send: fn })` * 2. Via class extension: `class MyManager extends Manager { send(packet) { ... } }` */ _send; /** * The Player class constructor used when creating new players. * * @remarks * Can be overridden in the manager options to use a custom Player implementation. */ Player = Player; /** * A Set of guild IDs that are waiting for a connection. * * @internal */ expecting = /* @__PURE__ */ new Set(); /** * Creates a new Manager instance. * * @param nodes - An array of Lavalink node options to connect to. * @param options - Configuration options for the Manager. * * @example * ```typescript * // Method 1: Define send function via options * const manager = new Manager([ * { * id: "main", * host: "localhost", * port: 2333, * password: "youshallnotpass" * } * ], { * userId: "bot_user_id", * send: (packet) => { * const guild = client.guilds.cache.get(packet.d.guild_id); * if (guild) guild.shard.send(packet); * } * }); * * // Method 2: Extend Manager class * class MyManager extends Manager { * send(packet) { * const guild = this.client.guilds.cache.get(packet.d.guild_id); * if (guild) guild.shard.send(packet); * } * } * const manager = new MyManager(nodes, { userId: "bot_user_id" }); * ``` */ constructor(nodes, options) { super(); options ??= {}; if (options.userId) this.userId = options.userId; if (options.player) this.Player = options.player; if (options.send && !this._send) this._send = options.send; for (const node of nodes) this.createNode(node); } /** * Connects all Lavalink nodes to their respective Lavalink servers. * * @returns A promise that resolves when all connections are established. * * @example * ```typescript * // Connect all nodes * manager.connect() * .then(() => console.log('All nodes connected!')) * .catch(error => console.error('Failed to connect nodes:', error)); * ``` */ async connect() { if (!this.userId) throw new Error( "Lavacord requires a client user ID before connecting. \nSet the user ID when constructing the Manager or after your Discord client is ready." ); return Promise.all(this.nodes.values().map((node) => node.connect().then(() => node))); } /** * Disconnects all players and nodes, effectively cleaning up all resources. * * @returns A promise that resolves when all disconnections are complete. * * @example * ```typescript * // Disconnect everything * manager.disconnect() * .then(() => console.log('All players and nodes cleaned up')) * .catch(error => console.error('Error during disconnection:', error)); * ``` */ async disconnect() { for await (const player of this.players.keys()) await this.leave(player); for (const node of this.nodes.values()) node.destroy(); } /** * Creates a new Lavalink node and adds it to the nodes map. * * @param options - Configuration options for the node. * @returns The newly created LavalinkNode instance. * * @example * ```typescript * // Add a new node * const newNode = manager.createNode({ * id: "node2", * host: "example.com", * port: 2333, * password: "securepassword", * resuming: true * }); * ``` */ createNode(options) { const node = new LavalinkNode(this, options); this.nodes.set(options.id, node); return node; } /** * Disconnects and removes a node from the manager. * * @param id - The ID of the node to remove. * @returns Whether the node was successfully removed. * * @example * ```typescript * // Remove a node * const removed = manager.removeNode("node1"); * if (removed) { * console.log("Node successfully removed"); * } else { * console.log("Node not found"); * } * ``` */ removeNode(id) { const node = this.nodes.get(id); if (!node) return false; node.destroy(); return this.nodes.delete(id); } /** * Joins a voice channel and creates a player for the guild. * * @param data - The data needed to join a voice channel. * @param joinOptions - Options for joining the channel (selfmute, selfdeaf). * @returns A promise resolving to the Player instance. * * @example * ```typescript * // Join a voice channel * const player = await manager.join({ * guild: "123456789012345678", // Guild ID * channel: "123456789012345679", // Voice Channel ID * node: "main" // Node ID * }, { * selfdeaf: true // Join deafened * }); * * // Now you can use the player to play music * const result = await Rest.load(player.node, "https://www.youtube.com/watch?v=dQw4w9WgXcQ"); * await player.play(result.tracks[0].track); * ``` */ async join(data, joinOptions = {}) { const player = this.players.get(data.guild); if (player) return player; await this.sendWS(data.guild, data.channel, joinOptions); return this.spawnPlayer(data); } /** * Leaves a voice channel and cleans up the player. * * @param guild - The ID of the guild to leave. * @returns A promise resolving to whether the operation was successful. * * @example * ```typescript * // Leave a voice channel * const success = await manager.leave("123456789012345678"); * if (success) { * console.log("Successfully left the voice channel"); * } else { * console.log("Not in a voice channel in this guild"); * } * ``` */ async leave(guild) { await this.sendWS(guild, null); const player = this.players.get(guild); if (!player) return false; await player.destroy(); return this.players.delete(guild); } /** * Switches a player from one node to another, implementing fallback capability. * * @param player - The player to move to another node. * @param node - The destination node. * @returns A promise resolving to the updated player. * * @example * ```typescript * // Switch a player to another node (e.g. if current node is failing) * const player = manager.players.get("123456789012345678"); * const newNode = manager.nodes.get("backup-node"); * * if (player && newNode) { * await manager.switch(player, newNode); * console.log("Player switched to backup node"); * } * ``` */ async switch(player, node) { player.node = node; if (!player.voice?.channelId) return player; await player.destroy(); if (!player.track) return player; await player.play(player.track.encoded, { position: player.state.position, volume: player.volume, filters: player.filters, // @ts-expect-error The type narrow for if no channelId didn't work voice: player.voice, paused: player.paused, userData: player.track.userData }); return player; } /** * Processes voice server update events from Discord. * * @param data - The voice server update data from Discord. * @returns A promise resolving to whether a connection was established. * * @example * ```typescript * // In your Discord.js client events * client.ws.on(GatewayDispatchEvents.VoiceServerUpdate, (data) => { * manager.voiceServerUpdate(data); * }); * ``` */ voiceServerUpdate(data) { this.voiceServers.set(data.guild_id, data); this.expecting.add(data.guild_id); return this._attemptConnection(data.guild_id); } /** * Processes voice state update events from Discord. * * @param data - The voice state update data from Discord. * @returns A promise resolving to whether a connection was established. * * @example * ```typescript * // In your Discord.js client events * client.ws.on(GatewayDispatchEvents.VoiceStateUpdate, (data) => { * manager.voiceStateUpdate(data); * }); * ``` */ async voiceStateUpdate(data) { if (data.user_id !== this.userId) return false; if (!data.guild_id) return false; if (data.channel_id) { this.voiceStates.set(data.guild_id, data); return this._attemptConnection(data.guild_id); } this.voiceServers.delete(data.guild_id); this.voiceStates.delete(data.guild_id); return false; } /** * Sends a voice state update packet to Discord. * * @param guild - The ID of the guild. * @param channel - The ID of the voice channel to join, or null to leave. * @param options - Options for joining (selfmute, selfdeaf). * @returns The result from the send function. * * @example * ```typescript * // Join a voice channel * manager.sendWS("123456789012345678", "123456789012345679", { selfdeaf: true }); * * // Leave a voice channel * manager.sendWS("123456789012345678", null); * ``` */ sendWS(guild, channel, { selfmute = false, selfdeaf = false } = {}) { return this.send({ op: 4, d: { guild_id: guild, channel_id: channel, self_mute: selfmute, self_deaf: selfdeaf } }); } /** * Gets all connected nodes, sorted by CPU load. * * @returns An array of connected nodes sorted by CPU load (least to most). * * @example * ```typescript * // Get the node with the least CPU load * const bestNode = manager.idealNodes[0]; * if (bestNode) { * console.log(`Best node for new connections: ${bestNode.id}`); * } * ``` */ get idealNodes() { return Array.from(this.nodes.values()).filter((node) => node.connected).sort((a, b) => { const aload = a.stats.cpu ? a.stats.cpu.systemLoad / a.stats.cpu.cores * 100 : 0; const bload = b.stats.cpu ? b.stats.cpu.systemLoad / b.stats.cpu.cores * 100 : 0; return aload - bload; }); } /** * Attempts to establish a connection for a guild using available voice data. * * @internal * @param guildID - The ID of the guild to attempt connecting. * @returns A promise resolving to whether a connection was established. */ async _attemptConnection(guildID) { const server = this.voiceServers.get(guildID); const state = this.voiceStates.get(guildID); if (!server || !state || !this.expecting.has(guildID)) return false; const player = this.players.get(guildID); if (!player) return false; await player.connect({ sessionId: state.session_id, event: server, channelId: state.channel_id }); this.expecting.delete(guildID); return true; } /** * Creates a new player instance. * * @internal * @param data - The data needed to create a player. * @returns The created Player instance. */ spawnPlayer(data) { const exists = this.players.get(data.guild); if (exists) return exists; const node = this.nodes.get(data.node); if (!node) throw new Error(`INVALID_HOST: No available node with ${data.node}`); const player = new this.Player(node, data.guild); this.players.set(data.guild, player); return player; } send(packet) { if (typeof this._send !== "function") { throw new Error( "Lavacord requires a send function to be defined in the Manager options. This function should send voice state updates to Discord." ); } return this._send(packet); } }; export { Manager }; //# sourceMappingURL=Manager.mjs.map //# sourceMappingURL=Manager.mjs.map