UNPKG

erela.js

Version:

An easy-to-use Lavalink client for NodeJS.

354 lines (353 loc) 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Node = void 0; const tslib_1 = require("tslib"); /* eslint-disable no-case-declarations */ const ws_1 = tslib_1.__importDefault(require("ws")); const undici_1 = require("undici"); const Utils_1 = require("./Utils"); function check(options) { if (!options) throw new TypeError("NodeOptions must not be empty."); if (typeof options.host !== "string" || !/.+/.test(options.host)) throw new TypeError('Node option "host" must be present and be a non-empty string.'); if (typeof options.port !== "undefined" && typeof options.port !== "number") throw new TypeError('Node option "port" must be a number.'); if (typeof options.password !== "undefined" && (typeof options.password !== "string" || !/.+/.test(options.password))) throw new TypeError('Node option "password" must be a non-empty string.'); if (typeof options.secure !== "undefined" && typeof options.secure !== "boolean") throw new TypeError('Node option "secure" must be a boolean.'); if (typeof options.identifier !== "undefined" && typeof options.identifier !== "string") throw new TypeError('Node option "identifier" must be a non-empty string.'); if (typeof options.retryAmount !== "undefined" && typeof options.retryAmount !== "number") throw new TypeError('Node option "retryAmount" must be a number.'); if (typeof options.retryDelay !== "undefined" && typeof options.retryDelay !== "number") throw new TypeError('Node option "retryDelay" must be a number.'); if (typeof options.requestTimeout !== "undefined" && typeof options.requestTimeout !== "number") throw new TypeError('Node option "requestTimeout" must be a number.'); } class Node { options; /** The socket for the node. */ socket = null; /** The HTTP pool used for rest calls. */ http; /** The amount of rest calls the node has made. */ calls = 0; /** The stats for the node. */ stats; manager; static _manager; reconnectTimeout; reconnectAttempts = 1; /** Returns if connected to the Node. */ get connected() { if (!this.socket) return false; return this.socket.readyState === ws_1.default.OPEN; } /** Returns the address for this node. */ get address() { return `${this.options.host}:${this.options.port}`; } /** @hidden */ static init(manager) { this._manager = manager; } /** * Creates an instance of Node. * @param options */ constructor(options) { this.options = options; if (!this.manager) this.manager = Utils_1.Structure.get("Node")._manager; if (!this.manager) throw new RangeError("Manager has not been initiated."); if (this.manager.nodes.has(options.identifier || options.host)) { return this.manager.nodes.get(options.identifier || options.host); } check(options); this.options = { port: 2333, password: "youshallnotpass", secure: false, retryAmount: 5, retryDelay: 30e3, ...options, }; if (this.options.secure) { this.options.port = 443; } this.http = new undici_1.Pool(`http${this.options.secure ? "s" : ""}://${this.address}`, this.options.poolOptions); this.options.identifier = options.identifier || options.host; this.stats = { players: 0, playingPlayers: 0, uptime: 0, memory: { free: 0, used: 0, allocated: 0, reservable: 0, }, cpu: { cores: 0, systemLoad: 0, lavalinkLoad: 0, }, frameStats: { sent: 0, nulled: 0, deficit: 0, }, }; this.manager.nodes.set(this.options.identifier, this); this.manager.emit("nodeCreate", this); } /** Connects to the Node. */ connect() { if (this.connected) return; const headers = { Authorization: this.options.password, "Num-Shards": String(this.manager.options.shards), "User-Id": this.manager.options.clientId, "Client-Name": this.manager.options.clientName, }; this.socket = new ws_1.default(`ws${this.options.secure ? "s" : ""}://${this.address}`, { headers }); this.socket.on("open", this.open.bind(this)); this.socket.on("close", this.close.bind(this)); this.socket.on("message", this.message.bind(this)); this.socket.on("error", this.error.bind(this)); } /** Destroys the Node and all players connected with it. */ destroy() { if (!this.connected) return; const players = this.manager.players.filter(p => p.node == this); if (players.size) players.forEach(p => p.destroy()); this.socket.close(1000, "destroy"); this.socket.removeAllListeners(); this.socket = null; this.reconnectAttempts = 1; clearTimeout(this.reconnectTimeout); this.manager.emit("nodeDestroy", this); this.manager.destroyNode(this.options.identifier); } /** * Makes an API call to the Node * @param endpoint The endpoint that we will make the call to * @param modify Used to modify the request before being sent * @returns The returned data */ async makeRequest(endpoint, modify) { const options = { path: `/${endpoint.replace(/^\//gm, "")}`, method: "GET", headers: { Authorization: this.options.password }, headersTimeout: this.options.requestTimeout, }; modify?.(options); const request = await this.http.request(options); this.calls++; return await request.body.json(); } /** * Sends data to the Node. * @param data */ send(data) { return new Promise((resolve, reject) => { if (!this.connected) return resolve(false); if (!data || !JSON.stringify(data).startsWith("{")) { return reject(false); } this.socket.send(JSON.stringify(data), (error) => { if (error) reject(error); else resolve(true); }); }); } reconnect() { this.reconnectTimeout = setTimeout(() => { if (this.reconnectAttempts >= this.options.retryAmount) { const error = new Error(`Unable to connect after ${this.options.retryAmount} attempts.`); this.manager.emit("nodeError", this, error); return this.destroy(); } this.socket.removeAllListeners(); this.socket = null; this.manager.emit("nodeReconnect", this); this.connect(); this.reconnectAttempts++; }, this.options.retryDelay); } open() { if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout); this.manager.emit("nodeConnect", this); } close(code, reason) { this.manager.emit("nodeDisconnect", this, { code, reason }); if (code !== 1000 || reason !== "destroy") this.reconnect(); } error(error) { if (!error) return; this.manager.emit("nodeError", this, error); } message(d) { if (Array.isArray(d)) d = Buffer.concat(d); else if (d instanceof ArrayBuffer) d = Buffer.from(d); const payload = JSON.parse(d.toString()); if (!payload.op) return; this.manager.emit("nodeRaw", payload); switch (payload.op) { case "stats": delete payload.op; this.stats = { ...payload }; break; case "playerUpdate": const player = this.manager.players.get(payload.guildId); if (player) player.position = payload.state.position || 0; break; case "event": this.handleEvent(payload); break; default: this.manager.emit("nodeError", this, new Error(`Unexpected op "${payload.op}" with data: ${payload}`)); return; } } handleEvent(payload) { if (!payload.guildId) return; const player = this.manager.players.get(payload.guildId); if (!player) return; const track = player.queue.current; const type = payload.type; if (payload.type === "TrackStartEvent") { this.trackStart(player, track, payload); } else if (payload.type === "TrackEndEvent") { this.trackEnd(player, track, payload); } else if (payload.type === "TrackStuckEvent") { this.trackStuck(player, track, payload); } else if (payload.type === "TrackExceptionEvent") { this.trackError(player, track, payload); } else if (payload.type === "WebSocketClosedEvent") { this.socketClosed(player, payload); } else { const error = new Error(`Node#event unknown event '${type}'.`); this.manager.emit("nodeError", this, error); } } trackStart(player, track, payload) { player.playing = true; player.paused = false; this.manager.emit("trackStart", player, track, payload); } trackEnd(player, track, payload) { // If a track had an error while starting if (["LOAD_FAILED", "CLEAN_UP"].includes(payload.reason)) { player.queue.previous = player.queue.current; player.queue.current = player.queue.shift(); if (!player.queue.current) return this.queueEnd(player, track, payload); this.manager.emit("trackEnd", player, track, payload); if (this.manager.options.autoPlay) player.play(); return; } // If a track was forcibly played if (payload.reason === "REPLACED") { this.manager.emit("trackEnd", player, track, payload); return; } // If a track ended and is track repeating if (track && player.trackRepeat) { if (payload.reason === "STOPPED") { player.queue.previous = player.queue.current; player.queue.current = player.queue.shift(); } if (!player.queue.current) return this.queueEnd(player, track, payload); this.manager.emit("trackEnd", player, track, payload); if (this.manager.options.autoPlay) player.play(); return; } // If a track ended and is track repeating if (track && player.queueRepeat) { player.queue.previous = player.queue.current; if (payload.reason === "STOPPED") { player.queue.current = player.queue.shift(); if (!player.queue.current) return this.queueEnd(player, track, payload); } else { player.queue.add(player.queue.current); player.queue.current = player.queue.shift(); } this.manager.emit("trackEnd", player, track, payload); if (this.manager.options.autoPlay) player.play(); return; } // If there is another song in the queue if (player.queue.length) { player.queue.previous = player.queue.current; player.queue.current = player.queue.shift(); this.manager.emit("trackEnd", player, track, payload); if (this.manager.options.autoPlay) player.play(); return; } // If there are no songs in the queue if (!player.queue.length) return this.queueEnd(player, track, payload); } queueEnd(player, track, payload) { player.queue.current = null; player.playing = false; this.manager.emit("queueEnd", player, track, payload); } trackStuck(player, track, payload) { player.stop(); this.manager.emit("trackStuck", player, track, payload); } trackError(player, track, payload) { player.stop(); this.manager.emit("trackError", player, track, payload); } socketClosed(player, payload) { this.manager.emit("socketClosed", player, payload); } } exports.Node = Node;