shoukaku-bun 4.2.0-b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/index.ts +9 -0
- package/package.json +56 -0
- package/src/Constants.ts +55 -0
- package/src/Shoukaku.ts +295 -0
- package/src/Utils.ts +58 -0
- package/src/connectors/Connector.ts +49 -0
- package/src/connectors/README.md +42 -0
- package/src/connectors/libs/DiscordJS.ts +21 -0
- package/src/connectors/libs/Eris.ts +21 -0
- package/src/connectors/libs/OceanicJS.ts +21 -0
- package/src/connectors/libs/Seyfert.ts +26 -0
- package/src/connectors/libs/index.ts +4 -0
- package/src/guild/Connection.ts +248 -0
- package/src/guild/Player.ts +543 -0
- package/src/node/Node.ts +442 -0
- package/src/node/Rest.ts +433 -0
package/src/node/Node.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
|
|
2
|
+
import { OpCodes, ShoukakuClientInfo, State, Versions } from '../Constants';
|
|
3
|
+
import type {
|
|
4
|
+
PlayerUpdate,
|
|
5
|
+
TrackEndEvent,
|
|
6
|
+
TrackExceptionEvent,
|
|
7
|
+
TrackStartEvent,
|
|
8
|
+
TrackStuckEvent,
|
|
9
|
+
WebSocketClosedEvent
|
|
10
|
+
} from '../guild/Player';
|
|
11
|
+
import type { NodeOption, Shoukaku, ShoukakuEvents } from '../Shoukaku';
|
|
12
|
+
import { TypedEventEmitter, wait } from '../Utils';
|
|
13
|
+
import { Rest } from './Rest';
|
|
14
|
+
|
|
15
|
+
export interface Ready {
|
|
16
|
+
op: OpCodes.READY;
|
|
17
|
+
resumed: boolean;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NodeMemory {
|
|
22
|
+
reservable: number;
|
|
23
|
+
used: number;
|
|
24
|
+
free: number;
|
|
25
|
+
allocated: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NodeFrameStats {
|
|
29
|
+
sent: number;
|
|
30
|
+
deficit: number;
|
|
31
|
+
nulled: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface NodeCpu {
|
|
35
|
+
cores: number;
|
|
36
|
+
systemLoad: number;
|
|
37
|
+
lavalinkLoad: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Stats {
|
|
41
|
+
op: OpCodes.STATS;
|
|
42
|
+
players: number;
|
|
43
|
+
playingPlayers: number;
|
|
44
|
+
memory: NodeMemory;
|
|
45
|
+
frameStats: NodeFrameStats | null;
|
|
46
|
+
cpu: NodeCpu;
|
|
47
|
+
uptime: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface NodeInfoVersion {
|
|
51
|
+
semver: string;
|
|
52
|
+
major: number;
|
|
53
|
+
minor: number;
|
|
54
|
+
patch: number;
|
|
55
|
+
preRelease?: string;
|
|
56
|
+
build?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface NodeInfoGit {
|
|
60
|
+
branch: string;
|
|
61
|
+
commit: string;
|
|
62
|
+
commitTime: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface NodeInfoPlugin {
|
|
66
|
+
name: string;
|
|
67
|
+
version: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface NodeInfo {
|
|
71
|
+
version: NodeInfoVersion;
|
|
72
|
+
buildTime: number;
|
|
73
|
+
git: NodeInfoGit;
|
|
74
|
+
jvm: string;
|
|
75
|
+
lavaplayer: string;
|
|
76
|
+
sourceManagers: string[];
|
|
77
|
+
filters: string[];
|
|
78
|
+
plugins: NodeInfoPlugin[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ResumableHeaders {
|
|
82
|
+
[key: string]: string;
|
|
83
|
+
'Client-Name': string;
|
|
84
|
+
'User-Agent': string;
|
|
85
|
+
'Authorization': string;
|
|
86
|
+
'User-Id': string;
|
|
87
|
+
'Session-Id': string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type NonResumableHeaders = Omit<ResumableHeaders, 'Session-Id'>;
|
|
91
|
+
|
|
92
|
+
export type NodeEvents = {
|
|
93
|
+
[K in keyof ShoukakuEvents]: ShoukakuEvents[K] extends [unknown, ...infer R] ? R : never;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Represents a Lavalink node
|
|
98
|
+
*/
|
|
99
|
+
export class Node extends TypedEventEmitter<NodeEvents> {
|
|
100
|
+
/**
|
|
101
|
+
* Shoukaku class
|
|
102
|
+
*/
|
|
103
|
+
public readonly manager: Shoukaku;
|
|
104
|
+
/**
|
|
105
|
+
* Lavalink rest API
|
|
106
|
+
*/
|
|
107
|
+
public readonly rest: Rest;
|
|
108
|
+
/**
|
|
109
|
+
* Name of this node
|
|
110
|
+
*/
|
|
111
|
+
public readonly name: string;
|
|
112
|
+
/**
|
|
113
|
+
* Group in which this node is contained
|
|
114
|
+
*/
|
|
115
|
+
public readonly group?: string;
|
|
116
|
+
/**
|
|
117
|
+
* URL of Lavalink
|
|
118
|
+
*/
|
|
119
|
+
private readonly url: string;
|
|
120
|
+
/**
|
|
121
|
+
* Credentials to access Lavalink
|
|
122
|
+
*/
|
|
123
|
+
private readonly auth: string;
|
|
124
|
+
/**
|
|
125
|
+
* The state of this connection
|
|
126
|
+
*/
|
|
127
|
+
public state: State;
|
|
128
|
+
/**
|
|
129
|
+
* The number of reconnects to Lavalink
|
|
130
|
+
*/
|
|
131
|
+
public reconnects: number;
|
|
132
|
+
/**
|
|
133
|
+
* Statistics from Lavalink
|
|
134
|
+
*/
|
|
135
|
+
public stats: Stats | null;
|
|
136
|
+
/**
|
|
137
|
+
* Information about lavalink node
|
|
138
|
+
*/
|
|
139
|
+
public info: NodeInfo | null;
|
|
140
|
+
/**
|
|
141
|
+
* Websocket instance
|
|
142
|
+
*/
|
|
143
|
+
public ws: WebSocket | null;
|
|
144
|
+
/**
|
|
145
|
+
* SessionId of this Lavalink connection (not to be confused with Discord SessionId)
|
|
146
|
+
*/
|
|
147
|
+
public sessionId: string | null;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param manager Shoukaku instance
|
|
151
|
+
* @param options Options on creating this node
|
|
152
|
+
* @param options.name Name of this node
|
|
153
|
+
* @param options.url URL of Lavalink
|
|
154
|
+
* @param options.auth Credentials to access Lavalink
|
|
155
|
+
* @param options.secure Whether to use secure protocols or not
|
|
156
|
+
* @param options.group Group of this node
|
|
157
|
+
*/
|
|
158
|
+
constructor(manager: Shoukaku, options: NodeOption) {
|
|
159
|
+
super();
|
|
160
|
+
this.manager = manager;
|
|
161
|
+
this.rest = new (this.manager.options.structures.rest ?? Rest)(this, options);
|
|
162
|
+
this.name = options.name;
|
|
163
|
+
this.group = options.group;
|
|
164
|
+
this.auth = options.auth;
|
|
165
|
+
this.url = `${options.secure ? 'wss' : 'ws'}://${options.url}/v${Versions.WEBSOCKET_VERSION}/websocket`;
|
|
166
|
+
this.state = State.DISCONNECTED;
|
|
167
|
+
this.reconnects = 0;
|
|
168
|
+
this.stats = null;
|
|
169
|
+
this.info = null;
|
|
170
|
+
this.ws = null;
|
|
171
|
+
this.sessionId = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Penalties for load balancing
|
|
176
|
+
* @returns Penalty score
|
|
177
|
+
* @internal @readonly
|
|
178
|
+
*/
|
|
179
|
+
get penalties(): number {
|
|
180
|
+
let penalties = 0;
|
|
181
|
+
if (!this.stats) return penalties;
|
|
182
|
+
|
|
183
|
+
penalties += this.stats.players;
|
|
184
|
+
penalties += Math.round(Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10);
|
|
185
|
+
|
|
186
|
+
if (this.stats.frameStats) {
|
|
187
|
+
penalties += this.stats.frameStats.deficit;
|
|
188
|
+
penalties += this.stats.frameStats.nulled * 2;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return penalties;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Connect to Lavalink
|
|
196
|
+
*/
|
|
197
|
+
public async connect(): Promise<void> {
|
|
198
|
+
if (!this.manager.id)
|
|
199
|
+
throw new Error('UserId missing, probably your connector is misconfigured?');
|
|
200
|
+
|
|
201
|
+
if (this.state === State.CONNECTED || this.state === State.CONNECTING)
|
|
202
|
+
return;
|
|
203
|
+
|
|
204
|
+
this.cleanupWebsocket();
|
|
205
|
+
|
|
206
|
+
this.state = State.CONNECTING;
|
|
207
|
+
|
|
208
|
+
const headers: NonResumableHeaders | ResumableHeaders = {
|
|
209
|
+
'Client-Name': ShoukakuClientInfo,
|
|
210
|
+
'User-Agent': this.manager.options.userAgent,
|
|
211
|
+
'Authorization': this.auth,
|
|
212
|
+
'User-Id': this.manager.id
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (this.sessionId && this.manager.options.resume) {
|
|
216
|
+
headers['Session-Id'] = this.sessionId;
|
|
217
|
+
this.emit('debug', `[Socket] -> [${this.name}] : Session-Id is present, attempting to resume`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.emit('debug', `[Socket] -> [${this.name}] : Connecting to ${this.url} ...`);
|
|
221
|
+
|
|
222
|
+
const createConnection = () => {
|
|
223
|
+
const url = new URL(this.url);
|
|
224
|
+
|
|
225
|
+
const server = new WebSocket(url.toString(), { headers });
|
|
226
|
+
|
|
227
|
+
const cleanup = () => {
|
|
228
|
+
server.onopen = null;
|
|
229
|
+
server.onclose = null;
|
|
230
|
+
server.onerror = null;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return new Promise<WebSocket>((resolve, reject) => {
|
|
234
|
+
server.onopen = () => {
|
|
235
|
+
cleanup();
|
|
236
|
+
resolve(server);
|
|
237
|
+
};
|
|
238
|
+
server.onclose = () => {
|
|
239
|
+
cleanup();
|
|
240
|
+
reject(new Error('Websocket closed before a connection was established'));
|
|
241
|
+
};
|
|
242
|
+
server.onerror = (error) => {
|
|
243
|
+
cleanup();
|
|
244
|
+
reject(new Error(`Websocket failed to connect due to: ${(error as any).message || 'Unknown error'}`));
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
let connectError: Error | undefined;
|
|
250
|
+
|
|
251
|
+
for (this.reconnects = 0; this.reconnects < this.manager.options.reconnectTries; this.reconnects++) {
|
|
252
|
+
try {
|
|
253
|
+
this.ws = await createConnection();
|
|
254
|
+
break;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
this.emit('reconnecting', this.manager.options.reconnectTries - this.reconnects, this.manager.options.reconnectInterval);
|
|
257
|
+
this.emit('debug', `[Socket] -> [${this.name}] : Reconnecting in ${this.manager.options.reconnectInterval} seconds. ${this.manager.options.reconnectTries - this.reconnects} tries left`);
|
|
258
|
+
connectError = error as Error;
|
|
259
|
+
await wait(this.manager.options.reconnectInterval * 1000);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (connectError) {
|
|
264
|
+
this.state = State.DISCONNECTED;
|
|
265
|
+
this.cleanupWebsocket();
|
|
266
|
+
let count = 0;
|
|
267
|
+
if (this.manager.options.moveOnDisconnect) {
|
|
268
|
+
count = await this.movePlayers();
|
|
269
|
+
}
|
|
270
|
+
this.emit('disconnect', count);
|
|
271
|
+
// Should I throw or not? :confusion:
|
|
272
|
+
throw connectError;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.ws!.onclose = event => void this.close(event.code, event.reason);
|
|
276
|
+
this.ws!.onerror = event => this.error(new Error(`WebSocket connection error: ${(event as any).message || 'Unknown error'}`));
|
|
277
|
+
this.ws!.onmessage = event => void this.message(event.data).catch(error => this.error(error as Error));
|
|
278
|
+
this.open();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Disconnect from Lavalink
|
|
283
|
+
* @param code Status code
|
|
284
|
+
* @param reason Reason for disconnect
|
|
285
|
+
*/
|
|
286
|
+
public disconnect(code: number, reason?: string): void {
|
|
287
|
+
if (this.state !== State.CONNECTED && this.state !== State.CONNECTING) return;
|
|
288
|
+
|
|
289
|
+
this.state = State.DISCONNECTING;
|
|
290
|
+
|
|
291
|
+
if (this.ws)
|
|
292
|
+
this.ws.close(code, reason);
|
|
293
|
+
else
|
|
294
|
+
void this.close(1000, reason ?? 'Unknown Reason');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Handle connection open event from Lavalink
|
|
299
|
+
* @param response Response from Lavalink
|
|
300
|
+
* @internal
|
|
301
|
+
*/
|
|
302
|
+
/**
|
|
303
|
+
* Handle connection open event from Lavalink
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
private open(): void {
|
|
307
|
+
this.reconnects = 0;
|
|
308
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
309
|
+
this.emit('debug', `[Socket] <-> [${this.name}] : Connection Handshake Done => ${this.url}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Handle message from Lavalink
|
|
314
|
+
* @param message JSON message
|
|
315
|
+
* @internal
|
|
316
|
+
*/
|
|
317
|
+
private async message(message: unknown): Promise<void> {
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
319
|
+
const json: Ready | Stats | PlayerUpdate | TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent = JSON.parse(message as string);
|
|
320
|
+
if (!json) return;
|
|
321
|
+
this.emit('raw', json);
|
|
322
|
+
switch (json.op) {
|
|
323
|
+
case OpCodes.STATS:
|
|
324
|
+
this.emit('debug', `[Socket] <- [${this.name}] : Node Status Update | Server Load: ${this.penalties}`);
|
|
325
|
+
this.stats = json;
|
|
326
|
+
break;
|
|
327
|
+
case OpCodes.READY: {
|
|
328
|
+
this.state = State.CONNECTED;
|
|
329
|
+
|
|
330
|
+
if (!json.sessionId) {
|
|
331
|
+
this.emit('debug', `[Socket] -> [${this.name}] : No session id found from ready op? disconnecting and reconnecting to avoid issues`);
|
|
332
|
+
return this.disconnect(1000);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.sessionId = json.sessionId;
|
|
336
|
+
|
|
337
|
+
const players = [ ...this.manager.players.values() ].filter(player => player.node.name === this.name);
|
|
338
|
+
|
|
339
|
+
let resumedByLibrary = false;
|
|
340
|
+
if (!json.resumed && Boolean(players.length && this.manager.options.resumeByLibrary)) {
|
|
341
|
+
try {
|
|
342
|
+
await this.resumePlayers();
|
|
343
|
+
resumedByLibrary = true;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
this.error(error as Error);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this.emit('debug', `[Socket] -> [${this.name}] : Lavalink is ready to communicate !`);
|
|
350
|
+
this.emit('ready', json.resumed, resumedByLibrary);
|
|
351
|
+
|
|
352
|
+
if (this.manager.options.resume) {
|
|
353
|
+
await this.rest.updateSession(this.manager.options.resume, this.manager.options.resumeTimeout);
|
|
354
|
+
this.emit('debug', `[Socket] -> [${this.name}] : Resuming configured for this Session Id: ${this.sessionId}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case OpCodes.EVENT:
|
|
360
|
+
case OpCodes.PLAYER_UPDATE: {
|
|
361
|
+
const player = this.manager.players.get(json.guildId);
|
|
362
|
+
if (!player) return;
|
|
363
|
+
if (json.op === OpCodes.EVENT)
|
|
364
|
+
player.onPlayerEvent(json);
|
|
365
|
+
else
|
|
366
|
+
player.onPlayerUpdate(json);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
default:
|
|
370
|
+
this.emit('debug', `[Player] -> [Node] : Unknown Message Op, Data => ${JSON.stringify(json)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Handle closed event from lavalink
|
|
376
|
+
* @param code Status close
|
|
377
|
+
* @param reason Reason for connection close
|
|
378
|
+
*/
|
|
379
|
+
private async close(code: number, reason: string): Promise<void> {
|
|
380
|
+
this.emit('close', code, String(reason));
|
|
381
|
+
this.emit('debug', `[Socket] <-/-> [${this.name}] : Connection Closed, Code: ${code || 'Unknown Code'}`);
|
|
382
|
+
|
|
383
|
+
this.state = State.DISCONNECTING;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await this.connect();
|
|
387
|
+
} catch (error) {
|
|
388
|
+
this.emit('error', error as Error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* To emit error events easily
|
|
394
|
+
* @param error error message
|
|
395
|
+
*/
|
|
396
|
+
public error(error: Error): void {
|
|
397
|
+
this.emit('error', error);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Tries to resume the players internally
|
|
402
|
+
* @internal
|
|
403
|
+
*/
|
|
404
|
+
private async resumePlayers(): Promise<void> {
|
|
405
|
+
const playersWithData = [];
|
|
406
|
+
const playersWithoutData = [];
|
|
407
|
+
|
|
408
|
+
for (const player of this.manager.players.values()) {
|
|
409
|
+
const serverUpdate = this.manager.connections.get(player.guildId)?.serverUpdate;
|
|
410
|
+
if (serverUpdate)
|
|
411
|
+
playersWithData.push(player);
|
|
412
|
+
else
|
|
413
|
+
playersWithoutData.push(player);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await Promise.allSettled([
|
|
417
|
+
...playersWithData.map(player => player.resume()),
|
|
418
|
+
...playersWithoutData.map(player => this.manager.leaveVoiceChannel(player.guildId))
|
|
419
|
+
]);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Tries to move the players to another node
|
|
424
|
+
* @internal
|
|
425
|
+
*/
|
|
426
|
+
private async movePlayers(): Promise<number> {
|
|
427
|
+
const players = [ ...this.manager.players.values() ];
|
|
428
|
+
const data = await Promise.allSettled(players.map(player => player.move()));
|
|
429
|
+
return data.filter(results => results.status === 'fulfilled').length;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private cleanupWebsocket(): void {
|
|
433
|
+
if (this.ws) {
|
|
434
|
+
this.ws.onopen = null;
|
|
435
|
+
this.ws.onclose = null;
|
|
436
|
+
this.ws.onerror = null;
|
|
437
|
+
this.ws.onmessage = null;
|
|
438
|
+
this.ws.close();
|
|
439
|
+
}
|
|
440
|
+
this.ws = null;
|
|
441
|
+
}
|
|
442
|
+
}
|