djs-selfbot-v13 3.7.32 → 3.7.34

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.
@@ -4,7 +4,8 @@ const EventEmitter = require('events');
4
4
  const { setTimeout, setInterval } = require('node:timers');
5
5
  const WebSocket = require('../../../WebSocket');
6
6
  const { Error } = require('../../../errors');
7
- const { Opcodes, VoiceOpcodes } = require('../../../util/Constants');
7
+ const { Opcodes, VoiceOpcodes, VoiceStatus } = require('../../../util/Constants');
8
+ const { DAVESession, getMaxProtocolVersion } = require('./DAVESession');
8
9
 
9
10
  /**
10
11
  * Represents a Voice Connection's WebSocket.
@@ -29,6 +30,7 @@ class VoiceWebSocket extends EventEmitter {
29
30
  this._sequenceNumber = -1;
30
31
 
31
32
  this.dead = false;
33
+ this._identified = false;
32
34
  this.connection.on('closing', this.shutdown.bind(this));
33
35
  }
34
36
 
@@ -57,6 +59,7 @@ class VoiceWebSocket extends EventEmitter {
57
59
  this.ws = null;
58
60
  }
59
61
  this.clearHeartbeat();
62
+ this._identified = false;
60
63
  }
61
64
 
62
65
  /**
@@ -67,7 +70,7 @@ class VoiceWebSocket extends EventEmitter {
67
70
  if (this.dead) return;
68
71
  if (this.ws) this.reset();
69
72
  if (this.attempts >= 5) {
70
- this.emit('debug', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts));
73
+ this.emit('error', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts));
71
74
  return;
72
75
  }
73
76
 
@@ -87,13 +90,14 @@ class VoiceWebSocket extends EventEmitter {
87
90
 
88
91
  /**
89
92
  * Sends data to the WebSocket if it is open.
90
- * @param {string} data The data to send to the WebSocket
91
- * @returns {Promise<string>}
93
+ * @param {string|Buffer} data The data to send to the WebSocket
94
+ * @returns {Promise<string|Buffer>}
92
95
  */
93
96
  send(data) {
94
- this.emit('debug', `[WS] >> ${data}`);
97
+ const preview = typeof data === 'string' ? data : `[bin] ${data.byteLength} bytes`;
98
+ this.emit('debug', `[WS] >> ${preview}`);
95
99
  return new Promise((resolve, reject) => {
96
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data);
100
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', preview);
97
101
  this.ws.send(data, null, error => {
98
102
  if (error) reject(error);
99
103
  else resolve(data);
@@ -111,23 +115,48 @@ class VoiceWebSocket extends EventEmitter {
111
115
  return this.send(packet);
112
116
  }
113
117
 
118
+ /**
119
+ * Sends a binary message over the WebSocket.
120
+ * @param {number} opcode The opcode to use
121
+ * @param {Buffer} payload The payload to send
122
+ * @returns {Promise<Buffer>}
123
+ */
124
+ sendBinaryMessage(opcode, payload) {
125
+ const message = Buffer.concat([Buffer.from([opcode]), payload]);
126
+ return this.send(message);
127
+ }
128
+
114
129
  /**
115
130
  * Called whenever the WebSocket opens.
116
131
  */
117
132
  onOpen() {
118
133
  this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`);
119
- this.sendPacket({
120
- op: Opcodes.DISPATCH,
121
- d: {
122
- server_id: this.connection.serverId || this.connection.channel.guild?.id || this.connection.channel.id,
123
- user_id: this.client.user.id,
124
- token: this.connection.authentication.token,
125
- session_id: this.connection.authentication.sessionId,
126
- streams: [{ type: 'screen', rid: '100', quality: 100 }],
127
- video: true,
128
- },
129
- }).catch(() => {
130
- this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED'));
134
+ }
135
+
136
+ sendIdentify() {
137
+ if (this._identified) return Promise.resolve();
138
+ this._identified = true;
139
+
140
+ const isStream = this.connection.constructor.name === 'StreamConnection';
141
+ const sessionId = this.connection.authentication.sessionId;
142
+ const maxDave = getMaxProtocolVersion();
143
+ const data = {
144
+ server_id: this.connection.serverId || this.connection.channel.guild?.id || this.connection.channel.id,
145
+ user_id: this.client.user.id,
146
+ token: this.connection.authentication.token,
147
+ session_id: sessionId,
148
+ };
149
+ if (maxDave > 0) data.max_dave_protocol_version = maxDave;
150
+
151
+ if (isStream) {
152
+ data.channel_id = this.connection.channel.id;
153
+ data.streams = [{ type: 'screen', rid: '100', quality: 100 }];
154
+ data.video = true;
155
+ }
156
+
157
+ return this.sendPacket({
158
+ op: VoiceOpcodes.IDENTIFY,
159
+ d: data,
131
160
  });
132
161
  }
133
162
 
@@ -138,18 +167,105 @@ class VoiceWebSocket extends EventEmitter {
138
167
  */
139
168
  onMessage(event) {
140
169
  try {
141
- return this.onPacket(WebSocket.unpack(event.data, 'json'));
170
+ const { data } = event;
171
+ if (data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
172
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
173
+ const seq = buffer.readUInt16BE(0);
174
+ const op = buffer.readUInt8(2);
175
+ const payload = buffer.subarray(3);
176
+ this._sequenceNumber = seq;
177
+ this.emit('debug', `[WS] << [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes`);
178
+ return this.onBinaryMessage({ op, seq, payload });
179
+ }
180
+ return this.onPacket(WebSocket.unpack(data, 'json'));
142
181
  } catch (error) {
143
182
  return this.onError(error);
144
183
  }
145
184
  }
146
185
 
186
+ usesSharedDaveSession() {
187
+ return (
188
+ this.connection.constructor.name === 'StreamConnection' &&
189
+ this.connection.voiceConnection?.dave === this.connection.dave
190
+ );
191
+ }
192
+
193
+ onBinaryMessage(message) {
194
+ const { dave } = this.connection;
195
+ if (!dave) return;
196
+ if (this.usesSharedDaveSession()) {
197
+ this.emit('debug', `[WS] << [bin] opcode ${message.op} ignored (shared MLS session)`);
198
+ return;
199
+ }
200
+
201
+ switch (message.op) {
202
+ case VoiceOpcodes.DAVE_MLS_EXTERNAL_SENDER:
203
+ dave.setExternalSender(message.payload);
204
+ break;
205
+ case VoiceOpcodes.DAVE_MLS_PROPOSALS: {
206
+ const payload = dave.processProposals(message.payload, this.connection.connectedClients);
207
+ if (payload) this.sendBinaryMessage(VoiceOpcodes.DAVE_MLS_COMMIT_WELCOME, payload);
208
+ break;
209
+ }
210
+ case VoiceOpcodes.DAVE_MLS_ANNOUNCE_COMMIT_TRANSITION: {
211
+ const { transitionId, success } = dave.processCommit(message.payload);
212
+ if (success) {
213
+ if (transitionId === 0) {
214
+ this.emit('transitioned', transitionId);
215
+ } else {
216
+ this.sendPacket({
217
+ op: VoiceOpcodes.DAVE_TRANSITION_READY,
218
+ d: { transition_id: transitionId },
219
+ });
220
+ }
221
+ }
222
+ break;
223
+ }
224
+ case VoiceOpcodes.DAVE_MLS_WELCOME: {
225
+ const { transitionId, success } = dave.processWelcome(message.payload);
226
+ if (success) {
227
+ if (transitionId === 0) {
228
+ this.emit('transitioned', transitionId);
229
+ } else {
230
+ this.sendPacket({
231
+ op: VoiceOpcodes.DAVE_TRANSITION_READY,
232
+ d: { transition_id: transitionId },
233
+ });
234
+ }
235
+ }
236
+ break;
237
+ }
238
+ default:
239
+ this.emit('unknownPacket', message);
240
+ }
241
+ }
242
+
147
243
  /**
148
244
  * Called whenever the connection to the WebSocket server is lost.
149
245
  * @param {CloseEvent} event The WebSocket close event
150
246
  */
151
247
  onClose(event) {
152
248
  this.emit('debug', `[WS] closed with code ${event.code} and reason: ${event.reason}`);
249
+ if (event.code === 4017) {
250
+ this.dead = true;
251
+ this.emit(
252
+ 'error',
253
+ new Error('VOICE_DAVE_REQUIRED', 'Discord requires DAVE/E2EE protocol support. Ensure @snazzah/davey is installed.'),
254
+ );
255
+ return;
256
+ }
257
+ if (event.code === 4006) {
258
+ this.dead = true;
259
+ const guildId = this.connection.channel.guild?.id;
260
+ if (guildId) {
261
+ this.connection.channel.client.ws.broadcast({
262
+ op: Opcodes.VOICE_STATE_UPDATE,
263
+ d: { guild_id: guildId, channel_id: null, self_mute: false, self_deaf: false },
264
+ });
265
+ }
266
+ this.emit('error', new Error('VOICE_SESSION_EXPIRED'));
267
+ return;
268
+ }
153
269
  if (!this.dead) setTimeout(this.connect.bind(this), this.attempts * 1000).unref();
154
270
  }
155
271
 
@@ -162,6 +278,90 @@ class VoiceWebSocket extends EventEmitter {
162
278
  this.emit('error', error);
163
279
  }
164
280
 
281
+ setupDaveSession(protocolVersion) {
282
+ const isStream = this.connection.constructor.name === 'StreamConnection';
283
+ const parentDave = this.connection.voiceConnection?.dave;
284
+
285
+ if (isStream && parentDave?.session) {
286
+ this.connection.dave = parentDave;
287
+ this.connection.connectedClients.add(this.client.user.id);
288
+ for (const id of this.connection.voiceConnection.connectedClients) {
289
+ this.connection.connectedClients.add(id);
290
+ }
291
+ this.emit('debug', '[DAVE] Reusing parent voice connection MLS session');
292
+ return;
293
+ }
294
+
295
+ if (this.connection.dave) {
296
+ const isSharedDave = isStream && this.connection.dave === parentDave;
297
+ if (!isSharedDave) {
298
+ this.connection.dave.destroy();
299
+ this.connection.dave.removeAllListeners();
300
+ }
301
+ }
302
+
303
+ if (!protocolVersion || getMaxProtocolVersion() === 0) {
304
+ this.connection.dave = null;
305
+ return;
306
+ }
307
+
308
+ const session = new DAVESession(
309
+ protocolVersion,
310
+ this.client.user.id,
311
+ this.connection.channel.id,
312
+ );
313
+
314
+ session.on('debug', msg => this.emit('debug', `[DAVE] ${msg}`));
315
+ session.on('keyPackage', keyPackage => {
316
+ this.sendBinaryMessage(VoiceOpcodes.DAVE_MLS_KEY_PACKAGE, keyPackage).catch(e => this.emit('error', e));
317
+ });
318
+ session.on('invalidateTransition', transitionId => {
319
+ this.sendPacket({
320
+ op: VoiceOpcodes.DAVE_MLS_INVALID_COMMIT_WELCOME,
321
+ d: { transition_id: transitionId },
322
+ }).catch(e => this.emit('error', e));
323
+ });
324
+ session.on('error', err => this.emit('error', err));
325
+
326
+ this.connection.dave = session;
327
+ this.connection.connectedClients.add(this.client.user.id);
328
+ if (this.connection.voiceConnection?.connectedClients) {
329
+ for (const id of this.connection.voiceConnection.connectedClients) {
330
+ this.connection.connectedClients.add(id);
331
+ }
332
+ }
333
+ session.reinit();
334
+ }
335
+
336
+ handleDavePacket(packet) {
337
+ const { dave } = this.connection;
338
+ if (!dave) return;
339
+
340
+ switch (packet.op) {
341
+ case VoiceOpcodes.DAVE_PREPARE_TRANSITION: {
342
+ const sendReady = dave.prepareTransition(packet.d);
343
+ if (sendReady) {
344
+ this.sendPacket({
345
+ op: VoiceOpcodes.DAVE_TRANSITION_READY,
346
+ d: { transition_id: packet.d.transition_id },
347
+ });
348
+ }
349
+ if (packet.d.transition_id === 0) this.emit('transitioned', 0);
350
+ break;
351
+ }
352
+ case VoiceOpcodes.DAVE_EXECUTE_TRANSITION: {
353
+ const transitioned = dave.executeTransition(packet.d.transition_id);
354
+ if (transitioned) this.emit('transitioned', packet.d.transition_id);
355
+ break;
356
+ }
357
+ case VoiceOpcodes.DAVE_PREPARE_EPOCH:
358
+ dave.prepareEpoch(packet.d);
359
+ break;
360
+ default:
361
+ break;
362
+ }
363
+ }
364
+
165
365
  /**
166
366
  * Called whenever a valid packet is received from the WebSocket.
167
367
  * @param {Object} packet The received packet
@@ -169,9 +369,24 @@ class VoiceWebSocket extends EventEmitter {
169
369
  onPacket(packet) {
170
370
  this.emit('debug', `[WS] << ${JSON.stringify(packet)}`);
171
371
  if (packet.seq) this._sequenceNumber = packet.seq;
372
+
373
+ if (
374
+ [
375
+ VoiceOpcodes.DAVE_PREPARE_TRANSITION,
376
+ VoiceOpcodes.DAVE_EXECUTE_TRANSITION,
377
+ VoiceOpcodes.DAVE_PREPARE_EPOCH,
378
+ ].includes(packet.op)
379
+ ) {
380
+ if (!this.usesSharedDaveSession()) this.handleDavePacket(packet);
381
+ return;
382
+ }
383
+
172
384
  switch (packet.op) {
173
385
  case VoiceOpcodes.HELLO:
174
386
  this.setHeartbeat(packet.d.heartbeat_interval);
387
+ this.sendIdentify().catch(() => {
388
+ this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED'));
389
+ });
175
390
  break;
176
391
  case VoiceOpcodes.READY:
177
392
  /**
@@ -180,11 +395,11 @@ class VoiceWebSocket extends EventEmitter {
180
395
  * @event VoiceWebSocket#ready
181
396
  */
182
397
  this.emit('ready', packet.d);
183
- this.connection.setVideoStatus(false);
184
398
  break;
185
399
  /* eslint-disable no-case-declarations */
186
400
  case VoiceOpcodes.SESSION_DESCRIPTION:
187
401
  packet.d.secret_key = new Uint8Array(packet.d.secret_key);
402
+ this.setupDaveSession(packet.d.dave_protocol_version ?? 0);
188
403
  /**
189
404
  * Emitted once the Voice Websocket receives a description of this voice session.
190
405
  * @param {Object} packet The received packet
@@ -192,6 +407,9 @@ class VoiceWebSocket extends EventEmitter {
192
407
  */
193
408
  this.emit('sessionDescription', packet.d);
194
409
  break;
410
+ case VoiceOpcodes.CLIENTS_CONNECT:
411
+ for (const id of packet.d.user_ids) this.connection.connectedClients.add(id);
412
+ break;
195
413
  case VoiceOpcodes.CLIENT_CONNECT:
196
414
  this.connection.ssrcMap.set(+packet.d.audio_ssrc, {
197
415
  userId: packet.d.user_id,
@@ -200,6 +418,7 @@ class VoiceWebSocket extends EventEmitter {
200
418
  });
201
419
  break;
202
420
  case VoiceOpcodes.CLIENT_DISCONNECT:
421
+ this.connection.connectedClients.delete(packet.d.user_id);
203
422
  const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);
204
423
  if (streamInfo) {
205
424
  this.connection.receiver.packets.streams.delete(packet.d.user_id);
@@ -258,10 +477,7 @@ class VoiceWebSocket extends EventEmitter {
258
477
  * Clears a heartbeat interval, if one exists.
259
478
  */
260
479
  clearHeartbeat() {
261
- if (!this.heartbeatInterval) {
262
- this.emit('warn', 'Tried to clear a heartbeat interval that does not exist');
263
- return;
264
- }
480
+ if (!this.heartbeatInterval) return;
265
481
  clearInterval(this.heartbeatInterval);
266
482
  this.heartbeatInterval = null;
267
483
  }
@@ -214,7 +214,9 @@ class MediaPlayer extends EventEmitter {
214
214
  args.push(...FFMPEG_H265_ARGUMENTS(options));
215
215
  }
216
216
 
217
- args.push('-force_key_frames', '00:02');
217
+ if (options?.fps) {
218
+ args.push('-g', String(options.fps), '-keyint_min', String(options.fps), '-sc_threshold', '0');
219
+ }
218
220
 
219
221
  if (options?.inputFFmpegArgs) {
220
222
  args.unshift(...options.inputFFmpegArgs);
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ class AnnexBBitstreamReader {
4
+ constructor(buffer) {
5
+ this._buffer = buffer;
6
+ this._byteOffset = 0;
7
+ this._bitOffset = 0;
8
+ }
9
+
10
+ readBits(count) {
11
+ if (count === 0) return 0;
12
+ let result = 0;
13
+ while (count > 0) {
14
+ if (this._byteOffset >= this._buffer.length) throw new Error('Bad byte offset');
15
+ if (
16
+ this._bitOffset === 0 &&
17
+ this._byteOffset >= 2 &&
18
+ this._buffer[this._byteOffset - 2] === 0 &&
19
+ this._buffer[this._byteOffset - 1] === 0 &&
20
+ this._buffer[this._byteOffset] === 3
21
+ ) {
22
+ this._byteOffset++;
23
+ }
24
+ if (this._bitOffset === 0 && count >= 8) {
25
+ result = (result << 8) | this._buffer[this._byteOffset++];
26
+ count -= 8;
27
+ } else {
28
+ const numBitsToRead = Math.min(count, 8 - this._bitOffset);
29
+ const mask = (1 << numBitsToRead) - 1;
30
+ const newBits = (this._buffer[this._byteOffset] >> (8 - this._bitOffset - numBitsToRead)) & mask;
31
+ result = (result << numBitsToRead) | newBits;
32
+ count -= numBitsToRead;
33
+ this._bitOffset += numBitsToRead;
34
+ if (this._bitOffset === 8) {
35
+ this._bitOffset = 0;
36
+ this._byteOffset++;
37
+ }
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+
43
+ readUnsigned(bits) {
44
+ return this.readBits(bits);
45
+ }
46
+
47
+ readSigned(bits) {
48
+ const unsigned = this.readUnsigned(bits);
49
+ if (unsigned & (1 << (bits - 1))) return unsigned - (1 << bits);
50
+ return unsigned;
51
+ }
52
+
53
+ readUnsignedExpGolomb() {
54
+ let leading0 = 0;
55
+ while (this.readBits(1) === 0) leading0++;
56
+ return (1 << leading0) + this.readBits(leading0) - 1;
57
+ }
58
+
59
+ readSignedExpGolomb() {
60
+ const unsigned = this.readUnsignedExpGolomb();
61
+ if (unsigned % 2 === 0) return unsigned / -2;
62
+ return (unsigned + 1) / 2;
63
+ }
64
+ }
65
+
66
+ class AnnexBBitstreamWriter {
67
+ constructor() {
68
+ this._arr = [];
69
+ this._pendingByte = 0;
70
+ this._bitOffset = 0;
71
+ }
72
+
73
+ toBuffer() {
74
+ return Buffer.from(this._arr);
75
+ }
76
+
77
+ flush() {
78
+ if (this._pendingByte <= 3 && this._arr.at(-1) === 0 && this._arr.at(-2) === 0) this._arr.push(3);
79
+ this._arr.push(this._pendingByte);
80
+ this._pendingByte = 0;
81
+ this._bitOffset = 0;
82
+ }
83
+
84
+ writeBits(bits, count) {
85
+ while (count > 0) {
86
+ if (this._bitOffset === 0) {
87
+ if (count >= 8) {
88
+ this._pendingByte = (bits >> (count - 8)) & 0xff;
89
+ count -= 8;
90
+ this.flush();
91
+ } else {
92
+ const mask = (1 << count) - 1;
93
+ this._pendingByte |= (bits & mask) << (8 - count);
94
+ this._bitOffset = count;
95
+ count = 0;
96
+ }
97
+ } else {
98
+ const numBitsToWrite = Math.min(8 - this._bitOffset, count);
99
+ const bitsToWrite = (bits >> (count - numBitsToWrite)) & ((1 << numBitsToWrite) - 1);
100
+ this._pendingByte |= bitsToWrite << (8 - this._bitOffset - numBitsToWrite);
101
+ count -= numBitsToWrite;
102
+ this._bitOffset += numBitsToWrite;
103
+ if (this._bitOffset === 8) {
104
+ this._bitOffset = 0;
105
+ this.flush();
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ writeUnsigned(num, count) {
112
+ if (num < 0) throw new Error('Expected a non-negative number');
113
+ this.writeBits(num, count);
114
+ }
115
+
116
+ writeSigned(num, count) {
117
+ if (count <= 0) return;
118
+ if (count > 32) throw new Error('writeSigned supports up to 32 bits');
119
+ const mask = count === 32 ? 0xffffffff >>> 0 : (((1 << count) >>> 0) - 1) >>> 0;
120
+ this.writeBits((num & mask) >>> 0, count);
121
+ }
122
+
123
+ writeUnsignedExpGolomb(num) {
124
+ if (num < 0) throw new Error('Expected a non-negative number');
125
+ num++;
126
+ const bitCount = 32 - Math.clz32(num >>> 0);
127
+ this.writeBits(0, bitCount - 1);
128
+ this.writeBits(num, bitCount);
129
+ }
130
+
131
+ writeSignedExpGolomb(num) {
132
+ if (num < 0) this.writeUnsignedExpGolomb(-2 * num);
133
+ else this.writeUnsignedExpGolomb(2 * num - 1);
134
+ }
135
+ }
136
+
137
+ module.exports = { AnnexBBitstreamReader, AnnexBBitstreamWriter };