const { EventEmitter } = require('events');
const { Node } = require('./Node');
/**
* Represents a Tsumi instance.
* @extends EventEmitter
* @class
*/
class TsumiInstance extends EventEmitter {
/**
* @param {Object} options
* @param {string} options.botId - The bot ID
* @param {Function} options.sendPayload - The function to send payloads
* @param {string} options.userAgent - The user agent to use
* @return {Object} - An event emitter for listening
*/
constructor(options) {
super();
if (!options?.botId || !options?.sendPayload)
throw new Error('Bot ID or sendPayload is required');
/**
* This Instance's option
* @type {Object}
*/
this.options = options;
/**
* This bot's ID
* @type {String}
*/
this.botId = options.botId;
/**
* This Instance's user agent
* @type {String}
*/
this.userAgent = options?.userAgent || 'Tsumi/0.0.18';
/**
* The nodes that are connected
* @type {Object}
* @default {}
*/
this.Nodes = {};
}
/**
* Purge all nodes
* @function
* @return {Boolean} - True if successful
*/
purge = () => {
this.Nodes = {};
return true;
};
/**
* Add a node to the instance
* @function
* @async
* @param {Object} node - The node to add
* @return {Object} - The node that was added
*/
addNode = async (node) => {
if (!node.host || !node.port || !node.pass)
throw new Error('Host, port, and pass are required');
const newNode = new Node({
serverName: node.serverName,
secure: node.secure,
host: node.host,
port: node.port,
pass: node.pass,
userAgent: this.userAgent,
botId: this.botId,
sendPayload: this.options.sendPayload,
});
if (!(await newNode.startWs())) throw new Error('Failed to connect to node', newNode);
this.emit('nodeAdded', newNode);
this.Nodes = { ...this.Nodes, [Object.keys(this.Nodes).length]: newNode };
if (Object.keys(this.Nodes).length === 1) this.emit('ready');
return newNode;
};
/**
* Get the ideal node
* @function
* @return {Object} The ideal node
*/
getIdealNode = () => {
return this.Nodes[sortNodesBySystemLoad(this.Nodes)];
};
/**
* Handling raw events for players
* @function
* @param {Object} data - The data to handle
*/
handleRaw = (data) => {
switch (data.t) {
case 'VOICE_SERVER_UPDATE': {
let player = findValue(this.Nodes, data.d.guild_id);
if (!player?.connectionInfo) break;
player.connectionInfo.token = data.d.token;
player.connectionInfo.endpoint = data.d.endpoint;
if (player.connectionInfo.sessionId) player.connect();
break;
}
case 'VOICE_STATE_UPDATE': {
let player = findValue(this.Nodes, data.d.guild_id);
if (!player?.connectionInfo) break;
if (data.d.member.user.id !== player.node.botId) break;
if (data.d.channel_id === null) return (player.connectionInfo = {});
player.connectionInfo.sessionId = data.d.session_id;
if (player.connectionInfo.token) player.connect();
break;
}
}
};
}
/**
* Handle finding values in an object
* @function
* @param {Object} obj - The object to search
* @param {string} searchKey - The key to search for
* @return {Object} - The value of the key
*/
function findValue(obj, searchKey) {
for (let key in obj) {
if (obj[key].players && obj[key].players[searchKey]) {
return obj[key].players[searchKey];
}
}
return null;
}
/**
* Sort nodes by system load
* @function
* @param {Object} nodes - The nodes to sort
* @return {Object} - Nodes sorted by system load
*/
function sortNodesBySystemLoad(nodes) {
let sortedNodes = Object.entries(nodes).sort(
(a, b) => a[1].stats.cpu.systemLoad - b[1].stats.cpu.systemLoad
);
return sortedNodes.map((node) => node[0]);
}
module.exports = { TsumiInstance };