diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 007d4cad..00000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -docs/ -dist/ -node_modules/ \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 762adf8a..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "extends": "@augu", - "rules": { - "indent": ["error", 4, { "SwitchCase": 1 }], - "quotes": ["error", "single"], - "brace-style": ["error", "1tbs"], - "array-bracket-spacing": ["error", "always"], - "block-spacing": ["error", "always"], - "arrow-spacing": "error", - "switch-colon-spacing": ["error", {"after": true, "before": false}], - "camelcase": "off", - "require-await": "error" - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7f83c41a..4d80b4e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .idea .gitattributes .DS_Store -.vscode node_modules/ dist/ package-lock.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..8d047dad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.insertSpaces": false +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..2950cfb9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,4 @@ +// @ts-check +import config from '@shipgirl/eslint-config'; + +export default [ ...config(import.meta.dirname) ]; diff --git a/package.json b/package.json index ae019729..132e5658 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build": "npm run build:ts && npm run build:docs", "build:ts": "tsup --config tsup-config.json", "build:docs": "typedoc --theme default --readme README.md --out docs/ --entryPointStrategy expand src/.", - "lint": "eslint --fix --ext .ts", + "lint": "eslint .", "prepare": "npm run build:ts" }, "keywords": [ @@ -44,15 +44,20 @@ "ws": "^8.18.0" }, "devDependencies": { - "@augu/eslint-config": "4.0.1", - "@types/node": "^20.14.11", + "@augu/eslint-config": "5.2.4", + "@eslint/js": "^9.9.0", + "@shipgirl/eslint-config": "^0.2.0", + "@stylistic/eslint-plugin": "^2.6.2", + "@types/eslint__js": "^8.42.3", + "@types/node": "^22.2.0", "@types/node-fetch": "^2.6.11", - "@types/ws": "^8.5.11", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", - "eslint": "^8.56.0", - "tsup": "^8.2.0", - "typedoc": "^0.26.4", - "typescript": "^5.5.3" + "@types/ws": "^8.5.12", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", + "eslint": "^9.9.0", + "tsup": "^8.2.4", + "typedoc": "^0.26.5", + "typescript": "^5.5.4", + "typescript-eslint": "^8.0.1" } } diff --git a/src/Constants.ts b/src/Constants.ts index 53467ea2..d4f72afd 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -2,56 +2,56 @@ import Info from '../package.json'; import { NodeOption, ShoukakuOptions } from './Shoukaku'; export enum State { - CONNECTING, - NEARLY, - CONNECTED, - RECONNECTING, - DISCONNECTING, - DISCONNECTED + CONNECTING, + NEARLY, + CONNECTED, + RECONNECTING, + DISCONNECTING, + DISCONNECTED } export enum VoiceState { - SESSION_READY, - SESSION_ID_MISSING, - SESSION_ENDPOINT_MISSING, - SESSION_FAILED_UPDATE + SESSION_READY, + SESSION_ID_MISSING, + SESSION_ENDPOINT_MISSING, + SESSION_FAILED_UPDATE } export enum OpCodes { - PLAYER_UPDATE = 'playerUpdate', - STATS = 'stats', - EVENT = 'event', - READY = 'ready' + PLAYER_UPDATE = 'playerUpdate', + STATS = 'stats', + EVENT = 'event', + READY = 'ready' } -export enum Versions { - REST_VERSION = 4, - WEBSOCKET_VERSION = 4 -} +export const Versions = { + REST_VERSION: 4, + WEBSOCKET_VERSION: 4 +}; export const ShoukakuDefaults: Required = { - resume: false, - resumeTimeout: 30, - resumeByLibrary: false, - reconnectTries: 3, - reconnectInterval: 5, - restTimeout: 60, - moveOnDisconnect: false, - userAgent: 'Discord Bot/unknown (https://github.com/shipgirlproject/Shoukaku.git)', - structures: {}, - voiceConnectionTimeout: 15, - nodeResolver: (nodes) => [ ...nodes.values() ] - .filter(node => node.state === State.CONNECTED) - .sort((a, b) => a.penalties - b.penalties) - .shift() + resume: false, + resumeTimeout: 30, + resumeByLibrary: false, + reconnectTries: 3, + reconnectInterval: 5, + restTimeout: 60, + moveOnDisconnect: false, + userAgent: 'Discord Bot/unknown (https://github.com/shipgirlproject/Shoukaku.git)', + structures: {}, + voiceConnectionTimeout: 15, + nodeResolver: (nodes) => [ ...nodes.values() ] + .filter(node => node.state === State.CONNECTED) + .sort((a, b) => a.penalties - b.penalties) + .shift() }; export const ShoukakuClientInfo = `${Info.name}/${Info.version} (${Info.repository.url})`; export const NodeDefaults: NodeOption = { - name: 'Default', - url: '', - auth: '', - secure: false, - group: undefined + name: 'Default', + url: '', + auth: '', + secure: false, + group: undefined }; diff --git a/src/Shoukaku.ts b/src/Shoukaku.ts index 5b363ca5..ca422fc6 100644 --- a/src/Shoukaku.ts +++ b/src/Shoukaku.ts @@ -5,171 +5,171 @@ import { Connector } from './connectors/Connector'; import { Constructor, mergeDefault } from './Utils'; import { Player } from './guild/Player'; import { Rest } from './node/Rest'; -import { Connection } from './guild/Connection.js'; +import { Connection } from './guild/Connection'; export interface Structures { - /** + /** * A custom structure that extends the Rest class */ - rest?: Constructor; - /** + rest?: Constructor; + /** * A custom structure that extends the Player class */ - player?: Constructor; + player?: Constructor; } export interface NodeOption { - /** + /** * Name of the Lavalink node */ - name: string; - /** + name: string; + /** * Lavalink node host and port without any prefix */ - url: string; - /** + url: string; + /** * Credentials to access Lavalink */ - auth: string; - /** + auth: string; + /** * Whether to use secure protocols or not */ - secure?: boolean; - /** + secure?: boolean; + /** * Name of the Lavalink node group */ - group?: string; + group?: string; } export interface ShoukakuOptions { - /** + /** * Whether to resume a connection on disconnect to Lavalink (Server Side) (Note: DOES NOT RESUME WHEN THE LAVALINK SERVER DIES) */ - resume?: boolean; - /** + resume?: boolean; + /** * Time to wait before lavalink starts to destroy the players of the disconnected client */ - resumeTimeout?: number; - /** + resumeTimeout?: number; + /** * Whether to resume the players by doing it in the library side (Client Side) (Note: TRIES TO RESUME REGARDLESS OF WHAT HAPPENED ON A LAVALINK SERVER) */ - resumeByLibrary?: boolean; - /** + resumeByLibrary?: boolean; + /** * Number of times to try and reconnect to Lavalink before giving up */ - reconnectTries?: number; - /** + reconnectTries?: number; + /** * Timeout before trying to reconnect */ - reconnectInterval?: number; - /** + reconnectInterval?: number; + /** * Time to wait for a response from the Lavalink REST API before giving up */ - restTimeout?: number; - /** + restTimeout?: number; + /** * Whether to move players to a different Lavalink node when a node disconnects */ - moveOnDisconnect?: boolean; - /** + moveOnDisconnect?: boolean; + /** * User Agent to use when making requests to Lavalink */ - userAgent?: string; - /** + userAgent?: string; + /** * Custom structures for shoukaku to use */ - structures?: Structures; - /** + structures?: Structures; + /** * Timeout before abort connection */ - voiceConnectionTimeout?: number; - /** + voiceConnectionTimeout?: number; + /** * Node Resolver to use if you want to customize it */ - nodeResolver?: (nodes: Map, connection?: Connection) => Node|undefined; + nodeResolver?: (nodes: Map, connection?: Connection) => Node | undefined; } export interface VoiceChannelOptions { - guildId: string; - shardId: number; - channelId: string; - deaf?: boolean; - mute?: boolean; + guildId: string; + shardId: number; + channelId: string; + deaf?: boolean; + mute?: boolean; } export interface ShoukakuEvents { - /** + /** * Emitted when reconnect tries are occurring and how many tries are left * @eventProperty */ - 'reconnecting': [name: string, reconnectsLeft: number, reconnectInterval: number]; - /** + 'reconnecting': [name: string, reconnectsLeft: number, reconnectInterval: number]; + /** * Emitted when data useful for debugging is produced * @eventProperty */ - 'debug': [name: string, info: string]; - /** + 'debug': [name: string, info: string]; + /** * Emitted when an error occurs * @eventProperty */ - 'error': [name: string, error: Error]; - /** + 'error': [name: string, error: Error]; + /** * Emitted when Shoukaku is ready to receive operations * @eventProperty */ - 'ready': [name: string, reconnected: boolean]; - /** + 'ready': [name: string, reconnected: boolean]; + /** * Emitted when a websocket connection to Lavalink closes * @eventProperty */ - 'close': [name: string, code: number, reason: string]; - /** + 'close': [name: string, code: number, reason: string]; + /** * Emitted when a websocket connection to Lavalink disconnects * @eventProperty */ - 'disconnect': [name: string, count: number]; - /** + 'disconnect': [name: string, count: number]; + /** * Emitted when a raw message is received from Lavalink * @eventProperty */ - 'raw': [name: string, json: unknown]; + 'raw': [name: string, json: unknown]; } -export declare interface Shoukaku { - on(event: K, listener: (...args: ShoukakuEvents[K]) => void): this; - once(event: K, listener: (...args: ShoukakuEvents[K]) => void): this; - off(event: K, listener: (...args: ShoukakuEvents[K]) => void): this; - emit(event: string | symbol, ...args: any[]): boolean; +export declare interface IShoukaku { + on(event: K, listener: (...args: ShoukakuEvents[K]) => void): this; + once(event: K, listener: (...args: ShoukakuEvents[K]) => void): this; + off(event: K, listener: (...args: ShoukakuEvents[K]) => void): this; + emit(event: string | symbol, ...args: unknown[]): boolean; } /** * Main Shoukaku class */ -export class Shoukaku extends EventEmitter { - /** +export class Shoukaku extends EventEmitter implements IShoukaku { + /** * Discord library connector */ - public readonly connector: Connector; - /** + public readonly connector: Connector; + /** * Shoukaku options */ - public readonly options: Required; - /** + public readonly options: Required; + /** * Connected Lavalink nodes */ - public readonly nodes: Map; - /** + public readonly nodes: Map; + /** * Voice connections being handled */ - public readonly connections: Map; - /** + public readonly connections: Map; + /** * Players being handled */ - public readonly players: Map; - /** + public readonly players: Map; + /** * Shoukaku instance identifier */ - public id: string|null; - /** + public id: string | null; + /** * @param connector A Discord library connector * @param nodes An array that conforms to the NodeOption type that specifies nodes to connect to * @param options Options to pass to create this Shoukaku instance @@ -184,27 +184,27 @@ export class Shoukaku extends EventEmitter { * @param options.structures Custom structures for shoukaku to use * @param options.nodeResolver Used if you have custom lavalink node resolving */ - constructor(connector: Connector, nodes: NodeOption[], options: ShoukakuOptions = {}) { - super(); - this.connector = connector.set(this); - this.options = mergeDefault(ShoukakuDefaults, options); - this.nodes = new Map(); - this.connections = new Map(); - this.players = new Map(); - this.id = null; - this.connector.listen(nodes); - } + constructor(connector: Connector, nodes: NodeOption[], options: ShoukakuOptions = {}) { + super(); + this.connector = connector.set(this); + this.options = mergeDefault(ShoukakuDefaults, options); + this.nodes = new Map(); + this.connections = new Map(); + this.players = new Map(); + this.id = null; + this.connector.listen(nodes); + } - /** + /** * Gets an ideal node based on the nodeResolver you provided * @param connection Optional connection class for ideal node selection, if you use it * @returns An ideal node for you to do things with */ - public getIdealNode(connection?: Connection): Node | undefined { - return this.options.nodeResolver(this.nodes, connection); - } + public getIdealNode(connection?: Connection): Node | undefined { + return this.options.nodeResolver(this.nodes, connection); + } - /** + /** * Add a Lavalink node to the pool of available nodes * @param options.name Name of this node * @param options.url URL of Lavalink @@ -212,31 +212,31 @@ export class Shoukaku extends EventEmitter { * @param options.secure Whether to use secure protocols or not * @param options.group Group of this node */ - public addNode(options: NodeOption): void { - const node = new Node(this, options); - node.on('debug', (...args) => this.emit('debug', node.name, ...args)); - node.on('reconnecting', (...args) => this.emit('reconnecting', node.name, ...args)); - node.on('error', (...args) => this.emit('error', node.name, ...args)); - node.on('close', (...args) => this.emit('close', node.name, ...args)); - node.on('ready', (...args) => this.emit('ready', node.name, ...args)); - node.on('raw', (...args) => this.emit('raw', node.name, ...args)); - node.once('disconnect', (...args) => this.clean(node, ...args)); - node.connect(); - this.nodes.set(node.name, node); - } + public addNode(options: NodeOption): void { + const node = new Node(this, options); + node.on('debug', (...args: unknown[]) => this.emit('debug', node.name, ...args)); + node.on('reconnecting', (...args: unknown[]) => this.emit('reconnecting', node.name, ...args)); + node.on('error', (...args: unknown[]) => this.emit('error', node.name, ...args)); + node.on('close', (...args: unknown[]) => this.emit('close', node.name, ...args)); + node.on('ready', (...args: unknown[]) => this.emit('ready', node.name, ...args)); + node.on('raw', (...args: unknown[]) => this.emit('raw', node.name, ...args)); + node.once('disconnect', (...args: unknown[]) => this.clean(node, ...args)); + node.connect(); + this.nodes.set(node.name, node); + } - /** + /** * Remove a Lavalink node from the pool of available nodes * @param name Name of the node * @param reason Reason of removing the node */ - public removeNode(name: string, reason = 'Remove node executed'): void { - const node = this.nodes.get(name); - if (!node) throw new Error('The node name you specified doesn\'t exist'); - node.disconnect(1000, reason); - } + public removeNode(name: string, reason = 'Remove node executed'): void { + const node = this.nodes.get(name); + if (!node) throw new Error('The node name you specified doesn\'t exist'); + node.disconnect(1000, reason); + } - /** + /** * Joins a voice channel * @param options.guildId GuildId in which the ChannelId of the voice channel is located * @param options.shardId ShardId to track where this should send on sharded websockets, put 0 if you are unsharded @@ -245,68 +245,68 @@ export class Shoukaku extends EventEmitter { * @param options.mute Optional boolean value to specify whether to mute or unmute the current bot user * @returns The created player */ - public async joinVoiceChannel(options: VoiceChannelOptions): Promise { - if (this.connections.has(options.guildId)) - throw new Error('This guild already have an existing connection'); - const connection = new Connection(this, options); - this.connections.set(connection.guildId, connection); - try { - await connection.connect(); - } catch (error) { - this.connections.delete(options.guildId); - throw error; - } - try { - const node = this.getIdealNode(connection); - if (!node) - throw new Error('Can\'t find any nodes to connect on'); - const player = this.options.structures.player ? new this.options.structures.player(connection.guildId, node) : new Player(connection.guildId, node); - const onUpdate = (state: VoiceState) => { - if (state !== VoiceState.SESSION_READY) return; - player.sendServerUpdate(connection); - }; - await player.sendServerUpdate(connection); - connection.on('connectionUpdate', onUpdate); - this.players.set(player.guildId, player); - return player; - } catch (error) { - connection.disconnect(); - this.connections.delete(options.guildId); - throw error; - } - } + public async joinVoiceChannel(options: VoiceChannelOptions): Promise { + if (this.connections.has(options.guildId)) + throw new Error('This guild already have an existing connection'); + const connection = new Connection(this, options); + this.connections.set(connection.guildId, connection); + try { + await connection.connect(); + } catch (error) { + this.connections.delete(options.guildId); + throw error; + } + try { + const node = this.getIdealNode(connection); + if (!node) + throw new Error('Can\'t find any nodes to connect on'); + const player = this.options.structures.player ? new this.options.structures.player(connection.guildId, node) : new Player(connection.guildId, node); + const onUpdate = (state: VoiceState) => { + if (state !== VoiceState.SESSION_READY) return; + void player.sendServerUpdate(connection); + }; + await player.sendServerUpdate(connection); + connection.on('connectionUpdate', onUpdate); + this.players.set(player.guildId, player); + return player; + } catch (error) { + connection.disconnect(); + this.connections.delete(options.guildId); + throw error; + } + } - /** + /** * Leaves a voice channel * @param guildId The id of the guild you want to delete * @returns The destroyed / disconnected player or undefined if none */ - public async leaveVoiceChannel(guildId: string): Promise { - const connection = this.connections.get(guildId); - if (connection) { - connection.disconnect(); - this.connections.delete(guildId); - } - const player = this.players.get(guildId); - if (player) { - try { - await player.destroy(); - } catch (_) { /* empty */ } - player.clean(); - this.players.delete(guildId); - } - } + public async leaveVoiceChannel(guildId: string): Promise { + const connection = this.connections.get(guildId); + if (connection) { + connection.disconnect(); + this.connections.delete(guildId); + } + const player = this.players.get(guildId); + if (player) { + try { + await player.destroy(); + } catch { /* empty */ } + player.clean(); + this.players.delete(guildId); + } + } - /** + /** * Cleans the disconnected lavalink node * @param node The node to clean * @param args Additional arguments for Shoukaku to emit * @returns A Lavalink node or undefined * @internal */ - private clean(node: Node, ...args: unknown[]): void { - node.removeAllListeners(); - this.nodes.delete(node.name); - this.emit('disconnect', node.name, ...args); - } + private clean(node: Node, ...args: unknown[]): void { + node.removeAllListeners(); + this.nodes.delete(node.name); + this.emit('disconnect', node.name, ...args); + } } diff --git a/src/Utils.ts b/src/Utils.ts index 26d31c58..22880589 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,4 +1,4 @@ -export type Constructor = new (...args: any[]) => T; +export type Constructor = new (...args: unknown[]) => T; /** * Merge the default options to user input @@ -6,20 +6,21 @@ export type Constructor = new (...args: any[]) => T; * @param given User input * @returns Merged options */ -export function mergeDefault(def: T, given: T): Required { - if (!given) return def as Required; - const defaultKeys: (keyof T)[] = Object.keys(def); - for (const key in given) { - if (defaultKeys.includes(key)) continue; - delete given[key]; - } - for (const key of defaultKeys) { - if (def[key] === null || (typeof def[key] === 'string' && def[key].length === 0)) { - if (!given[key]) throw new Error(`${String(key)} was not found from the given options.`); - } - if (given[key] === null || given[key] === undefined) given[key] = def[key]; - } - return given as Required; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function mergeDefault>(def: T, given: T): Required { + if (!given) return def as Required; + const defaultKeys: (keyof T)[] = Object.keys(def); + for (const key in given) { + if (defaultKeys.includes(key)) continue; + delete given[key]; + } + for (const key of defaultKeys) { + if (def[key] === null || (typeof def[key] === 'string' && def[key].length === 0)) { + if (!given[key]) throw new Error(`${String(key)} was not found from the given options.`); + } + if (given[key] === null || given[key] === undefined) given[key] = def[key]; + } + return given as Required; } /** @@ -28,5 +29,5 @@ export function mergeDefault(def: T, given: T) * @returns A promise that resolves in x seconds */ export function wait(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/src/connectors/Connector.ts b/src/connectors/Connector.ts index f3437e08..2d325bb7 100644 --- a/src/connectors/Connector.ts +++ b/src/connectors/Connector.ts @@ -1,46 +1,49 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */ import { NodeOption, Shoukaku } from '../Shoukaku'; import { NodeDefaults } from '../Constants'; import { mergeDefault } from '../Utils'; +import { ServerUpdate, StateUpdatePartial } from '../guild/Connection'; export interface ConnectorMethods { - sendPacket: any; - getId: any; + sendPacket: any; + getId: any; } export const AllowedPackets = [ 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE' ]; export abstract class Connector { - protected readonly client: any; - protected manager: Shoukaku|null; - constructor(client: any) { - this.client = client; - this.manager = null; - } - - public set(manager: Shoukaku): Connector { - this.manager = manager; - return this; - } - - protected ready(nodes: NodeOption[]): void { - this.manager!.id = this.getId(); - for (const node of nodes) this.manager!.addNode(mergeDefault(NodeDefaults, node)); - } - - protected raw(packet: any): void { - if (!AllowedPackets.includes(packet.t)) return; - const guildId = packet.d.guild_id; - const connection = this.manager!.connections.get(guildId); - if (!connection) return; - if (packet.t === 'VOICE_SERVER_UPDATE') return connection.setServerUpdate(packet.d); - const userId = packet.d.user_id; - if (userId !== this.manager!.id) return; - connection.setStateUpdate(packet.d); - } - - abstract getId(): string; - - abstract sendPacket(shardId: number, payload: any, important: boolean): void; - - abstract listen(nodes: NodeOption[]): void; + protected readonly client: any; + protected manager: Shoukaku | null; + constructor(client: any) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.client = client; + this.manager = null; + } + + public set(manager: Shoukaku): Connector { + this.manager = manager; + return this; + } + + protected ready(nodes: NodeOption[]): void { + this.manager!.id = this.getId(); + for (const node of nodes) this.manager!.addNode(mergeDefault(NodeDefaults, node)); + } + + protected raw(packet: any): void { + if (!AllowedPackets.includes(packet.t as string)) return; + const guildId = packet.d.guild_id as string; + const connection = this.manager!.connections.get(guildId); + if (!connection) return; + if (packet.t === 'VOICE_SERVER_UPDATE') return connection.setServerUpdate(packet.d as ServerUpdate); + const userId = packet.d.user_id as string; + if (userId !== this.manager!.id) return; + connection.setStateUpdate(packet.d as StateUpdatePartial); + } + + abstract getId(): string; + + abstract sendPacket(shardId: number, payload: unknown, important: boolean): void; + + abstract listen(nodes: NodeOption[]): void; } diff --git a/src/connectors/libs/DiscordJS.ts b/src/connectors/libs/DiscordJS.ts index 49544cd9..2d28aa83 100644 --- a/src/connectors/libs/DiscordJS.ts +++ b/src/connectors/libs/DiscordJS.ts @@ -1,20 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any */ import { Connector } from '../Connector'; import { NodeOption } from '../../Shoukaku'; export class DiscordJS extends Connector { - // sendPacket is where your library send packets to Discord Gateway - public sendPacket(shardId: number, payload: any, important: boolean): void { - return this.client.ws.shards.get(shardId)?.send(payload, important); - } - // getId is a getter where the lib stores the client user (the one logged in as a bot) id - public getId(): string { - return this.client.user.id; - } - // Listen attaches the event listener to the library you are using - public listen(nodes: NodeOption[]): void { - // Only attach to ready event once, refer to your library for its ready event - this.client.once('ready', () => this.ready(nodes)); - // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) - this.client.on('raw', (packet: any) => this.raw(packet)); - } + // sendPacket is where your library send packets to Discord Gateway + public sendPacket(shardId: number, payload: any, important: boolean): void { + return this.client.ws.shards.get(shardId)?.send(payload, important); + } + // getId is a getter where the lib stores the client user (the one logged in as a bot) id + public getId(): string { + return this.client.user.id; + } + // Listen attaches the event listener to the library you are using + public listen(nodes: NodeOption[]): void { + // Only attach to ready event once, refer to your library for its ready event + this.client.once('ready', () => this.ready(nodes)); + // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) + this.client.on('raw', (packet: any) => this.raw(packet)); + } } diff --git a/src/connectors/libs/Eris.ts b/src/connectors/libs/Eris.ts index 17e06edc..a57633fa 100644 --- a/src/connectors/libs/Eris.ts +++ b/src/connectors/libs/Eris.ts @@ -1,20 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any */ import { Connector } from '../Connector'; import { NodeOption } from '../../Shoukaku'; export class Eris extends Connector { - // sendPacket is where your library send packets to Discord Gateway - public sendPacket(shardId: number, payload: any, important: boolean): void { - return this.client.shards.get(shardId)?.sendWS(payload.op, payload.d, important); - } - // getId is a getter where the lib stores the client user (the one logged in as a bot) id - public getId(): string { - return this.client.user.id; - } - // Listen attaches the event listener to the library you are using - public listen(nodes: NodeOption[]): void { - // Only attach to ready event once, refer to your library for its ready event - this.client.once('ready', () => this.ready(nodes)); - // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) - this.client.on('rawWS', (packet: any) => this.raw(packet)); - } + // sendPacket is where your library send packets to Discord Gateway + public sendPacket(shardId: number, payload: any, important: boolean): void { + return this.client.shards.get(shardId)?.sendWS(payload.op, payload.d, important); + } + // getId is a getter where the lib stores the client user (the one logged in as a bot) id + public getId(): string { + return this.client.user.id; + } + // Listen attaches the event listener to the library you are using + public listen(nodes: NodeOption[]): void { + // Only attach to ready event once, refer to your library for its ready event + this.client.once('ready', () => this.ready(nodes)); + // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) + this.client.on('rawWS', (packet: any) => this.raw(packet)); + } } diff --git a/src/connectors/libs/OceanicJS.ts b/src/connectors/libs/OceanicJS.ts index 0379df5c..bea3206a 100644 --- a/src/connectors/libs/OceanicJS.ts +++ b/src/connectors/libs/OceanicJS.ts @@ -1,20 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any */ import { Connector } from '../Connector'; import { NodeOption } from '../../Shoukaku'; export class OceanicJS extends Connector { - // sendPacket is where your library send packets to Discord Gateway - public sendPacket(shardId: number, payload: any, important: boolean): void { - return this.client.shards.get(shardId)?.send(payload.op, payload.d, important); - } - // getId is a getter where the lib stores the client user (the one logged in as a bot) id - public getId(): string { - return this.client.user.id; - } - // Listen attaches the event listener to the library you are using - public listen(nodes: NodeOption[]): void { - // Only attach to ready event once, refer to your library for its ready event - this.client.once('ready', () => this.ready(nodes)); - // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) - this.client.on('packet', (packet: any) => this.raw(packet)); - } + // sendPacket is where your library send packets to Discord Gateway + public sendPacket(shardId: number, payload: any, important: boolean): void { + return this.client.shards.get(shardId)?.send(payload.op, payload.d, important); + } + // getId is a getter where the lib stores the client user (the one logged in as a bot) id + public getId(): string { + return this.client.user.id; + } + // Listen attaches the event listener to the library you are using + public listen(nodes: NodeOption[]): void { + // Only attach to ready event once, refer to your library for its ready event + this.client.once('ready', () => this.ready(nodes)); + // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) + this.client.on('packet', (packet: any) => this.raw(packet)); + } } diff --git a/src/connectors/libs/Seyfert.ts b/src/connectors/libs/Seyfert.ts index ddddf2ea..4c4b5c74 100644 --- a/src/connectors/libs/Seyfert.ts +++ b/src/connectors/libs/Seyfert.ts @@ -1,25 +1,26 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any */ import { Connector } from '../Connector'; import { NodeOption } from '../../Shoukaku'; export class Seyfert extends Connector { - // sendPacket is where your library send packets to Discord Gateway - public sendPacket(shardId: number, payload: any, important: boolean): void { - return this.client.gateway.send(shardId, payload); - } - // getId is a getter where the lib stores the client user (the one logged in as a bot) id - public getId(): string { - return this.client.botId; - } - // Listen attaches the event listener to the library you are using - public listen(nodes: NodeOption[]): void { - this.client.events.values.RAW = { - data: { name: "raw" }, - run: (packet: any) => { - // Only attach to ready event once, refer to your library for its ready event - if (packet.t === "READY") return this.ready(nodes); - // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) - return this.raw(packet); - } - } - } + // sendPacket is where your library send packets to Discord Gateway + public sendPacket(shardId: number, payload: unknown, important: boolean): void { + return this.client.gateway.send(shardId, payload); + } + // getId is a getter where the lib stores the client user (the one logged in as a bot) id + public getId(): string { + return this.client.botId; + } + // Listen attaches the event listener to the library you are using + public listen(nodes: NodeOption[]): void { + this.client.events.values.RAW = { + data: { name: 'raw' }, + run: (packet: any) => { + // Only attach to ready event once, refer to your library for its ready event + if (packet.t === 'READY') return this.ready(nodes); + // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) + return this.raw(packet); + } + }; + } } \ No newline at end of file diff --git a/src/connectors/libs/index.ts b/src/connectors/libs/index.ts index 4b1bedea..1d53e9b8 100644 --- a/src/connectors/libs/index.ts +++ b/src/connectors/libs/index.ts @@ -1,4 +1,4 @@ export * from './DiscordJS'; export * from './Eris'; export * from './OceanicJS'; -export * from "./Seyfert" \ No newline at end of file +export * from './Seyfert'; \ No newline at end of file diff --git a/src/guild/Connection.ts b/src/guild/Connection.ts index 28bd4568..4986e5a6 100644 --- a/src/guild/Connection.ts +++ b/src/guild/Connection.ts @@ -1,80 +1,79 @@ import { EventEmitter, once } from 'events'; import { State, VoiceState } from '../Constants'; -import { Shoukaku, VoiceChannelOptions } from '../Shoukaku.js'; +import { Shoukaku, VoiceChannelOptions } from '../Shoukaku'; /** * Represents the partial payload from a stateUpdate event */ export interface StateUpdatePartial { - channel_id?: string; - session_id?: string; - self_deaf: boolean; - self_mute: boolean; + channel_id?: string; + session_id?: string; + self_deaf: boolean; + self_mute: boolean; } /** * Represents the payload from a serverUpdate event */ export interface ServerUpdate { - token: string; - guild_id: string; - endpoint: string; + token: string; + guild_id: string; + endpoint: string; } /** * Represents a connection to a Discord voice channel */ export class Connection extends EventEmitter { - /** + /** * The manager where this connection is on */ - public manager: Shoukaku; - /** + public manager: Shoukaku; + /** * GuildId of the connection that is being managed by this instance */ - public guildId: string; - /** + public guildId: string; + /** * VoiceChannelId of the connection that is being managed by this instance */ - public channelId: string|null; - - /** + public channelId: string | null; + /** * ShardId where this connection sends data on */ - public shardId: number; - /** + public shardId: number; + /** * Mute status in connected voice channel */ - public muted: boolean; - /** + public muted: boolean; + /** * Deafen status in connected voice channel */ - public deafened: boolean; - /** + public deafened: boolean; + /** * Id of the voice channel where this instance was connected before the current channelId */ - public lastChannelId: string|null; - /** + public lastChannelId: string | null; + /** * Id of the currently active voice channel connection */ - public sessionId: string|null; - /** + public sessionId: string | null; + /** * Region of connected voice channel */ - public region: string|null; - /** + public region: string | null; + /** * Last region of the connected voice channel */ - public lastRegion: string|null; - /** + public lastRegion: string | null; + /** * Cached serverUpdate event from Lavalink */ - public serverUpdate: ServerUpdate|null; - /** + public serverUpdate: ServerUpdate | null; + /** * Connection state */ - public state: State; - /** + public state: State; + /** * @param manager The manager of this connection * @param options The options to pass in connection creation * @param options.guildId GuildId in which voice channel to connect to is located @@ -83,91 +82,92 @@ export class Connection extends EventEmitter { * @param options.deaf Optional boolean value to specify whether to deafen the current bot user * @param options.mute Optional boolean value to specify whether to mute the current bot user */ - constructor(manager: Shoukaku, options: VoiceChannelOptions) { - super(); - this.manager = manager; - this.guildId = options.guildId; - this.channelId = options.channelId; - this.shardId = options.shardId; - this.muted = options.mute ?? false; - this.deafened = options.deaf ?? false; - this.lastChannelId = null; - this.sessionId = null; - this.region = null; - this.lastRegion = null; - this.serverUpdate = null; - this.state = State.DISCONNECTED; - } - - /** + constructor(manager: Shoukaku, options: VoiceChannelOptions) { + super(); + this.manager = manager; + this.guildId = options.guildId; + this.channelId = options.channelId; + this.shardId = options.shardId; + this.muted = options.mute ?? false; + this.deafened = options.deaf ?? false; + this.lastChannelId = null; + this.sessionId = null; + this.region = null; + this.lastRegion = null; + this.serverUpdate = null; + this.state = State.DISCONNECTED; + } + + /** * Set the deafen status for the current bot user * @param deaf Boolean value to indicate whether to deafen or undeafen * @defaultValue false */ - public setDeaf(deaf = false): void { - this.deafened = deaf; - this.sendVoiceUpdate(); - } + public setDeaf(deaf = false): void { + this.deafened = deaf; + this.sendVoiceUpdate(); + } - /** + /** * Set the mute status for the current bot user * @param mute Boolean value to indicate whether to mute or unmute * @defaultValue false */ - public setMute(mute = false): void { - this.muted = mute; - this.sendVoiceUpdate(); - } + public setMute(mute = false): void { + this.muted = mute; + this.sendVoiceUpdate(); + } - /** + /** * Disconnect the current bot user from the connected voice channel * @internal */ - public disconnect(): void { - if (this.state === State.DISCONNECTED) return; - this.channelId = null; - this.deafened = false; - this.muted = false; - this.removeAllListeners(); - this.sendVoiceUpdate(); - this.state = State.DISCONNECTED; - this.debug(`[Voice] -> [Node] & [Discord] : Connection Destroyed | Guild: ${this.guildId}`); - } - - /** + public disconnect(): void { + if (this.state === State.DISCONNECTED) return; + this.channelId = null; + this.deafened = false; + this.muted = false; + this.removeAllListeners(); + this.sendVoiceUpdate(); + this.state = State.DISCONNECTED; + this.debug(`[Voice] -> [Node] & [Discord] : Connection Destroyed | Guild: ${this.guildId}`); + } + + /** * Connect the current bot user to a voice channel * @internal */ - public async connect(): Promise { - if (this.state === State.CONNECTING || this.state === State.CONNECTED) return; - - this.state = State.CONNECTING; - this.sendVoiceUpdate(); - this.debug(`[Voice] -> [Discord] : Requesting Connection | Guild: ${this.guildId}`); - - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), this.manager.options.voiceConnectionTimeout * 1000); - - try { - const [ status ] = await once(this, 'connectionUpdate', { signal: controller.signal }); - if (status !== VoiceState.SESSION_READY) { - switch(status) { - case VoiceState.SESSION_ID_MISSING: throw new Error('The voice connection is not established due to missing session id'); - case VoiceState.SESSION_ENDPOINT_MISSING: throw new Error('The voice connection is not established due to missing connection endpoint'); - } - } - this.state = State.CONNECTED; - } catch (error: any) { - this.debug(`[Voice] { + if (this.state === State.CONNECTING || this.state === State.CONNECTED) return; + + this.state = State.CONNECTING; + this.sendVoiceUpdate(); + this.debug(`[Voice] -> [Discord] : Requesting Connection | Guild: ${this.guildId}`); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.manager.options.voiceConnectionTimeout * 1000); + + try { + const [ status ] = await once(this, 'connectionUpdate', { signal: controller.signal }) as [ VoiceState ]; + if (status !== VoiceState.SESSION_READY) { + switch (status) { + case VoiceState.SESSION_ID_MISSING: throw new Error('The voice connection is not established due to missing session id'); + case VoiceState.SESSION_ENDPOINT_MISSING: throw new Error('The voice connection is not established due to missing connection endpoint'); + } + } + this.state = State.CONNECTED; + } catch (e: unknown) { + const error = e as Error; + this.debug(`[Voice] ; export type ResumeOptions = Omit; export enum PlayerEventType { - TRACK_START_EVENT = 'TrackStartEvent', - TRACK_END_EVENT = 'TrackEndEvent', - TRACK_EXCEPTION_EVENT = 'TrackExceptionEvent', - TRACK_STUCK_EVENT = 'TrackStuckEvent', - WEBSOCKET_CLOSED_EVENT = 'WebSocketClosedEvent', + TRACK_START_EVENT = 'TrackStartEvent', + TRACK_END_EVENT = 'TrackEndEvent', + TRACK_EXCEPTION_EVENT = 'TrackExceptionEvent', + TRACK_STUCK_EVENT = 'TrackStuckEvent', + WEBSOCKET_CLOSED_EVENT = 'WebSocketClosedEvent' } export interface Band { - band: number; - gain: number; + band: number; + gain: number; } export interface KaraokeSettings { - level?: number; - monoLevel?: number; - filterBand?: number; - filterWidth?: number; + level?: number; + monoLevel?: number; + filterBand?: number; + filterWidth?: number; } export interface TimescaleSettings { - speed?: number; - pitch?: number; - rate?: number; + speed?: number; + pitch?: number; + rate?: number; } export interface FreqSettings { - frequency?: number; - depth?: number; + frequency?: number; + depth?: number; } export interface RotationSettings { - rotationHz?: number; + rotationHz?: number; } export interface DistortionSettings { - sinOffset?: number; - sinScale?: number; - cosOffset?: number; - cosScale?: number; - tanOffset?: number; - tanScale?: number; - offset?: number; - scale?: number; + sinOffset?: number; + sinScale?: number; + cosOffset?: number; + cosScale?: number; + tanOffset?: number; + tanScale?: number; + offset?: number; + scale?: number; } export interface ChannelMixSettings { - leftToLeft?: number; - leftToRight?: number; - rightToLeft?: number; - rightToRight?: number; + leftToLeft?: number; + leftToRight?: number; + rightToLeft?: number; + rightToRight?: number; } export interface LowPassSettings { - smoothing?: number + smoothing?: number; } export interface PlayerEvent { - op: OpCodes.EVENT; - guildId: string; + op: OpCodes.EVENT; + guildId: string; } export interface TrackStartEvent extends PlayerEvent { - type: PlayerEventType.TRACK_START_EVENT; - track: Track; + type: PlayerEventType.TRACK_START_EVENT; + track: Track; } export interface TrackEndEvent extends PlayerEvent { - type: PlayerEventType.TRACK_END_EVENT; - track: Track; - reason: TrackEndReason; + type: PlayerEventType.TRACK_END_EVENT; + track: Track; + reason: TrackEndReason; } export interface TrackStuckEvent extends PlayerEvent { - type: PlayerEventType.TRACK_STUCK_EVENT; - track: Track; - thresholdMs: number; + type: PlayerEventType.TRACK_STUCK_EVENT; + track: Track; + thresholdMs: number; } export interface TrackExceptionEvent extends PlayerEvent { - type: PlayerEventType.TRACK_EXCEPTION_EVENT; - exception: Exception; + type: PlayerEventType.TRACK_EXCEPTION_EVENT; + exception: Exception; } export interface WebSocketClosedEvent extends PlayerEvent { - type: PlayerEventType.WEBSOCKET_CLOSED_EVENT; - code: number; - byRemote: boolean; - reason: string; + type: PlayerEventType.WEBSOCKET_CLOSED_EVENT; + code: number; + byRemote: boolean; + reason: string; } export interface PlayerUpdate { - op: OpCodes.PLAYER_UPDATE; - state: { - connected: boolean; - position: number; - time: number; - ping: number; - }; - guildId: string; + op: OpCodes.PLAYER_UPDATE; + state: { + connected: boolean; + position: number; + time: number; + ping: number; + }; + guildId: string; } export interface FilterOptions { - volume?: number; - equalizer?: Band[]; - karaoke?: KaraokeSettings|null; - timescale?: TimescaleSettings|null; - tremolo?: FreqSettings|null; - vibrato?: FreqSettings|null; - rotation?: RotationSettings|null; - distortion?: DistortionSettings|null; - channelMix?: ChannelMixSettings|null; - lowPass?: LowPassSettings|null; + volume?: number; + equalizer?: Band[]; + karaoke?: KaraokeSettings | null; + timescale?: TimescaleSettings | null; + tremolo?: FreqSettings | null; + vibrato?: FreqSettings | null; + rotation?: RotationSettings | null; + distortion?: DistortionSettings | null; + channelMix?: ChannelMixSettings | null; + lowPass?: LowPassSettings | null; } export interface PlayerEvents { - /** + /** * Emitted when the current playing track ends * @eventProperty */ - 'end': [reason: TrackEndEvent]; - /** + 'end': [reason: TrackEndEvent]; + /** * Emitted when the current playing track gets stuck due to an error * @eventProperty */ - 'stuck': [data: TrackStuckEvent]; - /** + 'stuck': [data: TrackStuckEvent]; + /** * Emitted when the current websocket connection is closed * @eventProperty */ - 'closed': [reason: WebSocketClosedEvent]; - /** + 'closed': [reason: WebSocketClosedEvent]; + /** * Emitted when a new track starts * @eventProperty */ - 'start': [data: TrackStartEvent]; - /** + 'start': [data: TrackStartEvent]; + /** * Emitted when there is an error caused by the current playing track * @eventProperty */ - 'exception': [reason: TrackExceptionEvent]; - /** + 'exception': [reason: TrackExceptionEvent]; + /** * Emitted when the library manages to resume the player * @eventProperty */ - 'resumed': [player: Player]; - /** + 'resumed': [player: Player]; + /** * Emitted when a playerUpdate even is received from Lavalink * @eventProperty */ - 'update': [data: PlayerUpdate]; + 'update': [data: PlayerUpdate]; } -export declare interface Player { - on(event: K, listener: (...args: PlayerEvents[K]) => void): this; - once(event: K, listener: (...args: PlayerEvents[K]) => void): this; - off(event: K, listener: (...args: PlayerEvents[K]) => void): this; - emit(event: string | symbol, ...args: unknown[]): boolean; +export declare interface IPlayer { + on(event: K, listener: (...args: PlayerEvents[K]) => void): this; + once(event: K, listener: (...args: PlayerEvents[K]) => void): this; + off(event: K, listener: (...args: PlayerEvents[K]) => void): this; + emit(event: string | symbol, ...args: unknown[]): boolean; } - /** * Wrapper object around Lavalink */ -export class Player extends EventEmitter { - /** +export class Player extends EventEmitter implements IPlayer { + /** * GuildId of this player */ - public readonly guildId: string; - /** + public readonly guildId: string; + /** * Lavalink node this player is connected to */ - public node: Node; - /** + public node: Node; + /** * Base64 encoded data of the current track */ - public track: string|null; - /** + public track: string | null; + /** * Global volume of the player */ - public volume: number; - /** + public volume: number; + /** * Pause status in current player */ - public paused: boolean; - /** + public paused: boolean; + /** * Ping represents the number of milliseconds between heartbeat and ack. Could be `-1` if not connected */ - public ping: number; - /** + public ping: number; + /** * Position in ms of current track */ - public position: number; - /** + public position: number; + /** * Filters on current track */ - public filters: FilterOptions; - /** + public filters: FilterOptions; + /** * @param node An instance of Node (Lavalink API wrapper) * @param connection An instance of connection class */ - constructor(guildId: string, node: Node) { - super(); - this.guildId = guildId; - this.node = node; - this.track = null; - this.volume = 100; - this.paused = false; - this.position = 0; - this.ping = 0; - this.filters = {}; - } - - public get data(): UpdatePlayerInfo { - const connection = this.node.manager.connections.get(this.guildId)!; - return { - guildId: this.guildId, - playerOptions: { - track: { - encoded: this.track - }, - position: this.position, - paused: this.paused, - filters: this.filters, - voice: { - token: connection.serverUpdate!.token, - endpoint: connection.serverUpdate!.endpoint, - sessionId: connection.sessionId! - }, - volume: this.volume - } - }; - } - - /** + constructor(guildId: string, node: Node) { + super(); + this.guildId = guildId; + this.node = node; + this.track = null; + this.volume = 100; + this.paused = false; + this.position = 0; + this.ping = 0; + this.filters = {}; + } + + public get data(): UpdatePlayerInfo { + const connection = this.node.manager.connections.get(this.guildId)!; + return { + guildId: this.guildId, + playerOptions: { + track: { + encoded: this.track + }, + position: this.position, + paused: this.paused, + filters: this.filters, + voice: { + token: connection.serverUpdate!.token, + endpoint: connection.serverUpdate!.endpoint, + sessionId: connection.sessionId! + }, + volume: this.volume + } + }; + } + + /** * Move player to another node * @param name Name of node to move to, or the default ideal node * @returns true if the player was moved, false if not */ - public async move(name?: string): Promise { - const connection = this.node.manager.connections.get(this.guildId); - const node = this.node.manager.nodes.get(name!) || this.node.manager.getIdealNode(connection); + public async move(name?: string): Promise { + const connection = this.node.manager.connections.get(this.guildId); + const node = this.node.manager.nodes.get(name!) ?? this.node.manager.getIdealNode(connection); - if (!node && ![ ...this.node.manager.nodes.values() ].some(node => node.state === State.CONNECTED)) - throw new Error('No available nodes to move to'); + if (!node && ![ ...this.node.manager.nodes.values() ].some(node => node.state === State.CONNECTED)) + throw new Error('No available nodes to move to'); - if (!node || node.name === this.node.name || node.state !== State.CONNECTED) return false; + if (!node || node.name === this.node.name || node.state !== State.CONNECTED) return false; - let lastNode = this.node.manager.nodes.get(this.node.name); - if (!lastNode || lastNode.state !== State.CONNECTED) - lastNode = this.node.manager.getIdealNode(connection); + let lastNode = this.node.manager.nodes.get(this.node.name); + if (!lastNode || lastNode.state !== State.CONNECTED) + lastNode = this.node.manager.getIdealNode(connection); - await this.destroy(); + await this.destroy(); - try { - this.node = node; - await this.resume(); - return true; - } catch (error) { - this.node = lastNode!; - await this.resume(); - return false; - } - } - - /** + try { + this.node = node; + await this.resume(); + return true; + } catch { + this.node = lastNode!; + await this.resume(); + return false; + } + } + + /** * Destroys the player in remote lavalink side */ - public async destroy(): Promise { - await this.node.rest.destroyPlayer(this.guildId); - } + public async destroy(): Promise { + await this.node.rest.destroyPlayer(this.guildId); + } - /** + /** * Play a new track * @param playable Options for playing this track * @param noReplace Set it to true if you don't want to replace the currently playing track */ - public playTrack(playerOptions: PlayOptions, noReplace: boolean = false): Promise { - return this.update(playerOptions, noReplace); - } + public playTrack(playerOptions: PlayOptions, noReplace = false): Promise { + return this.update(playerOptions, noReplace); + } - /** + /** * Stop the currently playing track */ - public stopTrack(): Promise { - return this.update({ track: { encoded: null }, position: 0 }); - } + public stopTrack(): Promise { + return this.update({ track: { encoded: null }, position: 0 }); + } - /** + /** * Pause or unpause the currently playing track * @param paused Boolean value to specify whether to pause or unpause the current bot user */ - public setPaused(paused: boolean = true): Promise { - return this.update({ paused }); - } + public setPaused(paused = true): Promise { + return this.update({ paused }); + } - /** + /** * Seek to a specific time in the currently playing track * @param position Position to seek to in milliseconds */ - public seekTo(position: number): Promise { - return this.update({ position }); - } + public seekTo(position: number): Promise { + return this.update({ position }); + } - /** + /** * Sets the global volume of the player * @param volume Target volume 0-1000 */ - public setGlobalVolume(volume: number): Promise { - return this.update({ volume }); - } + public setGlobalVolume(volume: number): Promise { + return this.update({ volume }); + } - /** + /** * Sets the filter volume of the player * @param volume Target volume 0.0-5.0 */ - async setFilterVolume(volume: number): Promise { - return this.setFilters({ volume }); - } + // eslint-disable-next-line require-await + async setFilterVolume(volume: number): Promise { + return this.setFilters({ volume }); + } - /** + /** * Change the equalizer settings applied to the currently playing track * @param equalizer An array of objects that conforms to the Bands type that define volumes at different frequencies */ - public async setEqualizer(equalizer: Band[]): Promise { - return this.setFilters({ equalizer }); - } + // eslint-disable-next-line require-await + public async setEqualizer(equalizer: Band[]): Promise { + return this.setFilters({ equalizer }); + } - /** + /** * Change the karaoke settings applied to the currently playing track * @param karaoke An object that conforms to the KaraokeSettings type that defines a range of frequencies to mute */ - public setKaraoke(karaoke?: KaraokeSettings): Promise { - return this.setFilters({ karaoke: karaoke || null }); - } + public setKaraoke(karaoke?: KaraokeSettings): Promise { + return this.setFilters({ karaoke: karaoke ?? null }); + } - /** + /** * Change the timescale settings applied to the currently playing track * @param timescale An object that conforms to the TimescaleSettings type that defines the time signature to play the audio at */ - public setTimescale(timescale?: TimescaleSettings): Promise { - return this.setFilters({ timescale: timescale || null }); - } + public setTimescale(timescale?: TimescaleSettings): Promise { + return this.setFilters({ timescale: timescale ?? null }); + } - /** + /** * Change the tremolo settings applied to the currently playing track * @param tremolo An object that conforms to the FreqSettings type that defines an oscillation in volume */ - public setTremolo(tremolo?: FreqSettings): Promise { - return this.setFilters({ tremolo: tremolo || null }); - } + public setTremolo(tremolo?: FreqSettings): Promise { + return this.setFilters({ tremolo: tremolo ?? null }); + } - /** + /** * Change the vibrato settings applied to the currently playing track * @param vibrato An object that conforms to the FreqSettings type that defines an oscillation in pitch */ - public setVibrato(vibrato?: FreqSettings): Promise { - return this.setFilters({ vibrato: vibrato || null }); - } + public setVibrato(vibrato?: FreqSettings): Promise { + return this.setFilters({ vibrato: vibrato ?? null }); + } - /** + /** * Change the rotation settings applied to the currently playing track * @param rotation An object that conforms to the RotationSettings type that defines the frequency of audio rotating round the listener */ - public setRotation(rotation?: RotationSettings): Promise { - return this.setFilters({ rotation: rotation || null }); - } + public setRotation(rotation?: RotationSettings): Promise { + return this.setFilters({ rotation: rotation ?? null }); + } - /** + /** * Change the distortion settings applied to the currently playing track * @param distortion An object that conforms to DistortionSettings that defines distortions in the audio * @returns The current player instance */ - public setDistortion(distortion?: DistortionSettings): Promise { - return this.setFilters({ distortion: distortion || null }); - } + public setDistortion(distortion?: DistortionSettings): Promise { + return this.setFilters({ distortion: distortion ?? null }); + } - /** + /** * Change the channel mix settings applied to the currently playing track * @param channelMix An object that conforms to ChannelMixSettings that defines how much the left and right channels affect each other (setting all factors to 0.5 causes both channels to get the same audio) */ - public setChannelMix(channelMix?: ChannelMixSettings): Promise { - return this.setFilters({ channelMix: channelMix || null }); - } + public setChannelMix(channelMix?: ChannelMixSettings): Promise { + return this.setFilters({ channelMix: channelMix ?? null }); + } - /** + /** * Change the low pass settings applied to the currently playing track * @param lowPass An object that conforms to LowPassSettings that defines the amount of suppression on higher frequencies */ - public setLowPass(lowPass?: LowPassSettings): Promise { - return this.setFilters({ lowPass: lowPass || null }); - } + public setLowPass(lowPass?: LowPassSettings): Promise { + return this.setFilters({ lowPass: lowPass ?? null }); + } - /** + /** * Change the all filter settings applied to the currently playing track * @param filters An object that conforms to FilterOptions that defines all filters to apply/modify */ - public setFilters(filters: FilterOptions): Promise { - return this.update({ filters }); - } + public setFilters(filters: FilterOptions): Promise { + return this.update({ filters }); + } - /** + /** * Clear all filters applied to the currently playing track */ - public clearFilters(): Promise { - return this.setFilters({ - volume: 1, - equalizer: [], - karaoke: null, - timescale: null, - tremolo: null, - vibrato: null, - rotation: null, - distortion: null, - channelMix: null, - lowPass: null, - }); - } - - /** + public clearFilters(): Promise { + return this.setFilters({ + volume: 1, + equalizer: [], + karaoke: null, + timescale: null, + tremolo: null, + vibrato: null, + rotation: null, + distortion: null, + channelMix: null, + lowPass: null + }); + } + + /** * Resumes the current track * @param options An object that conforms to ResumeOptions that specify behavior on resuming * @param noReplace Set it to true if you don't want to replace the currently playing track */ - public async resume(options: ResumeOptions = {}, noReplace: boolean = false): Promise { - const data = this.data; + public async resume(options: ResumeOptions = {}, noReplace = false): Promise { + const data = this.data; - if (typeof options.position === 'number') - data.playerOptions.position = options.position; - if (typeof options.endTime === 'number') - data.playerOptions.endTime = options.endTime; - if (typeof options.paused === 'boolean') - data.playerOptions.paused = options.paused; - if (typeof options.volume === 'number') - data.playerOptions.volume = options.volume; + if (typeof options.position === 'number') + data.playerOptions.position = options.position; + if (typeof options.endTime === 'number') + data.playerOptions.endTime = options.endTime; + if (typeof options.paused === 'boolean') + data.playerOptions.paused = options.paused; + if (typeof options.volume === 'number') + data.playerOptions.volume = options.volume; - await this.update(data.playerOptions, noReplace); + await this.update(data.playerOptions, noReplace); - this.emit('resumed', this); - } + this.emit('resumed', this); + } - /** + /** * If you want to update the whole player yourself, sends raw update player info to lavalink * @param playerOptions Options to update the player data * @param noReplace Set it to true if you don't want to replace the currently playing track */ - public async update(playerOptions: UpdatePlayerOptions, noReplace: boolean = false): Promise { - const data = { - guildId: this.guildId, - noReplace, - playerOptions - } - - await this.node.rest.updatePlayer(data); - - if (!noReplace) this.paused = false - - if (playerOptions.filters) { - const filters = { ...this.filters, ...playerOptions.filters }; - this.filters = filters; - } - - if (typeof playerOptions.track !== 'undefined') - this.track = playerOptions.track.encoded || null; - if (typeof playerOptions.paused === 'boolean') - this.paused = playerOptions.paused; - if (typeof playerOptions.volume === 'number') - this.volume = playerOptions.volume; - if (typeof playerOptions.position === 'number') - this.position = playerOptions.position; - } - - /** + public async update(playerOptions: UpdatePlayerOptions, noReplace = false): Promise { + const data = { + guildId: this.guildId, + noReplace, + playerOptions + }; + + await this.node.rest.updatePlayer(data); + + if (!noReplace) this.paused = false; + + if (playerOptions.filters) { + const filters = { ...this.filters, ...playerOptions.filters }; + this.filters = filters; + } + + if (typeof playerOptions.track !== 'undefined') + this.track = playerOptions.track.encoded ?? null; + if (typeof playerOptions.paused === 'boolean') + this.paused = playerOptions.paused; + if (typeof playerOptions.volume === 'number') + this.volume = playerOptions.volume; + if (typeof playerOptions.position === 'number') + this.position = playerOptions.position; + } + + /** * Cleans this player instance * @internal */ - public clean(): void { - this.removeAllListeners(); - this.track = null; - this.volume = 100; - this.position = 0; - this.filters = {}; - } + public clean(): void { + this.removeAllListeners(); + this.track = null; + this.volume = 100; + this.position = 0; + this.filters = {}; + } - /** + /** * Sends server update to lavalink * @internal */ - public async sendServerUpdate(connection: Connection): Promise { - const playerUpdate = { - guildId: this.guildId, - playerOptions: { - voice: { - token: connection.serverUpdate!.token, - endpoint: connection.serverUpdate!.endpoint, - sessionId: connection.sessionId! - } - } - }; - await this.node.rest.updatePlayer(playerUpdate); - } - - /** + public async sendServerUpdate(connection: Connection): Promise { + const playerUpdate = { + guildId: this.guildId, + playerOptions: { + voice: { + token: connection.serverUpdate!.token, + endpoint: connection.serverUpdate!.endpoint, + sessionId: connection.sessionId! + } + } + }; + await this.node.rest.updatePlayer(playerUpdate); + } + + /** * Handle player update data */ - public onPlayerUpdate(json: PlayerUpdate): void { - const { position, ping } = json.state; - this.position = position; - this.ping = ping; - this.emit('update', json); - } + public onPlayerUpdate(json: PlayerUpdate): void { + const { position, ping } = json.state; + this.position = position; + this.ping = ping; + this.emit('update', json); + } - /** + /** * Handle player events received from Lavalink * @param json JSON data from Lavalink * @internal */ - public onPlayerEvent(json: TrackStartEvent|TrackEndEvent|TrackStuckEvent|TrackExceptionEvent|WebSocketClosedEvent): void { - switch (json.type) { - case 'TrackStartEvent': - if (this.track) this.track = json.track.encoded; - this.emit('start', json); - break; - case 'TrackEndEvent': - this.emit('end', json); - break; - case 'TrackStuckEvent': - this.emit('stuck', json); - break; - case 'TrackExceptionEvent': - this.emit('exception', json); - break; - case 'WebSocketClosedEvent': - this.emit('closed', json); - break; - default: - this.node.emit( - 'debug', - this.node.name, - `[Player] -> [Node] : Unknown Player Event Type, Data => ${JSON.stringify(json)}` - ); - } - } + public onPlayerEvent(json: TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent): void { + switch (json.type) { + case PlayerEventType.TRACK_START_EVENT: + if (this.track) this.track = json.track.encoded; + this.emit('start', json); + break; + case PlayerEventType.TRACK_END_EVENT: + this.emit('end', json); + break; + case PlayerEventType.TRACK_STUCK_EVENT: + this.emit('stuck', json); + break; + case PlayerEventType.TRACK_EXCEPTION_EVENT: + this.emit('exception', json); + break; + case PlayerEventType.WEBSOCKET_CLOSED_EVENT: + this.emit('closed', json); + break; + default: + this.node.emit( + 'debug', + this.node.name, + `[Player] -> [Node] : Unknown Player Event Type, Data => ${JSON.stringify(json)}` + ); + } + } } diff --git a/src/node/Node.ts b/src/node/Node.ts index cacec587..4fd01f92 100644 --- a/src/node/Node.ts +++ b/src/node/Node.ts @@ -8,141 +8,141 @@ import { PlayerUpdate, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, Trac import Websocket from 'ws'; export interface Ready { - op: OpCodes.READY; - resumed: boolean; - sessionId: string; + op: OpCodes.READY; + resumed: boolean; + sessionId: string; } export interface Stats { - op: OpCodes.STATS; - players: number; - playingPlayers: number; - memory: { - reservable: number; - used: number; - free: number; - allocated: number - }; - frameStats: { - sent: number; - deficit: number; - nulled: number - }; - cpu: { - cores: number; - systemLoad: number; - lavalinkLoad: number; - }; - uptime: number; + op: OpCodes.STATS; + players: number; + playingPlayers: number; + memory: { + reservable: number; + used: number; + free: number; + allocated: number; + }; + frameStats: { + sent: number; + deficit: number; + nulled: number; + }; + cpu: { + cores: number; + systemLoad: number; + lavalinkLoad: number; + }; + uptime: number; } -export type NodeInfoVersion = { - semver: string; - major: number; - minor: number; - patch: number; - preRelease?: string; - build?: string; +export interface NodeInfoVersion { + semver: string; + major: number; + minor: number; + patch: number; + preRelease?: string; + build?: string; } -export type NodeInfoGit = { - branch: string; - commit: string; - commitTime: number; +export interface NodeInfoGit { + branch: string; + commit: string; + commitTime: number; } -export type NodeInfoPlugin = { - name: string; - version: string; +export interface NodeInfoPlugin { + name: string; + version: string; } -export type NodeInfo = { - version: NodeInfoVersion; - buildTime: number; - git: NodeInfoGit; - jvm: string; - lavaplayer: string; - sourceManagers: string[]; - filters: string[]; - plugins: NodeInfoPlugin[]; +export interface NodeInfo { + version: NodeInfoVersion; + buildTime: number; + git: NodeInfoGit; + jvm: string; + lavaplayer: string; + sourceManagers: string[]; + filters: string[]; + plugins: NodeInfoPlugin[]; } export interface ResumableHeaders { - [key: string]: string; - 'Client-Name': string; - 'User-Agent': string; - 'Authorization': string; - 'User-Id': string; - 'Session-Id': string; + [key: string]: string; + 'Client-Name': string; + 'User-Agent': string; + 'Authorization': string; + 'User-Id': string; + 'Session-Id': string; } -export interface NonResumableHeaders extends Omit {} +export type NonResumableHeaders = Omit; /** * Represents a Lavalink node */ export class Node extends EventEmitter { - /** + /** * Shoukaku class */ - public readonly manager: Shoukaku; - /** + public readonly manager: Shoukaku; + /** * Lavalink rest API */ - public readonly rest: Rest; - /** + public readonly rest: Rest; + /** * Name of this node */ - public readonly name: string; - /** + public readonly name: string; + /** * Group in which this node is contained */ - public readonly group?: string; - /** + public readonly group?: string; + /** * Websocket version this node will use */ - public readonly version: string; - /** + public readonly version: string; + /** * URL of Lavalink */ - private readonly url: string; - /** + private readonly url: string; + /** * Credentials to access Lavalink */ - private readonly auth: string; - /** + private readonly auth: string; + /** * The number of reconnects to Lavalink */ - public reconnects: number; - /** + public reconnects: number; + /** * The state of this connection */ - public state: State; - /** + public state: State; + /** * Statistics from Lavalink */ - public stats: Stats|null; - /** + public stats: Stats | null; + /** * Information about lavalink node */ - public info: NodeInfo|null; - /** + public info: NodeInfo | null; + /** * Websocket instance */ - public ws: Websocket|null; - /** + public ws: Websocket | null; + /** * SessionId of this Lavalink connection (not to be confused with Discord SessionId) */ - public sessionId: string|null; - /** + public sessionId: string | null; + /** * Boolean that represents if the node has initialized once */ - protected initialized: boolean; - /** + protected initialized: boolean; + /** * Boolean that represents if this connection is destroyed */ - protected destroyed: boolean; - /** + protected destroyed: boolean; + /** * @param manager Shoukaku instance * @param options Options on creating this node * @param options.name Name of this node @@ -151,272 +151,276 @@ export class Node extends EventEmitter { * @param options.secure Whether to use secure protocols or not * @param options.group Group of this node */ - constructor(manager: Shoukaku, options: NodeOption) { - super(); - this.manager = manager; - this.rest = new (this.manager.options.structures.rest || Rest)(this, options); - this.name = options.name; - this.group = options.group; - this.version = `/v${Versions.WEBSOCKET_VERSION}`; - this.url = `${options.secure ? 'wss' : 'ws'}://${options.url}`; - this.auth = options.auth; - this.reconnects = 0; - this.state = State.DISCONNECTED; - this.stats = null; - this.info = null; - this.ws = null; - this.sessionId = null; - this.initialized = false; - this.destroyed = false; - } - - /** + constructor(manager: Shoukaku, options: NodeOption) { + super(); + this.manager = manager; + this.rest = new (this.manager.options.structures.rest ?? Rest)(this, options); + this.name = options.name; + this.group = options.group; + this.version = `/v${Versions.WEBSOCKET_VERSION}`; + this.url = `${options.secure ? 'wss' : 'ws'}://${options.url}`; + this.auth = options.auth; + this.reconnects = 0; + this.state = State.DISCONNECTED; + this.stats = null; + this.info = null; + this.ws = null; + this.sessionId = null; + this.initialized = false; + this.destroyed = false; + } + + /** * Penalties for load balancing * @returns Penalty score * @internal @readonly */ - get penalties(): number { - let penalties = 0; - if (!this.stats) return penalties; + get penalties(): number { + let penalties = 0; + if (!this.stats) return penalties; - penalties += this.stats.players; - penalties += Math.round(Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10); + penalties += this.stats.players; + penalties += Math.round(Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10); - if (this.stats.frameStats) { - penalties += this.stats.frameStats.deficit; - penalties += this.stats.frameStats.nulled * 2; - } + if (this.stats.frameStats) { + penalties += this.stats.frameStats.deficit; + penalties += this.stats.frameStats.nulled * 2; + } - return penalties; - } + return penalties; + } - /** + /** * If we should clean this node * @internal @readonly */ - private get shouldClean(): boolean { - return this.destroyed || this.reconnects >= this.manager.options.reconnectTries; - } + private get shouldClean(): boolean { + return this.destroyed || this.reconnects >= this.manager.options.reconnectTries; + } - /** + /** * Connect to Lavalink */ - public connect(): void { - if (!this.manager.id) throw new Error('Don\'t connect a node when the library is not yet ready'); - if (this.destroyed) throw new Error('You can\'t re-use the same instance of a node once disconnected, please re-add the node again'); - - this.state = State.CONNECTING; - - const headers: NonResumableHeaders|ResumableHeaders = { - 'Client-Name': ShoukakuClientInfo, - 'User-Agent': this.manager.options.userAgent, - 'Authorization': this.auth, - 'User-Id': this.manager.id - }; - - if (this.sessionId) headers['Session-Id'] = this.sessionId; - this.emit('debug', `[Socket] -> [${this.name}] : Connecting ${this.url}, Version: ${this.version}, Trying to resume? ${!!this.sessionId}`); - if (!this.initialized) this.initialized = true; - - const url = new URL(`${this.url}${this.version}/websocket`); - this.ws = new Websocket(url.toString(), { headers } as Websocket.ClientOptions); - this.ws.once('upgrade', response => this.open(response)); - this.ws.once('close', (...args) => this.close(...args)); - this.ws.on('error', error => this.error(error)); - this.ws.on('message', data => this.message(data).catch(error => this.error(error))); - } - - /** + public connect(): void { + if (!this.manager.id) throw new Error('Don\'t connect a node when the library is not yet ready'); + if (this.destroyed) throw new Error('You can\'t re-use the same instance of a node once disconnected, please re-add the node again'); + + this.state = State.CONNECTING; + + const headers: NonResumableHeaders | ResumableHeaders = { + 'Client-Name': ShoukakuClientInfo, + 'User-Agent': this.manager.options.userAgent, + 'Authorization': this.auth, + 'User-Id': this.manager.id + }; + + if (this.sessionId) headers['Session-Id'] = this.sessionId; + this.emit('debug', `[Socket] -> [${this.name}] : Connecting ${this.url}, Version: ${this.version}, Trying to resume? ${!!this.sessionId}`); + if (!this.initialized) this.initialized = true; + + const url = new URL(`${this.url}${this.version}/websocket`); + this.ws = new Websocket(url.toString(), { headers } as Websocket.ClientOptions); + this.ws.once('upgrade', response => this.open(response)); + this.ws.once('close', (...args) => this.close(...args)); + this.ws.on('error', error => this.error(error)); + this.ws.on('message', data => void this.message(data).catch(error => this.error(error))); + } + + /** * Disconnect from lavalink * @param code Status code * @param reason Reason for disconnect */ - public disconnect(code: number, reason?: string): void { - this.destroyed = true; - this.internalDisconnect(code, reason); - } + public disconnect(code: number, reason?: string): void { + this.destroyed = true; + this.internalDisconnect(code, reason); + } - /** + /** * Handle connection open event from Lavalink * @param response Response from Lavalink * @internal */ - private open(response: IncomingMessage): void { - const resumed = response.headers['session-resumed'] === 'true'; - this.emit('debug', `[Socket] <-> [${this.name}] : Connection Handshake Done! ${this.url} | Upgrade Headers Resumed: ${resumed}`); - this.reconnects = 0; - this.state = State.NEARLY; - } - - /** + private open(response: IncomingMessage): void { + const resumed = response.headers['session-resumed'] === 'true'; + this.emit('debug', `[Socket] <-> [${this.name}] : Connection Handshake Done! ${this.url} | Upgrade Headers Resumed: ${resumed}`); + this.reconnects = 0; + this.state = State.NEARLY; + } + + /** * Handle message from Lavalink * @param message JSON message * @internal */ - private async message(message: unknown): Promise { - const json: Ready|Stats|PlayerUpdate|TrackStartEvent|TrackEndEvent|TrackStuckEvent|TrackExceptionEvent|WebSocketClosedEvent = JSON.parse(message as string); - if (!json) return; - this.emit('raw', json); - switch(json.op) { - case OpCodes.STATS: - this.emit('debug', `[Socket] <- [${this.name}] : Node Status Update | Server Load: ${this.penalties}`); - this.stats = json; - break; - case OpCodes.READY: - if (!json.sessionId) { - this.emit('debug', `[Socket] -> [${this.name}] : No session id found from ready op? disconnecting and reconnecting to avoid issues`); - return this.internalDisconnect(1000); - } - - this.sessionId = json.sessionId; - - const players = [ ...this.manager.players.values() ].filter(player => player.node.name === this.name); - const resumeByLibrary = this.initialized && (players.length && this.manager.options.resumeByLibrary); - - if (!json.resumed && resumeByLibrary) { - try { - await this.resumePlayers(); - } catch (error) { - this.error(error); - } - } - - this.state = State.CONNECTED; - this.emit('debug', `[Socket] -> [${this.name}] : Lavalink is ready! | Lavalink resume: ${json.resumed} | Lib resume: ${!!resumeByLibrary}`); - this.emit('ready', json.resumed || resumeByLibrary); - - if (this.manager.options.resume) { - await this.rest.updateSession(this.manager.options.resume, this.manager.options.resumeTimeout); - this.emit('debug', `[Socket] -> [${this.name}] : Resuming configured!`); - } - break; - case OpCodes.EVENT: - case OpCodes.PLAYER_UPDATE: - const player = this.manager.players.get(json.guildId); - if (!player) return; - if (json.op === OpCodes.EVENT) - player.onPlayerEvent(json); - else - player.onPlayerUpdate(json); - break; - default: - this.emit('debug', `[Player] -> [Node] : Unknown Message Op, Data => ${JSON.stringify(json)}`); - } - } - - /** + private async message(message: unknown): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const json: Ready | Stats | PlayerUpdate | TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent = JSON.parse(message as string); + if (!json) return; + this.emit('raw', json); + switch (json.op) { + case OpCodes.STATS: + this.emit('debug', `[Socket] <- [${this.name}] : Node Status Update | Server Load: ${this.penalties}`); + this.stats = json; + break; + case OpCodes.READY: { + if (!json.sessionId) { + this.emit('debug', `[Socket] -> [${this.name}] : No session id found from ready op? disconnecting and reconnecting to avoid issues`); + return this.internalDisconnect(1000); + } + + this.sessionId = json.sessionId; + + const players = [ ...this.manager.players.values() ].filter(player => player.node.name === this.name); + const resumeByLibrary = this.initialized && (players.length && this.manager.options.resumeByLibrary); + + if (!json.resumed && resumeByLibrary) { + try { + await this.resumePlayers(); + } catch (error) { + this.error(error); + } + } + + this.state = State.CONNECTED; + this.emit('debug', `[Socket] -> [${this.name}] : Lavalink is ready! | Lavalink resume: ${json.resumed} | Lib resume: ${!!resumeByLibrary}`); + this.emit('ready', json.resumed || resumeByLibrary); + + if (this.manager.options.resume) { + await this.rest.updateSession(this.manager.options.resume, this.manager.options.resumeTimeout); + this.emit('debug', `[Socket] -> [${this.name}] : Resuming configured!`); + } + break; + } + case OpCodes.EVENT: + case OpCodes.PLAYER_UPDATE: { + const player = this.manager.players.get(json.guildId); + if (!player) return; + if (json.op === OpCodes.EVENT) + player.onPlayerEvent(json); + else + player.onPlayerUpdate(json); + break; + } + default: + this.emit('debug', `[Player] -> [Node] : Unknown Message Op, Data => ${JSON.stringify(json)}`); + } + } + + /** * Handle closed event from lavalink * @param code Status close * @param reason Reason for connection close */ - private close(code: number, reason: unknown): void { - this.emit('debug', `[Socket] <-/-> [${this.name}] : Connection Closed, Code: ${code || 'Unknown Code'}`); - this.emit('close', code, reason); - if (this.shouldClean) - this.clean(); - else - this.reconnect(); - } - - /** + private close(code: number, reason: unknown): void { + this.emit('debug', `[Socket] <-/-> [${this.name}] : Connection Closed, Code: ${code || 'Unknown Code'}`); + this.emit('close', code, reason); + if (this.shouldClean) + void this.clean(); + else + void this.reconnect(); + } + + /** * To emit error events easily * @param error error message */ - public error(error: Error|unknown): void { - this.emit('error', error); - } + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + public error(error: Error | unknown): void { + this.emit('error', error); + } - /** + /** * Internal disconnect function * @internal */ - private internalDisconnect(code: number, reason?: string): void { - if (this.destroyed) return; + private internalDisconnect(code: number, reason?: string): void { + if (this.destroyed) return; - this.state = State.DISCONNECTING; + this.state = State.DISCONNECTING; - if (this.ws) - this.ws.close(code, reason); - else - this.clean(); - } + if (this.ws) + this.ws.close(code, reason); + else + void this.clean(); + } - /** + /** * Destroys the websocket connection * @internal */ - private destroy(count: number = 0): void { - this.ws?.removeAllListeners(); - this.ws?.close(); - this.ws = null; - this.sessionId = null; - this.state = State.DISCONNECTED; - if (!this.shouldClean) return; - this.destroyed = true; - this.emit('disconnect', count); - } - - /** + private destroy(count = 0): void { + this.ws?.removeAllListeners(); + this.ws?.close(); + this.ws = null; + this.sessionId = null; + this.state = State.DISCONNECTED; + if (!this.shouldClean) return; + this.destroyed = true; + this.emit('disconnect', count); + } + + /** * Cleans and moves players to other nodes if possible * @internal */ - private async clean(): Promise { - const move = this.manager.options.moveOnDisconnect; - if (!move) return this.destroy(); - let count = 0; - try { - count = await this.movePlayers(); - } catch (error) { - this.error(error); - } finally { - this.destroy(count); - } - } - - /** + private async clean(): Promise { + const move = this.manager.options.moveOnDisconnect; + if (!move) return this.destroy(); + let count = 0; + try { + count = await this.movePlayers(); + } catch (error) { + this.error(error); + } finally { + this.destroy(count); + } + } + + /** * Reconnect to Lavalink * @internal */ - private async reconnect(): Promise { - if (this.state === State.RECONNECTING) return; - if (this.state !== State.DISCONNECTED) this.destroy(); - this.state = State.RECONNECTING; - this.reconnects++; - this.emit('reconnecting', this.manager.options.reconnectTries - this.reconnects, this.manager.options.reconnectInterval); - this.emit('debug', `[Socket] -> [${this.name}] : Reconnecting in ${this.manager.options.reconnectInterval} seconds. ${this.manager.options.reconnectTries - this.reconnects} tries left`); - await wait(this.manager.options.reconnectInterval * 1000); - this.connect(); - } - - /** + private async reconnect(): Promise { + if (this.state === State.RECONNECTING) return; + if (this.state !== State.DISCONNECTED) this.destroy(); + this.state = State.RECONNECTING; + this.reconnects++; + this.emit('reconnecting', this.manager.options.reconnectTries - this.reconnects, this.manager.options.reconnectInterval); + this.emit('debug', `[Socket] -> [${this.name}] : Reconnecting in ${this.manager.options.reconnectInterval} seconds. ${this.manager.options.reconnectTries - this.reconnects} tries left`); + await wait(this.manager.options.reconnectInterval * 1000); + this.connect(); + } + + /** * Tries to resume the players internally * @internal */ - private async resumePlayers(): Promise { - const playersWithData = []; - const playersWithoutData = []; - - for (const player of this.manager.players.values()) { - const serverUpdate = this.manager.connections.get(player.guildId)?.serverUpdate; - if (serverUpdate) - playersWithData.push(player); - else - playersWithoutData.push(player); - } - - await Promise.allSettled([ - ...playersWithData.map(player => player.resume()), - ...playersWithoutData.map(player => this.manager.leaveVoiceChannel(player.guildId)) - ]); - } - - /** + private async resumePlayers(): Promise { + const playersWithData = []; + const playersWithoutData = []; + + for (const player of this.manager.players.values()) { + const serverUpdate = this.manager.connections.get(player.guildId)?.serverUpdate; + if (serverUpdate) + playersWithData.push(player); + else + playersWithoutData.push(player); + } + + await Promise.allSettled([ + ...playersWithData.map(player => player.resume()), + ...playersWithoutData.map(player => this.manager.leaveVoiceChannel(player.guildId)) + ]); + } + + /** * Tries to move the players to another node * @internal */ - private async movePlayers(): Promise { - const players = [ ...this.manager.players.values() ]; - const data = await Promise.allSettled(players.map(player => player.move())); - return data.filter(results => results.status === 'fulfilled').length; - } + private async movePlayers(): Promise { + const players = [ ...this.manager.players.values() ]; + const data = await Promise.allSettled(players.map(player => player.move())); + return data.filter(results => results.status === 'fulfilled').length; + } } diff --git a/src/node/Rest.ts b/src/node/Rest.ts index 0ef26a98..6a5bcd98 100644 --- a/src/node/Rest.ts +++ b/src/node/Rest.ts @@ -6,181 +6,181 @@ import { FilterOptions } from '../guild/Player'; export type Severity = 'common' | 'suspicious' | 'fault'; export enum LoadType { - TRACK = 'track', - PLAYLIST = 'playlist', - SEARCH = 'search', - EMPTY = 'empty', - ERROR = 'error' + TRACK = 'track', + PLAYLIST = 'playlist', + SEARCH = 'search', + EMPTY = 'empty', + ERROR = 'error' } export interface Track { - encoded: string; - info: { - identifier: string; - isSeekable: boolean; - author: string; - length: number; - isStream: boolean; - position: number; - title: string; - uri?: string; - artworkUrl?: string; - isrc?: string; - sourceName: string; - } - pluginInfo: unknown; + encoded: string; + info: { + identifier: string; + isSeekable: boolean; + author: string; + length: number; + isStream: boolean; + position: number; + title: string; + uri?: string; + artworkUrl?: string; + isrc?: string; + sourceName: string; + }; + pluginInfo: unknown; } export interface Playlist { - encoded: string; - info: { - name: string; - selectedTrack: number; - } - pluginInfo: unknown; - tracks: Track[]; + encoded: string; + info: { + name: string; + selectedTrack: number; + }; + pluginInfo: unknown; + tracks: Track[]; } export interface Exception { - message: string; - severity: Severity; - cause: string; + message: string; + severity: Severity; + cause: string; } export interface TrackResult { - loadType: LoadType.TRACK, - data: Track + loadType: LoadType.TRACK; + data: Track; } export interface PlaylistResult { - loadType: LoadType.PLAYLIST, - data: Playlist + loadType: LoadType.PLAYLIST; + data: Playlist; } export interface SearchResult { - loadType: LoadType.SEARCH, - data: Track[] + loadType: LoadType.SEARCH; + data: Track[]; } export interface EmptyResult { - loadType: LoadType.EMPTY, - data: {} + loadType: LoadType.EMPTY; + data: Record; } export interface ErrorResult { - loadType: LoadType.ERROR, - data: Exception + loadType: LoadType.ERROR; + data: Exception; } export type LavalinkResponse = TrackResult | PlaylistResult | SearchResult | EmptyResult | ErrorResult; export interface Address { - address: string; - failingTimestamp: number; - failingTime: string; + address: string; + failingTimestamp: number; + failingTime: string; } export interface RoutePlanner { - class: null | 'RotatingIpRoutePlanner' | 'NanoIpRoutePlanner' | 'RotatingNanoIpRoutePlanner' | 'BalancingIpRoutePlanner'; - details: null | { - ipBlock: { - type: string; - size: string; - }; - failingAddresses: Address[]; - rotateIndex: string; - ipIndex: string; - currentAddress: string; - blockIndex: string; - currentAddressIndex: string; - }; + class: null | 'RotatingIpRoutePlanner' | 'NanoIpRoutePlanner' | 'RotatingNanoIpRoutePlanner' | 'BalancingIpRoutePlanner'; + details: null | { + ipBlock: { + type: string; + size: string; + }; + failingAddresses: Address[]; + rotateIndex: string; + ipIndex: string; + currentAddress: string; + blockIndex: string; + currentAddressIndex: string; + }; } export interface LavalinkPlayerVoice { - token: string; - endpoint: string; - sessionId: string; - connected?: boolean; - ping?: number + token: string; + endpoint: string; + sessionId: string; + connected?: boolean; + ping?: number; } -export interface LavalinkPlayerVoiceOptions extends Omit {} +export type LavalinkPlayerVoiceOptions = Omit; export interface LavalinkPlayer { - guildId: string, - track?: Track, - volume: number; - paused: boolean; - voice: LavalinkPlayerVoice - filters: FilterOptions + guildId: string; + track?: Track; + volume: number; + paused: boolean; + voice: LavalinkPlayerVoice; + filters: FilterOptions; } export interface UpdatePlayerTrackOptions { - encoded?: string|null; - identifier?: string; - userData?: unknown; + encoded?: string | null; + identifier?: string; + userData?: unknown; } export interface UpdatePlayerOptions { - track?: UpdatePlayerTrackOptions; - position?: number; - endTime?: number; - volume?: number; - paused?: boolean; - filters?: FilterOptions; - voice?: LavalinkPlayerVoiceOptions; + track?: UpdatePlayerTrackOptions; + position?: number; + endTime?: number; + volume?: number; + paused?: boolean; + filters?: FilterOptions; + voice?: LavalinkPlayerVoiceOptions; } export interface UpdatePlayerInfo { - guildId: string; - playerOptions: UpdatePlayerOptions; - noReplace?: boolean; + guildId: string; + playerOptions: UpdatePlayerOptions; + noReplace?: boolean; } export interface SessionInfo { - resumingKey?: string; - timeout: number; + resumingKey?: string; + timeout: number; } interface FetchOptions { - endpoint: string; - options: { - headers?: Record; - params?: Record; - method?: string; - body?: Record; - [key: string]: unknown; - }; + endpoint: string; + options: { + headers?: Record; + params?: Record; + method?: string; + body?: Record; + [key: string]: unknown; + }; } interface FinalFetchOptions { - method: string; - headers: Record; - signal: AbortSignal; - body?: string; + method: string; + headers: Record; + signal: AbortSignal; + body?: string; } /** * Wrapper around Lavalink REST API */ export class Rest { - /** + /** * Node that initialized this instance */ - protected readonly node: Node; - /** + protected readonly node: Node; + /** * URL of Lavalink */ - protected readonly url: string; - /** + protected readonly url: string; + /** * Credentials to access Lavalink */ - protected readonly auth: string; - /** + protected readonly auth: string; + /** * Rest version to use */ - protected readonly version: string; - /** + protected readonly version: string; + /** * @param node An instance of Node * @param options The options to initialize this rest class * @param options.name Name of this node @@ -189,250 +189,250 @@ export class Rest { * @param options.secure Weather to use secure protocols or not * @param options.group Group of this node */ - constructor(node: Node, options: NodeOption) { - this.node = node; - this.url = `${options.secure ? 'https' : 'http'}://${options.url}`; - this.version = `/v${Versions.REST_VERSION}`; - this.auth = options.auth; - } - - protected get sessionId(): string { - return this.node.sessionId!; - } - - /** + constructor(node: Node, options: NodeOption) { + this.node = node; + this.url = `${options.secure ? 'https' : 'http'}://${options.url}`; + this.version = `/v${Versions.REST_VERSION}`; + this.auth = options.auth; + } + + protected get sessionId(): string { + return this.node.sessionId!; + } + + /** * Resolve a track * @param identifier Track ID * @returns A promise that resolves to a Lavalink response */ - public resolve(identifier: string): Promise { - const options = { - endpoint: '/loadtracks', - options: { params: { identifier }} - }; - return this.fetch(options); - } - - /** + public resolve(identifier: string): Promise { + const options = { + endpoint: '/loadtracks', + options: { params: { identifier }} + }; + return this.fetch(options); + } + + /** * Decode a track * @param track Encoded track * @returns Promise that resolves to a track */ - public decode(track: string): Promise { - const options = { - endpoint: '/decodetrack', - options: { params: { track }} - }; - return this.fetch(options); - } - - /** + public decode(track: string): Promise { + const options = { + endpoint: '/decodetrack', + options: { params: { track }} + }; + return this.fetch(options); + } + + /** * Gets all the player with the specified sessionId * @returns Promise that resolves to an array of Lavalink players */ - public async getPlayers(): Promise { - const options = { - endpoint: `/sessions/${this.sessionId}/players`, - options: {} - }; - return await this.fetch(options) ?? []; - } - - /** + public async getPlayers(): Promise { + const options = { + endpoint: `/sessions/${this.sessionId}/players`, + options: {} + }; + return await this.fetch(options) ?? []; + } + + /** * Gets the player with the specified guildId * @returns Promise that resolves to a Lavalink player */ - public getPlayer(guildId: string): Promise { - const options = { - endpoint: `/sessions/${this.sessionId}/players/${guildId}`, - options: {} - }; - return this.fetch(options); - } - - /** + public getPlayer(guildId: string): Promise { + const options = { + endpoint: `/sessions/${this.sessionId}/players/${guildId}`, + options: {} + }; + return this.fetch(options); + } + + /** * Updates a Lavalink player * @param data SessionId from Discord * @returns Promise that resolves to a Lavalink player */ - public updatePlayer(data: UpdatePlayerInfo): Promise { - const options = { - endpoint: `/sessions/${this.sessionId}/players/${data.guildId}`, - options: { - method: 'PATCH', - params: { noReplace: data.noReplace?.toString() || 'false' }, - headers: { 'Content-Type': 'application/json' }, - body: data.playerOptions as Record - } - }; - return this.fetch(options); - } - - /** + public updatePlayer(data: UpdatePlayerInfo): Promise { + const options = { + endpoint: `/sessions/${this.sessionId}/players/${data.guildId}`, + options: { + method: 'PATCH', + params: { noReplace: data.noReplace?.toString() ?? 'false' }, + headers: { 'Content-Type': 'application/json' }, + body: data.playerOptions as Record + } + }; + return this.fetch(options); + } + + /** * Deletes a Lavalink player * @param guildId guildId where this player is */ - public async destroyPlayer(guildId: string): Promise { - const options = { - endpoint: `/sessions/${this.sessionId}/players/${guildId}`, - options: { method: 'DELETE' } - }; - await this.fetch(options); - } - - /** + public async destroyPlayer(guildId: string): Promise { + const options = { + endpoint: `/sessions/${this.sessionId}/players/${guildId}`, + options: { method: 'DELETE' } + }; + await this.fetch(options); + } + + /** * Updates the session with a resume boolean and timeout * @param resuming Whether resuming is enabled for this session or not * @param timeout Timeout to wait for resuming * @returns Promise that resolves to a Lavalink player */ - public updateSession(resuming?: boolean, timeout?: number): Promise { - const options = { - endpoint: `/sessions/${this.sessionId}`, - options: { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: { resuming, timeout } - } - }; - return this.fetch(options); - } - - /** + public updateSession(resuming?: boolean, timeout?: number): Promise { + const options = { + endpoint: `/sessions/${this.sessionId}`, + options: { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: { resuming, timeout } + } + }; + return this.fetch(options); + } + + /** * Gets the status of this node * @returns Promise that resolves to a node stats response */ - public stats(): Promise { - const options = { - endpoint: '/stats', - options: {} - }; - return this.fetch(options); - } - - /** + public stats(): Promise { + const options = { + endpoint: '/stats', + options: {} + }; + return this.fetch(options); + } + + /** * Get routeplanner status from Lavalink * @returns Promise that resolves to a routeplanner response */ - public getRoutePlannerStatus(): Promise { - const options = { - endpoint: '/routeplanner/status', - options: {} - }; - return this.fetch(options); - } - - /** + public getRoutePlannerStatus(): Promise { + const options = { + endpoint: '/routeplanner/status', + options: {} + }; + return this.fetch(options); + } + + /** * Release blacklisted IP address into pool of IPs * @param address IP address */ - public async unmarkFailedAddress(address: string): Promise { - const options = { - endpoint: '/routeplanner/free/address', - options: { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: { address } - } - }; - await this.fetch(options); - } - - /** + public async unmarkFailedAddress(address: string): Promise { + const options = { + endpoint: '/routeplanner/free/address', + options: { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: { address } + } + }; + await this.fetch(options); + } + + /** * Get Lavalink info */ - public getLavalinkInfo(): Promise { - const options = { - endpoint: '/info', - options: { - headers: { 'Content-Type': 'application/json' } - } - }; - return this.fetch(options); - } - - /** + public getLavalinkInfo(): Promise { + const options = { + endpoint: '/info', + options: { + headers: { 'Content-Type': 'application/json' } + } + }; + return this.fetch(options); + } + + /** * Make a request to Lavalink * @param fetchOptions.endpoint Lavalink endpoint * @param fetchOptions.options Options passed to fetch * @throws `RestError` when encountering a Lavalink error response * @internal */ - protected async fetch(fetchOptions: FetchOptions) { - const { endpoint, options } = fetchOptions; - let headers = { - 'Authorization': this.auth, - 'User-Agent': this.node.manager.options.userAgent - }; - - if (options.headers) headers = { ...headers, ...options.headers }; - - const url = new URL(`${this.url}${this.version}${endpoint}`); - - if (options.params) url.search = new URLSearchParams(options.params).toString(); - - const abortController = new AbortController(); - const timeout = setTimeout(() => abortController.abort(), this.node.manager.options.restTimeout * 1000); - - const method = options.method?.toUpperCase() || 'GET'; - - const finalFetchOptions: FinalFetchOptions = { - method, - headers, - signal: abortController.signal - }; - - if (![ 'GET', 'HEAD' ].includes(method) && options.body) - finalFetchOptions.body = JSON.stringify(options.body); - - const request = await fetch(url.toString(), finalFetchOptions) - .finally(() => clearTimeout(timeout)); - - if (!request.ok) { - const response = await request - .json() - .catch(() => null) as LavalinkRestError | null; - throw new RestError(response ?? { - timestamp: Date.now(), - status: request.status, - error: 'Unknown Error', - message: 'Unexpected error response from Lavalink server', - path: endpoint, - }); - } - try { - return await request.json() as T; - } catch { - return; - } - } + protected async fetch(fetchOptions: FetchOptions) { + const { endpoint, options } = fetchOptions; + let headers = { + 'Authorization': this.auth, + 'User-Agent': this.node.manager.options.userAgent + }; + + if (options.headers) headers = { ...headers, ...options.headers }; + + const url = new URL(`${this.url}${this.version}${endpoint}`); + + if (options.params) url.search = new URLSearchParams(options.params).toString(); + + const abortController = new AbortController(); + const timeout = setTimeout(() => abortController.abort(), this.node.manager.options.restTimeout * 1000); + + const method = options.method?.toUpperCase() ?? 'GET'; + + const finalFetchOptions: FinalFetchOptions = { + method, + headers, + signal: abortController.signal + }; + + if (![ 'GET', 'HEAD' ].includes(method) && options.body) + finalFetchOptions.body = JSON.stringify(options.body); + + const request = await fetch(url.toString(), finalFetchOptions) + .finally(() => clearTimeout(timeout)); + + if (!request.ok) { + const response = await request + .json() + .catch(() => null) as LavalinkRestError | null; + throw new RestError(response ?? { + timestamp: Date.now(), + status: request.status, + error: 'Unknown Error', + message: 'Unexpected error response from Lavalink server', + path: endpoint + }); + } + try { + return await request.json() as T; + } catch { + return; + } + } } interface LavalinkRestError { - timestamp: number; - status: number; - error: string; - trace?: string; - message: string; - path: string; + timestamp: number; + status: number; + error: string; + trace?: string; + message: string; + path: string; } export class RestError extends Error { - public timestamp: number; - public status: number; - public error: string; - public trace?: string; - public path: string; - - constructor({ timestamp, status, error, trace, message, path }: LavalinkRestError) { - super(`Rest request failed with response code: ${status}${message ? ` | message: ${message}` : ''}`); - this.name = 'RestError'; - this.timestamp = timestamp; - this.status = status; - this.error = error; - this.trace = trace; - this.message = message; - this.path = path; - Object.setPrototypeOf(this, new.target.prototype); - } + public timestamp: number; + public status: number; + public error: string; + public trace?: string; + public path: string; + + constructor({ timestamp, status, error, trace, message, path }: LavalinkRestError) { + super(`Rest request failed with response code: ${status}${message ? ` | message: ${message}` : ''}`); + this.name = 'RestError'; + this.timestamp = timestamp; + this.status = status; + this.error = error; + this.trace = trace; + this.message = message; + this.path = path; + Object.setPrototypeOf(this, new.target.prototype); + } } diff --git a/tsconfig.json b/tsconfig.json index d4e9c8fb..779a41b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowJs": true, "module": "node16", "moduleResolution": "node16", "outDir": "dist",