lavalink-client
Version:
Easy, flexible and feature-rich lavalink@v4 Client. Both for Beginners and Proficients.
621 lines (620 loc) • 30.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LavalinkManager = void 0;
const events_1 = require("events");
const Constants_1 = require("./Constants.js");
const NodeManager_1 = require("./NodeManager.js");
const Player_1 = require("./Player.js");
const Queue_1 = require("./Queue.js");
const Utils_1 = require("./Utils.js");
class LavalinkManager extends events_1.EventEmitter {
/**
* Emit an event
* @param event The event to emit
* @param args The arguments to pass to the event
* @returns
*/
emit(event, ...args) {
return super.emit(event, ...args);
}
/**
* Add an event listener
* @param event The event to listen to
* @param listener The listener to add
* @returns
*/
on(event, listener) {
return super.on(event, listener);
}
/**
* Add an event listener that only fires once
* @param event The event to listen to
* @param listener The listener to add
* @returns
*/
once(event, listener) {
return super.once(event, listener);
}
/**
* Remove an event listener
* @param event The event to remove the listener from
* @param listener The listener to remove
* @returns
*/
off(event, listener) {
return super.off(event, listener);
}
/**
* Remove an event listener
* @param event The event to remove the listener from
* @param listener The listener to remove
* @returns
*/
removeListener(event, listener) {
return super.removeListener(event, listener);
}
/** The Options of LavalinkManager (changeable) */
options;
/** LavalinkManager's NodeManager to manage all Nodes */
nodeManager;
/** LavalinkManager's Utils Class */
utils;
/** Wether the manager was initiated or not */
initiated = false;
/** All Players stored in a MiniMap */
players = new Utils_1.MiniMap();
/**
* Applies the options provided by the User
* @param options
* @returns
*/
applyOptions(options) {
this.options = {
client: {
...(options?.client || {}),
id: options?.client?.id,
username: options?.client?.username ?? "lavalink-client"
},
sendToShard: options?.sendToShard,
nodes: options?.nodes,
playerOptions: {
applyVolumeAsFilter: options?.playerOptions?.applyVolumeAsFilter ?? false,
clientBasedPositionUpdateInterval: options?.playerOptions?.clientBasedPositionUpdateInterval ?? 100,
defaultSearchPlatform: options?.playerOptions?.defaultSearchPlatform ?? "ytsearch",
onDisconnect: {
destroyPlayer: options?.playerOptions?.onDisconnect?.destroyPlayer ?? true,
autoReconnect: options?.playerOptions?.onDisconnect?.autoReconnect ?? false,
autoReconnectOnlyWithTracks: options?.playerOptions?.onDisconnect?.autoReconnectOnlyWithTracks ?? false,
},
onEmptyQueue: {
autoPlayFunction: options?.playerOptions?.onEmptyQueue?.autoPlayFunction ?? null,
destroyAfterMs: options?.playerOptions?.onEmptyQueue?.destroyAfterMs ?? undefined
},
volumeDecrementer: options?.playerOptions?.volumeDecrementer ?? 1,
requesterTransformer: options?.playerOptions?.requesterTransformer ?? null,
useUnresolvedData: options?.playerOptions?.useUnresolvedData ?? false,
minAutoPlayMs: options?.playerOptions?.minAutoPlayMs ?? 10_000,
maxErrorsPerTime: {
threshold: options?.playerOptions?.maxErrorsPerTime?.threshold ?? 35_000,
maxAmount: options?.playerOptions?.maxErrorsPerTime?.maxAmount ?? 3
}
},
linksWhitelist: options?.linksWhitelist ?? [],
linksBlacklist: options?.linksBlacklist ?? [],
linksAllowed: options?.linksAllowed ?? true,
autoSkip: options?.autoSkip ?? true,
autoSkipOnResolveError: options?.autoSkipOnResolveError ?? true,
emitNewSongsOnly: options?.emitNewSongsOnly ?? false,
queueOptions: {
maxPreviousTracks: options?.queueOptions?.maxPreviousTracks ?? 25,
queueChangesWatcher: options?.queueOptions?.queueChangesWatcher ?? null,
queueStore: options?.queueOptions?.queueStore ?? new Queue_1.DefaultQueueStore(),
},
advancedOptions: {
enableDebugEvents: options?.advancedOptions?.enableDebugEvents ?? false,
maxFilterFixDuration: options?.advancedOptions?.maxFilterFixDuration ?? 600_000,
debugOptions: {
logCustomSearches: options?.advancedOptions?.debugOptions?.logCustomSearches ?? false,
noAudio: options?.advancedOptions?.debugOptions?.noAudio ?? false,
playerDestroy: {
dontThrowError: options?.advancedOptions?.debugOptions?.playerDestroy?.dontThrowError ?? false,
debugLog: options?.advancedOptions?.debugOptions?.playerDestroy?.debugLog ?? false,
}
}
}
};
return;
}
/**
* Validates the current manager's options
* @param options
*/
validateOptions(options) {
if (typeof options?.sendToShard !== "function")
throw new SyntaxError("ManagerOption.sendToShard was not provided, which is required!");
// only check in .init()
// if(typeof options?.client !== "object" || typeof options?.client.id !== "string") throw new SyntaxError("ManagerOption.client = { id: string, username?:string } was not provided, which is required");
if (options?.autoSkip && typeof options?.autoSkip !== "boolean")
throw new SyntaxError("ManagerOption.autoSkip must be either false | true aka boolean");
if (options?.autoSkipOnResolveError && typeof options?.autoSkipOnResolveError !== "boolean")
throw new SyntaxError("ManagerOption.autoSkipOnResolveError must be either false | true aka boolean");
if (options?.emitNewSongsOnly && typeof options?.emitNewSongsOnly !== "boolean")
throw new SyntaxError("ManagerOption.emitNewSongsOnly must be either false | true aka boolean");
if (!options?.nodes || !Array.isArray(options?.nodes) || !options?.nodes.every(node => this.utils.isNodeOptions(node)))
throw new SyntaxError("ManagerOption.nodes must be an Array of NodeOptions and is required of at least 1 Node");
/* QUEUE STORE */
if (options?.queueOptions?.queueStore) {
const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(options?.queueOptions?.queueStore));
const requiredKeys = ["get", "set", "stringify", "parse", "delete"];
if (!requiredKeys.every(v => keys.includes(v)) || !requiredKeys.every(v => typeof options?.queueOptions?.queueStore[v] === "function"))
throw new SyntaxError(`The provided ManagerOption.QueueStore, does not have all required functions: ${requiredKeys.join(", ")}`);
}
/* QUEUE WATCHER */
if (options?.queueOptions?.queueChangesWatcher) {
const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(options?.queueOptions?.queueChangesWatcher));
const requiredKeys = ["tracksAdd", "tracksRemoved", "shuffled"];
if (!requiredKeys.every(v => keys.includes(v)) || !requiredKeys.every(v => typeof options?.queueOptions?.queueChangesWatcher[v] === "function"))
throw new SyntaxError(`The provided ManagerOption.DefaultQueueChangesWatcher, does not have all required functions: ${requiredKeys.join(", ")}`);
}
if (typeof options?.queueOptions?.maxPreviousTracks !== "number" || options?.queueOptions?.maxPreviousTracks < 0)
options.queueOptions.maxPreviousTracks = 25;
}
/**
* Create the Lavalink Manager
* @param options
*
* @example
* ```ts
* //const client = new Client({...}); // create your BOT Client (e.g. via discord.js)
* client.lavalink = new LavalinkManager({
* nodes: [
* {
* authorization: "yourverystrongpassword",
* host: "localhost",
* port: 2333,
* id: "testnode"
* },
* sendToShard(guildId, payload) => client.guilds.cache.get(guildId)?.shard?.send(payload),
* client: {
* id: process.env.CLIENT_ID,
* username: "TESTBOT"
* },
* // optional Options:
* autoSkip: true,
* playerOptions: {
* applyVolumeAsFilter: false,
* clientBasedPositionUpdateInterval: 150,
* defaultSearchPlatform: "ytmsearch",
* volumeDecrementer: 0.75,
* //requesterTransformer: YourRequesterTransformerFunction,
* onDisconnect: {
* autoReconnect: true,
* destroyPlayer: false
* },
* onEmptyQueue: {
* destroyAfterMs: 30_000,
* //autoPlayFunction: YourAutoplayFunction,
* },
* useUnresolvedData: true
* },
* queueOptions: {
* maxPreviousTracks: 25,
* //queueStore: yourCustomQueueStoreManagerClass,
* //queueChangesWatcher: yourCustomQueueChangesWatcherClass
* },
* linksBlacklist: [],
* linksWhitelist: [],
* advancedOptions: {
* maxFilterFixDuration: 600_000,
* debugOptions: {
* noAudio: false,
* playerDestroy: {
* dontThrowError: false,
* debugLogs: false
* }
* }
* }
* ]
* })
* ```
*/
constructor(options) {
super();
if (!options)
throw new SyntaxError("No Manager Options Provided");
this.utils = new Utils_1.ManagerUtils(this);
// use the validators
this.applyOptions(options);
this.validateOptions(this.options);
// create classes
this.nodeManager = new NodeManager_1.NodeManager(this);
}
/**
* Get a Player from Lava
* @param guildId The guildId of the player
*
* @example
* ```ts
* const player = client.lavalink.getPlayer(interaction.guildId);
* ```
* A quicker and easier way than doing:
* ```ts
* const player = client.lavalink.players.get(interaction.guildId);
* ```
* @returns
*/
getPlayer(guildId) {
return this.players.get(guildId);
}
/**
* Create a Music-Player. If a player exists, then it returns it before creating a new one
* @param options
* @returns
*
* @example
* ```ts
* const player = client.lavalink.createPlayer({
* guildId: interaction.guildId,
* voiceChannelId: interaction.member.voice.channelId,
* // everything below is optional
* textChannelId: interaction.channelId,
* volume: 100,
* selfDeaf: true,
* selfMute: false,
* instaUpdateFiltersFix: true,
* applyVolumeAsFilter: false
* //only needed if you want to autopick node by region (configured by you)
* // vcRegion: interaction.member.voice.rtcRegion,
* // provide a specific node
* // node: client.lavalink.nodeManager.leastUsedNodes("memory")[0]
* });
* ```
*/
createPlayer(options) {
const oldPlayer = this.getPlayer(options?.guildId);
if (oldPlayer)
return oldPlayer;
const newPlayer = new Player_1.Player(options, this);
this.players.set(newPlayer.guildId, newPlayer);
return newPlayer;
}
/**
* Destroy a player with optional destroy reason and disconnect it from the voice channel
* @param guildId
* @param destroyReason
* @returns
*
* @example
* ```ts
* client.lavalink.destroyPlayer(interaction.guildId, "forcefully destroyed the player");
* // recommend to do it on the player tho: player.destroy("forcefully destroyed the player");
* ```
*/
destroyPlayer(guildId, destroyReason) {
const oldPlayer = this.getPlayer(guildId);
if (!oldPlayer)
return;
return oldPlayer.destroy(destroyReason);
}
/**
* Delete's a player from the cache without destroying it on lavalink (only works when it's disconnected)
* @param guildId
* @returns
*
* @example
* ```ts
* client.lavalink.deletePlayer(interaction.guildId);
* // shouldn't be used except you know what you are doing.
* ```
*/
deletePlayer(guildId) {
const oldPlayer = this.getPlayer(guildId);
if (!oldPlayer)
return;
// oldPlayer.connected is operational. you could also do oldPlayer.voice?.token
if (oldPlayer.voiceChannelId === "string" && oldPlayer.connected && !oldPlayer.get("internal_destroywithoutdisconnect")) {
if (!this.options?.advancedOptions?.debugOptions?.playerDestroy?.dontThrowError)
throw new Error(`Use Player#destroy() not LavalinkManager#deletePlayer() to stop the Player ${JSON.stringify(oldPlayer.toJSON?.())}`);
else if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.PlayerDeleteInsteadOfDestroy, {
state: "warn",
message: "Use Player#destroy() not LavalinkManager#deletePlayer() to stop the Player",
functionLayer: "LavalinkManager > deletePlayer()",
});
}
}
return this.players.delete(guildId);
}
/**
* Checks wether the the lib is useable based on if any node is connected
*
* @example
* ```ts
* if(!client.lavalink.useable) return console.error("can'T search yet, because there is no useable lavalink node.")
* // continue with code e.g. createing a player and searching
* ```
*/
get useable() {
return this.nodeManager.nodes.filter(v => v.connected).size > 0;
}
/**
* Initiates the Manager, creates all nodes and connects all of them
* @param clientData
*
* @example
* ```ts
* // on the bot ready event
* client.on("ready", () => {
* client.lavalink.init({
* id: client.user.id,
* username: client.user.username
* });
* });
* ```
*/
async init(clientData) {
if (this.initiated)
return this;
clientData = clientData ?? {};
this.options.client = { ...(this.options?.client || {}), ...clientData };
if (!this.options?.client.id)
throw new Error('"client.id" is not set. Pass it in Manager#init() or as a option in the constructor.');
if (typeof this.options?.client.id !== "string")
throw new Error('"client.id" set is not type of "string"');
let success = 0;
for (const node of [...this.nodeManager.nodes.values()]) {
try {
await node.connect();
success++;
}
catch (err) {
console.error(err);
this.nodeManager.emit("error", node, err);
}
}
if (success > 0)
this.initiated = true;
else if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.FailedToConnectToNodes, {
state: "error",
message: "Failed to connect to at least 1 Node",
functionLayer: "LavalinkManager > init()",
});
}
return this;
}
/**
* Sends voice data to the Lavalink server.
* ! Without this the library won't work
* @param data
*
* @example
*
* ```ts
* // on the bot "raw" event
* client.on("raw", (d) => {
* // required in order to send audio updates and register channel deletion etc.
* client.lavalink.sendRawData(d)
* })
* ```
*/
async sendRawData(data) {
if (!this.initiated) {
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "log",
message: "Manager is not initated yet",
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, manager is not initated yet");
return;
}
if (!("t" in data)) {
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "error",
message: "No 't' in payload-data of the raw event:",
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no 't' in payload-data of the raw event:", data);
return;
}
// for channel Delete
if ("CHANNEL_DELETE" === data.t) {
const update = "d" in data ? data.d : data;
if (!update.guild_id)
return;
const player = this.getPlayer(update.guild_id);
if (player && player.voiceChannelId === update.id)
return void player.destroy(Constants_1.DestroyReasons.ChannelDeleted);
}
// for voice updates
if (["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(data.t)) {
const update = ("d" in data ? data.d : data);
if (!update) {
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "warn",
message: `No Update data found in payload :: ${JSON.stringify(data, null, 2)}`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no update data found in payload:", data);
return;
}
if (!("token" in update) && !("session_id" in update)) {
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "error",
message: `No 'token' nor 'session_id' found in payload :: ${JSON.stringify(data, null, 2)}`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no 'token' nor 'session_id' found in payload:", data);
return;
}
const player = this.getPlayer(update.guild_id);
if (!player) {
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "warn",
message: `No Lavalink Player found via key: 'guild_id' of update-data :: ${JSON.stringify(update, null, 2)}`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, No Lavalink Player found via key: 'guild_id' of update-data:", update);
return;
}
if (player.get("internal_destroystatus") === true) {
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "warn",
message: `Player is in a destroying state. can't signal the voice states`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, Player is in a destroying state. can't signal the voice states");
return;
}
if ("token" in update) {
if (!player.node?.sessionId)
throw new Error("Lavalink Node is either not ready or not up to date");
const sessionId2Use = player.voice?.sessionId || ("sessionId" in update ? update.sessionId : undefined);
if (!sessionId2Use) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "error",
message: `Can't send updatePlayer for voice token session - Missing sessionId :: ${JSON.stringify({ voice: { token: update.token, endpoint: update.endpoint, sessionId: sessionId2Use, }, update, playerVoice: player.voice }, null, 2)}`,
functionLayer: "LavalinkManager > sendRawData()",
});
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, Sent updatePlayer for voice token session", { voice: { token: update.token, endpoint: update.endpoint, sessionId: sessionId2Use, }, playerVoice: player.voice, update });
}
else {
await player.node.updatePlayer({
guildId: player.guildId,
playerOptions: {
voice: {
token: update.token,
endpoint: update.endpoint,
sessionId: sessionId2Use,
}
}
});
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "log",
message: `Sent updatePlayer for voice token session :: ${JSON.stringify({ voice: { token: update.token, endpoint: update.endpoint, sessionId: sessionId2Use, }, update, playerVoice: player.voice }, null, 2)}`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, Can't send updatePlayer for voice token session - Missing sessionId", { voice: { token: update.token, endpoint: update.endpoint, sessionId: sessionId2Use, } });
}
return;
}
/* voice state update */
if (update.user_id !== this.options?.client.id) {
if (update.user_id && player.voiceChannelId) {
this.emit(update.channel_id === player.voiceChannelId ? "playerVoiceJoin" : "playerVoiceLeave", player, update.user_id);
}
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "warn",
message: `voice update user is not equal to provided client id of the LavalinkManager.options.client.id :: user: "${update.user_id}" manager client id: "${this.options?.client.id}"`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, voice update user is not equal to provided client id of the manageroptions#client#id", "user:", update.user_id, "manager client id:", this.options?.client.id);
return;
}
if (update.channel_id) {
if (player.voiceChannelId !== update.channel_id)
this.emit("playerMove", player, player.voiceChannelId, update.channel_id);
player.voice.sessionId = update.session_id || player.voice.sessionId;
if (!player.voice.sessionId) {
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.NoAudioDebug, {
state: "warn",
message: `Function to assing sessionId provided, but no found in Payload: ${JSON.stringify({ update, playerVoice: player.voice }, null, 2)}`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
console.debug(`Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, Function to assing sessionId provided, but no found in Payload: ${JSON.stringify(update, null, 2)}`);
}
player.voiceChannelId = update.channel_id;
const selfMuteChanged = typeof update.self_mute === "boolean" && player.voiceState.selfMute !== update.self_mute;
const serverMuteChanged = typeof update.mute === "boolean" && player.voiceState.serverMute !== update.mute;
const selfDeafChanged = typeof update.self_deaf === "boolean" && player.voiceState.selfDeaf !== update.self_deaf;
const serverDeafChanged = typeof update.deaf === "boolean" && player.voiceState.serverDeaf !== update.deaf;
const suppressChange = typeof update.suppress === "boolean" && player.voiceState.suppress !== update.suppress;
player.voiceState.selfDeaf = update.self_deaf ?? player.voiceState?.selfDeaf;
player.voiceState.selfMute = update.self_mute ?? player.voiceState?.selfMute;
player.voiceState.serverDeaf = update.deaf ?? player.voiceState?.serverDeaf;
player.voiceState.serverMute = update.mute ?? player.voiceState?.serverMute;
player.voiceState.suppress = update.suppress ?? player.voiceState?.suppress;
if (selfMuteChanged || serverMuteChanged)
this.emit("playerMuteChange", player, player.voiceState.selfMute, player.voiceState.serverMute);
if (selfDeafChanged || serverDeafChanged)
this.emit("playerDeafChange", player, player.voiceState.selfDeaf, player.voiceState.serverDeaf);
if (suppressChange)
this.emit("playerSuppressChange", player, player.voiceState.suppress);
}
else {
const { autoReconnectOnlyWithTracks, destroyPlayer, autoReconnect } = this.options?.playerOptions?.onDisconnect ?? {};
if (destroyPlayer === true) {
return void await player.destroy(Constants_1.DestroyReasons.Disconnected);
}
if (autoReconnect === true) {
try {
const previousPosition = player.position;
const previousPaused = player.paused;
if (this.options?.advancedOptions?.enableDebugEvents) {
this.emit("debug", Constants_1.DebugEvents.PlayerAutoReconnect, {
state: "log",
message: `Auto reconnecting player because LavalinkManager.options.playerOptions.onDisconnect.autoReconnect is true`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
// connect if there are tracks & autoReconnectOnlyWithTracks = true or autoReconnectOnlyWithTracks is false
if (!autoReconnectOnlyWithTracks || (autoReconnectOnlyWithTracks && (player.queue.current || player.queue.tracks.length))) {
await player.connect();
}
// replay the current playing stream
if (player.queue.current) {
return void await player.play({ position: previousPosition, paused: previousPaused, clientTrack: player.queue.current, });
}
// try to play the next track
if (player.queue.tracks.length) {
return void await player.play({ paused: previousPaused });
}
// debug log if nothing was possible
this.emit("debug", Constants_1.DebugEvents.PlayerAutoReconnect, {
state: "log",
message: `Auto reconnected, but nothing to play`,
functionLayer: "LavalinkManager > sendRawData()",
});
}
catch (e) {
console.error(e);
return void await player.destroy(Constants_1.DestroyReasons.PlayerReconnectFail);
}
}
this.emit("playerDisconnect", player, player.voiceChannelId);
player.voiceChannelId = null;
player.voice = Object.assign({});
return;
}
}
}
}
exports.LavalinkManager = LavalinkManager;