riffy-extended
Version:
Riffy is a pro lavalink client. It is designed to be simple and easy to use, with a focus on stability and more features.
317 lines (259 loc) • 12.1 kB
JavaScript
const Websocket = require("ws");
const { Rest } = require("./Rest");
class Node {
/**
* @param {import("./Riffy").Riffy} riffy
* @param {} node
*/
constructor(riffy, node, options) {
this.riffy = riffy
this.name = node.name || node.host;
this.host = node.host || "localhost";
this.port = node.port || 2333;
this.password = node.password || "youshallnotpass";
this.restVersion = options.restVersion;
this.secure = node.secure || false;
this.sessionId = node.sessionId || null;
this.rest = new Rest(riffy, this);
if (options.restVersion === "v4") {
this.wsUrl = `ws${this.secure ? "s" : ""}://${this.host}:${this.port}/v4/websocket`;
} else {
this.wsUrl = `ws${this.secure ? "s" : ""}://${this.host}:${this.port}`;
}
this.restUrl = `http${this.secure ? "s" : ""}://${this.host}:${this.port}`;
this.ws = null;
this.regions = node.regions;
/**
* Lavalink Info fetched While connecting.
* @todo Add Types
* @todo Add This Property Later
*/
//this.info = {};
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.connected = false;
this.resumeKey = options.resumeKey || null;
this.resumeTimeout = options.resumeTimeout || 60;
this.autoResume = options.autoResume || false;
this.reconnectTimeout = options.reconnectTimeout || 5000
this.reconnectTries = options.reconnectTries || 3;
this.reconnectAttempt = null;
this.reconnectAttempted = 1;
this.lastStats = Date.now();
}
connect() {
if (this.ws) this.ws.close();
this.riffy.emit('debug', this.name, `Checking Node Version`);
/**
* @todo Probably to wait until version checks are completed before continuing to connnect to Lavalink.
* @todo Add option to skip the version checks in-case needed.
*/
//(async () => {
// const requestOpts = (version = this.restVersion) => {
// return {
// method: "GET",
// endpoint: `/${version}/info`,
// undefined,
// includeHeaders: true
// };
// }
// await Promise.all([this.rest.makeRequest(...Object.values(requestOpts())), this.rest.makeRequest(...Object.values(requestOpts(this.restVersion == "v3" ? "v4" : "v3")))]).then(([restVersionRequest, flippedRestRequest]) => {
/**
* Lavalink Node's Version that was fetched, checks and uses the succeeded request
* Uses `lavalink-api-version` header if `major` property isn't available/is `0` in the request, it can use either one variable. Defaults to `0` if `lavalink-api-version` isn't available.
*/
// const nodeFetchedVersionObj = Object.assign(
// (
// ("version" in restVersionRequest && restVersionRequest) ||
// flippedRestRequest
// ).version,
// {
// major: !(restVersionRequest?.version || flippedRestRequest?.version)?.major
// ? Number(
// (restVersionRequest || flippedRestRequest).headers.get("lavalink-api-version")
// ) || 0
// : (restVersionRequest?.version || flippedRestRequest?.version)?.major,
// }
// );
/* if(restVersionRequest?.status == 404) this.riffy.emit(
"debug",
`[Node (${this.name}) - Version Check] ${
this.restVersion
} set By User/Defaulted Version Check Failed, attempted ${
this.restVersion == "v3" ? "v4" : "v3"
} For version Checking`
);
if(flippedRestRequest?.status === 404 && restVersionRequest?.status === 404) {
this.riffy.emit("debug", `[Node (${this.name}) - Version Check] Both Version Checks failed, Disconnecting Gracefully & Throwing Error`)
// Disconnect Websocket & Destroy the players(if any created - Just incase)
this.destroy()
throw new Error(`${this.name}(${this.host}) is using unsupported Lavalink Version, Supported Lavalink Versions are v3 and v4.`)
}
if(restVersionRequest?.status !== 404 || flippedRestRequest?.status !== 404) {
this.riffy.emit(
"debug",
`[Node (${this.name}) - Version Check] Check ${restVersionRequest?.status === 404 ? "Un" : ""}successful Lavalink Server uses ${nodeFetchedVersionObj.semver} ${restVersionRequest.status === 404 ? `Doesn't match with restVersion: ${this.restVersion}, Provided in Riffy Options` : ""}`
);
// If defaulted/user-specified fails Graceful Destroy/close the node's connection.
if(restVersionRequest?.status === 404) {
this.riffy.emit("debug", `[Node (${this.name}) - Version Check] Disconnecting Gracefully & Throwing Error`)
// Disconnect Websocket & Destroy the players(if any created - Just incase)
this.destroy()
throw new Error(`${this.name} is specified/defaulted to use ${this.restVersion}, but found using Lavalink version v${nodeFetchedVersionObj.major}, TIP: Set 'restVersion' property to v${nodeFetchedVersionObj.major}`);
}
}
const { headers, ...restVersionRequestWithoutHeaders } = restVersionRequest;
// If `restVersionRequest` isn't failed then update the `info` or set it back to empty Object.
this.info = !("status" in restVersionRequest) ? restVersionRequestWithoutHeaders : {};
})
})() */
const headers = {
"Authorization": this.password,
"User-Id": this.riffy.clientId,
"Client-Name": `Riffy/${this.riffy.version}`,
};
if (this.restVersion === "v4") {
if (this.sessionId) headers["Session-Id"] = this.sessionId;
} else {
if (this.resumeKey) headers["Resume-Key"] = this.resumeKey;
}
this.ws = new Websocket(this.wsUrl, { headers });
this.ws.on("open", this.open.bind(this));
this.ws.on("error", this.error.bind(this));
this.ws.on("message", this.message.bind(this));
this.ws.on("close", this.close.bind(this));
}
open() {
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
this.connected = true;
this.riffy.emit('debug', this.name, `Connection with Lavalink established on ${this.wsUrl}`);
/** @todo Add Version Checking of Node */
if (this.autoResume) {
for (const player of this.riffy.players.values()) {
if (player.node === this) {
player.restart();
}
}
}
}
error(event) {
if (!event) return;
this.riffy.emit("nodeError", this, event);
}
message(msg) {
if (Array.isArray(msg)) msg = Buffer.concat(msg);
else if (msg instanceof ArrayBuffer) msg = Buffer.from(msg);
const payload = JSON.parse(msg.toString());
if (!payload.op) return;
this.riffy.emit("raw", "Node", payload);
this.riffy.emit("debug", this.name, `Lavalink Node Update : ${JSON.stringify(payload)}`);
if (payload.op === "stats") {
this.stats = { ...payload };
this.lastStats = Date.now();
}
if (payload.op === "ready") {
if (this.sessionId !== payload.sessionId) {
this.rest.setSessionId(payload.sessionId);
this.sessionId = payload.sessionId;
}
this.riffy.emit("nodeConnect", this);
this.riffy.emit("debug", this.name, `Ready Payload received ${JSON.stringify(payload)}`);
if (this.restVersion === "v4") {
if (this.sessionId) {
this.rest.makeRequest(`PATCH`, `/${this.rest.version}/sessions/${this.sessionId}`, { resuming: true, timeout: this.resumeTimeout });
this.riffy.emit("debug", this.name, `Resuming configured on Lavalink`);
}
} else {
if (this.resumeKey) {
this.rest.makeRequest(`PATCH`, `/${this.rest.version}/sessions/${this.sessionId}`, { resumingKey: this.resumeKey, timeout: this.resumeTimeout });
this.riffy.emit("debug", this.name, `Resuming configured on Lavalink`);
}
}
}
const player = this.riffy.players.get(payload.guildId);
if (payload.guildId && player) player.emit(payload.op, payload);
}
close(event, reason) {
this.riffy.emit("nodeDisconnect", this, { event, reason });
this.riffy.emit("debug", `Connection with Lavalink closed with Error code : ${event || "Unknown code"}, reason: ${reason || "Unknown reason"}`);
this.connected = false;
this.reconnect();
}
reconnect() {
this.reconnectAttempt = setTimeout(() => {
if (this.reconnectAttempted >= this.reconnectTries) {
const error = new Error(`Unable to connect with ${this.name} node after ${this.reconnectTries} attempts.`);
this.riffy.emit("nodeError", this, error);
return this.destroy();
}
this.ws?.removeAllListeners();
this.ws = null;
this.riffy.emit("nodeReconnect", this);
this.connect();
this.reconnectAttempted++;
}, this.reconnectTimeout);
}
destroy() {
if (!this.connected) return;
this.riffy.players.forEach((player) => {
if (player.node !== this) return;
player.destroy()
});
if (this.ws) this.ws.close(1000, "destroy");
this.ws?.removeAllListeners();
this.ws = null;
clearTimeout(this.reconnectAttempt);
this.riffy.emit("nodeDestroy", this);
this.riffy.nodeMap.delete(this.name);
this.connected = false;
}
disconnect() {
if (!this.connected) return;
this.riffy.players.forEach((player) => { if (player.node == this) { player.move() } });
this.ws.close(1000, "destroy");
this.ws?.removeAllListeners();
this.ws = null;
this.riffy.nodes.delete(this.name);
this.riffy.emit("nodeDisconnect", this);
this.connected = false;
}
get penalties() {
let penalties = 0;
if (!this.connected) return penalties;
if (this.stats.players) {
penalties += this.stats.players;
}
if (this.stats.cpu && this.stats.cpu.systemLoad) {
penalties += Math.round(Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10);
}
if (this.stats.frameStats) {
if (this.stats.frameStats.deficit) {
penalties += this.stats.frameStats.deficit;
}
if (this.stats.frameStats.nulled) {
penalties += this.stats.frameStats.nulled * 2;
}
}
return penalties;
}
}
module.exports = { Node };