rockcord 4.0.0-dev.4 → 4.0.0

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.
Files changed (69) hide show
  1. package/README.md +5 -3
  2. package/package.json +17 -14
  3. package/src/client/Client.js +3 -3
  4. package/src/client/WebhookClient.js +1 -1
  5. package/src/client/actions/VoiceStateUpdate.js +2 -2
  6. package/src/client/voice/VoiceConnection.js +98 -24
  7. package/src/client/voice/dispatcher/AudioDispatcher.js +1 -1
  8. package/src/client/voice/dispatcher/BaseDispatcher.js +49 -61
  9. package/src/client/voice/dispatcher/VideoDispatcher.js +86 -2
  10. package/src/client/voice/networking/CongestionControl.js +84 -0
  11. package/src/client/voice/networking/DAVE.js +26 -1
  12. package/src/client/voice/networking/MediaRecovery.js +300 -0
  13. package/src/client/voice/networking/VoiceUDPClient.js +10 -1
  14. package/src/client/voice/networking/VoiceWebSocket.js +4 -1
  15. package/src/client/voice/player/MediaPlayer.js +45 -11
  16. package/src/client/voice/player/processing/AnnexBNalSplitter.js +0 -1
  17. package/src/client/voice/receiver/AudioJitter.js +101 -0
  18. package/src/client/voice/receiver/PacketHandler.js +66 -9
  19. package/src/client/voice/receiver/Receiver.js +9 -4
  20. package/src/client/voice/receiver/Recorder.js +186 -57
  21. package/src/client/voice/util/PlayInterface.js +2 -0
  22. package/src/client/voice/util/Secretbox.js +1 -1
  23. package/src/client/voice/util/TransportCrypto.js +54 -0
  24. package/src/client/voice/util/VideoFrames.js +4 -1
  25. package/src/client/websocket/WebSocketShard.js +0 -1
  26. package/src/managers/ApplicationCommandManager.js +0 -2
  27. package/src/managers/ApplicationCommandPermissionsManager.js +0 -1
  28. package/src/managers/GuildChannelManager.js +2 -3
  29. package/src/managers/GuildMemberManager.js +0 -1
  30. package/src/managers/GuildTextThreadManager.js +1 -1
  31. package/src/managers/MessageManager.js +5 -5
  32. package/src/managers/RoleManager.js +1 -1
  33. package/src/rest/APIRequest.js +0 -1
  34. package/src/rest/APIRouter.js +2 -2
  35. package/src/rest/DiscordAPIError.js +1 -1
  36. package/src/rest/RequestHandler.js +3 -3
  37. package/src/sharding/ShardingManager.js +2 -2
  38. package/src/structures/ApplicationCommand.js +2 -7
  39. package/src/structures/BaseCommandInteraction.js +1 -2
  40. package/src/structures/BaseGuildTextChannel.js +1 -1
  41. package/src/structures/BaseGuildVoiceChannel.js +1 -1
  42. package/src/structures/ClientPresence.js +0 -1
  43. package/src/structures/ClientUser.js +13 -5
  44. package/src/structures/ContainerComponent.js +1 -1
  45. package/src/structures/DMChannel.js +1 -1
  46. package/src/structures/GroupDMChannel.js +4 -4
  47. package/src/structures/Guild.js +0 -2
  48. package/src/structures/GuildAuditLogs.js +4 -4
  49. package/src/structures/GuildScheduledEvent.js +0 -2
  50. package/src/structures/Interaction.js +1 -1
  51. package/src/structures/InteractionCollector.js +2 -2
  52. package/src/structures/InteractionWebhook.js +0 -1
  53. package/src/structures/Message.js +16 -17
  54. package/src/structures/MessageComponentInteraction.js +1 -1
  55. package/src/structures/MessageEmbed.js +0 -2
  56. package/src/structures/MessagePayload.js +2 -2
  57. package/src/structures/ModalSubmitInteraction.js +2 -2
  58. package/src/structures/ThreadChannel.js +3 -3
  59. package/src/structures/ThreadOnlyChannel.js +1 -1
  60. package/src/structures/WebEmbed.js +0 -1
  61. package/src/structures/interfaces/Application.js +2 -2
  62. package/src/structures/interfaces/Collector.js +1 -4
  63. package/src/structures/interfaces/TextBasedChannel.js +6 -6
  64. package/src/util/AttachmentFlags.js +0 -1
  65. package/src/util/Constants.js +0 -2
  66. package/src/util/Options.js +3 -3
  67. package/src/util/Util.js +1 -2
  68. package/typings/index.d.ts +109 -115
  69. package/typings/rawDataTypes.d.ts +5 -15
@@ -4,6 +4,11 @@ const { spawn } = require('child_process');
4
4
  const { createSocket } = require('dgram');
5
5
  const { EventEmitter } = require('events');
6
6
  const { Buffer } = require('node:buffer');
7
+ const fs = require('node:fs');
8
+ const os = require('node:os');
9
+ const path = require('node:path');
10
+ const process = require('node:process');
11
+ const { setTimeout } = require('node:timers');
7
12
  const { Writable } = require('stream');
8
13
  const kill = require('tree-kill');
9
14
  const { RtpPacket } = require('werift-rtp');
@@ -16,9 +21,12 @@ const { StreamOutput } = require('../util/Socket');
16
21
  * @extends {EventEmitter}
17
22
  */
18
23
  class Recorder extends EventEmitter {
19
- constructor(receiver, { userId, portUdpH264, portUdpOpus, output } = {}) {
24
+ constructor(receiver, { userId, codec = 'H264', portUdpVideo, portUdpH264, portUdpOpus, output } = {}) {
20
25
  super();
21
26
 
27
+ if (!['H264', 'H265', 'VP8'].includes(codec)) throw new RangeError('Unsupported recording codec');
28
+ this.codec = codec;
29
+
22
30
  Object.defineProperty(this, 'receiver', { value: receiver });
23
31
 
24
32
  /**
@@ -27,16 +35,15 @@ class Recorder extends EventEmitter {
27
35
  */
28
36
  this.userId = userId;
29
37
 
30
- this.portUdpH264 = portUdpH264 || null;
31
- this.portUdpH265 = null;
38
+ this.portUdpVideo = (portUdpVideo ?? portUdpH264) || null;
32
39
  this.portUdpOpus = portUdpOpus || null;
33
40
 
34
41
  this.promise = null;
35
42
 
36
- if (!portUdpH264 || !portUdpOpus) {
43
+ if (!this.portUdpVideo || !portUdpOpus) {
37
44
  this.promise = (async () => {
38
- const selected = new Set([this.portUdpH264, this.portUdpOpus].filter(Boolean));
39
- const missing = Number(!this.portUdpH264) + Number(!this.portUdpOpus);
45
+ const selected = new Set([this.portUdpVideo, this.portUdpOpus].filter(Boolean));
46
+ const missing = Number(!this.portUdpVideo) + Number(!this.portUdpOpus);
40
47
  const ports = [];
41
48
  for (let attempt = 0; ports.length < missing && attempt < 64; attempt++) {
42
49
  const port = await randomPort('udp4');
@@ -46,7 +53,7 @@ class Recorder extends EventEmitter {
46
53
  }
47
54
  }
48
55
  if (ports.length < missing) throw new Error('Could not allocate recording ports');
49
- this.portUdpH264 ??= ports.shift();
56
+ this.portUdpVideo ??= ports.shift();
50
57
  this.portUdpOpus ??= ports.shift();
51
58
  })();
52
59
  }
@@ -63,6 +70,8 @@ class Recorder extends EventEmitter {
63
70
  */
64
71
  this.ready = false;
65
72
  this.destroyed = false;
73
+ this.closed = false;
74
+ this.completion = new Promise(resolve => (this._resolveCompletion = resolve));
66
75
 
67
76
  this.socket = createSocket('udp4');
68
77
 
@@ -71,6 +80,7 @@ class Recorder extends EventEmitter {
71
80
  }
72
81
 
73
82
  fail(error) {
83
+ this._error = error;
74
84
  if (this.listenerCount('error')) this.emit('error', error);
75
85
  else this.emit('debug', error.message);
76
86
  this.destroy();
@@ -78,47 +88,75 @@ class Recorder extends EventEmitter {
78
88
  async init(output) {
79
89
  await this.promise;
80
90
  if (this.destroyed) return;
81
- const sdpData = Util.getSDPCodecName(this.portUdpH264, this.portUdpH265, this.portUdpOpus);
91
+ // Keep stdin available for FFmpeg's graceful quit command. Reading SDP from
92
+ // stdin and closing it leaves no way to write the Matroska trailer on Windows.
93
+ this.portUdpH264 = this.codec === 'H264' ? this.portUdpVideo : null;
94
+ this.portUdpH265 = this.codec === 'H265' ? this.portUdpVideo : null;
95
+ this.portUdpVP8 = this.codec === 'VP8' ? this.portUdpVideo : null;
96
+ const videoType = Util.getPayloadType(this.codec);
97
+ const opusType = Util.getPayloadType('opus');
98
+ const sdpData = [
99
+ 'v=0',
100
+ 'o=- 0 0 IN IP4 127.0.0.1',
101
+ 's=Rockcord recording',
102
+ 'c=IN IP4 127.0.0.1',
103
+ 't=0 0',
104
+ `m=video ${this.portUdpVideo} RTP/AVP ${videoType}`,
105
+ `a=rtpmap:${videoType} ${this.codec}/90000`,
106
+ ...(this.codec === 'H264' ? [`a=fmtp:${videoType} packetization-mode=1`] : []),
107
+ `m=audio ${this.portUdpOpus} RTP/AVP ${opusType}`,
108
+ `a=rtpmap:${opusType} opus/48000/2`,
109
+ `a=fmtp:${opusType} minptime=10;useinbandfec=1`,
110
+ '',
111
+ ].join('\r\n');
112
+ this._directory = fs.mkdtempSync(path.join(os.tmpdir(), 'rockcord-recorder-'));
113
+ this._sdpPath = path.join(this._directory, 'input.sdp');
114
+ fs.writeFileSync(this._sdpPath, sdpData);
82
115
  const isStream = output instanceof Writable;
83
116
  if (isStream) {
84
117
  this.outputStream = StreamOutput(output);
85
118
  }
86
- const stream = spawn('ffmpeg', [
87
- '-reorder_queue_size',
88
- '500',
89
- '-thread_queue_size',
90
- '500',
91
- '-err_detect',
92
- 'ignore_err',
93
- '-flags2',
94
- '+export_mvs',
95
- '-fflags',
96
- '+genpts+discardcorrupt',
97
- '-use_wallclock_as_timestamps',
98
- '1',
99
- '-f',
100
- 'sdp',
101
- '-analyzeduration',
102
- '1M',
103
- '-probesize',
104
- '1M',
105
- '-protocol_whitelist',
106
- 'file,udp,rtp,pipe,fd',
107
- '-i',
108
- '-', // Read from stdin
109
- '-buffer_size',
110
- '4M',
111
- '-max_delay',
112
- '500000', // 500ms
113
- '-rtbufsize',
114
- '4M',
115
- '-c',
116
- 'copy',
117
- '-y',
118
- '-f',
119
- 'matroska',
120
- isStream ? this.outputStream.url : output,
121
- ]);
119
+ const stream = spawn(
120
+ process.env.FFMPEG_PATH || 'ffmpeg',
121
+ [
122
+ '-hide_banner',
123
+ '-reorder_queue_size',
124
+ '0',
125
+ '-thread_queue_size',
126
+ '500',
127
+ '-err_detect',
128
+ 'ignore_err',
129
+ '-flags2',
130
+ '+export_mvs',
131
+ '-fflags',
132
+ '+genpts+discardcorrupt',
133
+ '-f',
134
+ 'sdp',
135
+ '-analyzeduration',
136
+ '5000000',
137
+ '-probesize',
138
+ '1M',
139
+ '-listen_timeout',
140
+ '0',
141
+ '-protocol_whitelist',
142
+ 'file,udp,rtp,pipe,fd',
143
+ '-i',
144
+ this._sdpPath,
145
+ '-buffer_size',
146
+ '4M',
147
+ '-max_delay',
148
+ '0',
149
+ '-rtbufsize',
150
+ '4M',
151
+ '-c',
152
+ 'copy',
153
+ '-y',
154
+ '-f',
155
+ 'matroska',
156
+ isStream ? this.outputStream.url : output,
157
+ ],
158
+ { windowsHide: true },
159
+ );
122
160
 
123
161
  /**
124
162
  * The FFmpeg process
@@ -127,15 +165,44 @@ class Recorder extends EventEmitter {
127
165
  this.stream = stream;
128
166
  stream.on('error', error => this.fail(error));
129
167
  stream.stdin.on('error', error => this.fail(error));
130
- stream.once('exit', () => this.destroy());
131
- this.stream.stdin.write(sdpData);
132
- this.stream.stdin.end();
133
- this.stream.stderr.once('data', data => {
168
+ stream.once('close', code => {
169
+ this.exitCode = code;
170
+ if (code && !this._error) this._error = new Error(`FFmpeg recording exited with code ${code}`);
171
+ this.destroy();
172
+ this._finalize();
173
+ });
174
+ this.stream.stdout?.resume();
175
+ stream.once('spawn', () => this._waitForPorts().catch(error => this.fail(error)));
176
+ this.stream.stderr.on('data', data => {
134
177
  this.emit('debug', `stderr: ${data}`);
135
- this.ready = true;
136
- this.emit('ready');
137
178
  });
138
179
  }
180
+
181
+ async _waitForPorts() {
182
+ const bound = port =>
183
+ new Promise(resolve => {
184
+ const probe = createSocket('udp4');
185
+ probe.once('error', error => {
186
+ try {
187
+ probe.close();
188
+ } catch {
189
+ /* An unsuccessful bind may close the probe. */
190
+ }
191
+ resolve(error.code === 'EADDRINUSE');
192
+ });
193
+ probe.bind(port, '127.0.0.1', () => probe.close(() => resolve(false)));
194
+ });
195
+ for (let attempt = 0; attempt < 120 && !this.destroyed; attempt++) {
196
+ const ports = await Promise.all([bound(this.portUdpVideo), bound(this.portUdpOpus)]);
197
+ if (ports.every(Boolean)) {
198
+ this.ready = true;
199
+ this.emit('ready', this);
200
+ return;
201
+ }
202
+ await new Promise(resolve => setTimeout(resolve, 25));
203
+ }
204
+ if (!this.destroyed) throw new Error('FFmpeg did not open the recording RTP ports');
205
+ }
139
206
  /**
140
207
  * Send a payload to FFmpeg via UDP
141
208
  * @param {RtpPacket|string|Buffer} payload The payload
@@ -156,15 +223,28 @@ class Recorder extends EventEmitter {
156
223
  const message = payload.serialize();
157
224
  // Get port from payloadType
158
225
  let port;
226
+ let kind;
159
227
  if (payload.header.payloadType === Util.getPayloadType('opus')) {
228
+ kind = 'audio';
160
229
  port = this.portUdpOpus;
161
- } else if (payload.header.payloadType === Util.getPayloadType('H264')) {
162
- port = this.portUdpH264;
163
- } else if (payload.header.payloadType === Util.getPayloadType('H265')) {
164
- port = this.portUdpH265;
230
+ this._lastAudioHeader = payload.header;
231
+ } else if (payload.header.payloadType === Util.getPayloadType(this.codec)) {
232
+ kind = 'video';
233
+ port = this.portUdpVideo;
234
+ this._lastVideoHeader = payload.header;
165
235
  } else {
166
236
  return;
167
237
  }
238
+ // FFmpeg's RTP probation starts at sequence zero. Normalize each new local
239
+ // source to one so the first keyframe's leading fragment is not discarded.
240
+ // Keep subsequent gaps and wrap intact, and never mutate the caller's packet.
241
+ const sourceKey = `_${kind}SequenceSource`;
242
+ const offsetKey = `_${kind}SequenceOffset`;
243
+ if (this[sourceKey] !== payload.header.ssrc) {
244
+ this[sourceKey] = payload.header.ssrc;
245
+ this[offsetKey] = 1 - payload.header.sequenceNumber;
246
+ }
247
+ message.writeUInt16BE((payload.header.sequenceNumber + this[offsetKey]) & 65535, 2);
168
248
  this.socket.send(message, 0, message.length, port, '127.0.0.1', callback);
169
249
  }
170
250
 
@@ -172,15 +252,64 @@ class Recorder extends EventEmitter {
172
252
  if (this.destroyed) return;
173
253
  this.destroyed = true;
174
254
  this.ready = false;
255
+ if (this.receiver?.videoStreams?.get(this.userId) === this) this.receiver.videoStreams.delete(this.userId);
256
+ if (this.stream?.pid && this.stream.exitCode === null && !this._error) {
257
+ // RTCP BYE ends both local RTP inputs. Unlike an immediate quit, EOF lets
258
+ // FFmpeg drain its parser and muxer, preserving the final buffered frame.
259
+ for (const [port, ssrc] of [
260
+ [this.portUdpVideo, this._lastVideoHeader?.ssrc ?? 1],
261
+ [this.portUdpOpus, this._lastAudioHeader?.ssrc ?? 2],
262
+ ]) {
263
+ // Include an empty, padded reason: FFmpeg's RTP demuxer requires at
264
+ // least 12 bytes even though a reason-less BYE may be eight bytes.
265
+ const bye = Buffer.from([0x81, 203, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0]);
266
+ bye.writeUInt32BE(ssrc, 4);
267
+ this.socket.send(bye, port + 1, '127.0.0.1', () => {});
268
+ }
269
+ this._quitTimer = setTimeout(() => this.stream.stdin.end('q\n'), 2000).unref();
270
+ this._killTimer = setTimeout(() => {
271
+ this._error = new Error('FFmpeg recording did not stop within 5 seconds');
272
+ kill(this.stream.pid, () => {});
273
+ }, 5000).unref();
274
+ } else if (this.stream?.pid && this.stream.exitCode === null) {
275
+ // Wait for close before removing the SDP: Windows keeps the input file
276
+ // locked until the failed child has actually exited.
277
+ kill(this.stream.pid, () => {});
278
+ } else {
279
+ this._finalize();
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Stop receiving and wait for FFmpeg to finalize the recording.
285
+ * @returns {Promise<void>}
286
+ */
287
+ async stop() {
288
+ this.destroy();
289
+ await this.completion;
290
+ if (this._error) throw this._error;
291
+ }
292
+
293
+ _finalize() {
294
+ if (this.closed) return;
295
+ this.closed = true;
296
+ clearTimeout(this._killTimer);
297
+ clearTimeout(this._quitTimer);
175
298
  try {
176
299
  this.socket.close();
177
300
  } catch {
178
301
  /* Socket may not have bound yet. */
179
302
  }
180
303
  this.outputStream?.destroy();
181
- if (this.stream?.pid) kill(this.stream.pid, () => {});
182
- if (this.receiver?.videoStreams?.get(this.userId) === this) this.receiver.videoStreams.delete(this.userId);
183
- this.emit('closed');
304
+ try {
305
+ if (this._sdpPath) fs.rmSync(this._sdpPath, { force: true });
306
+ if (this._directory) fs.rmdirSync(this._directory);
307
+ } catch (error) {
308
+ this._error ??= error;
309
+ this.emit('debug', `Recorder temporary-file cleanup failed: ${error.message}`);
310
+ }
311
+ this._resolveCompletion();
312
+ this.emit('closed', this);
184
313
  }
185
314
 
186
315
  /**
@@ -91,6 +91,8 @@ class PlayInterface {
91
91
  * Ex: ['-config1', 'value1', '-config2', 'value2']
92
92
  * @property {number|'auto'} [bitrate=2000] The bitrate (quality) of the video in kbps.
93
93
  * If set to 'auto', ffmpeg will automatically select
94
+ * @property {boolean|Object} [congestionControl=true] Enable loss-based AIMD video pacing.
95
+ * An object can set bitrate and minBitrate in kbps. Audio playback is unaffected.
94
96
  */
95
97
 
96
98
  /**
@@ -56,7 +56,7 @@ async function importModule(name, usingImport = false) {
56
56
  for (const libName of Object.keys(libs)) {
57
57
  try {
58
58
  const lib = await importModule(libName);
59
- if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; // eslint-disable-line no-await-in-loop
59
+ if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready;
60
60
  exports.methods = libs[libName](lib);
61
61
  break;
62
62
  } catch {} // eslint-disable-line no-empty
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ const { Buffer } = require('node:buffer');
4
+ const crypto = require('node:crypto');
5
+ const secretbox = require('./Secretbox');
6
+
7
+ const states = new WeakMap();
8
+
9
+ // RTP, RTX and RTCP all share a nonce space for a negotiated session key.
10
+ function encrypt(connection, payload, header) {
11
+ const { secret_key: key, mode } = connection.authentication;
12
+ let state = states.get(connection);
13
+ if (!state || !state.key.equals(Buffer.from(key))) {
14
+ state = { key: Buffer.from(key), value: 0 };
15
+ states.set(connection, state);
16
+ }
17
+ if (state.value >= 0xffffffff) throw new RangeError('Voice encryption nonce exhausted; establish a new session key');
18
+ const nonce = Buffer.alloc(mode === 'aead_aes256_gcm_rtpsize' ? 12 : 24);
19
+ nonce.writeUInt32BE(++state.value);
20
+ let ciphertext;
21
+ if (mode === 'aead_aes256_gcm_rtpsize') {
22
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
23
+ cipher.setAAD(header);
24
+ ciphertext = Buffer.concat([cipher.update(payload), cipher.final(), cipher.getAuthTag()]);
25
+ } else if (mode === 'aead_xchacha20_poly1305_rtpsize') {
26
+ ciphertext = Buffer.from(secretbox.methods.crypto_aead_xchacha20poly1305_ietf_encrypt(payload, header, nonce, key));
27
+ } else {
28
+ throw new RangeError(`Unsupported encryption method: ${mode}`);
29
+ }
30
+ return [ciphertext, nonce.subarray(0, 4)];
31
+ }
32
+
33
+ function decrypt(connection, packet, headerSize) {
34
+ const { secret_key: key, mode } = connection.authentication;
35
+ if (!key || packet.length < headerSize + 20) throw new RangeError('Truncated encrypted media packet');
36
+ const header = packet.subarray(0, headerSize);
37
+ const nonce = Buffer.alloc(mode === 'aead_aes256_gcm_rtpsize' ? 12 : 24);
38
+ packet.copy(nonce, 0, packet.length - 4);
39
+ const ciphertext = packet.subarray(headerSize, packet.length - 4);
40
+ let plain;
41
+ if (mode === 'aead_aes256_gcm_rtpsize') {
42
+ const cipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
43
+ cipher.setAAD(header);
44
+ cipher.setAuthTag(ciphertext.subarray(-16));
45
+ plain = Buffer.concat([cipher.update(ciphertext.subarray(0, -16)), cipher.final()]);
46
+ } else if (mode === 'aead_xchacha20_poly1305_rtpsize') {
47
+ plain = Buffer.from(secretbox.methods.crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext, header, nonce, key));
48
+ } else {
49
+ throw new RangeError(`Unsupported decryption method: ${mode}`);
50
+ }
51
+ return Buffer.concat([header, plain]);
52
+ }
53
+
54
+ module.exports = { encrypt, decrypt };
@@ -132,7 +132,7 @@ class VideoFrames {
132
132
  this.completed = new Map();
133
133
  }
134
134
 
135
- push(packet, codec) {
135
+ push(packet, codec, validate) {
136
136
  const { ssrc, timestamp, sequenceNumber: seq, marker } = packet.header;
137
137
  const now = Date.now();
138
138
  const key = `${ssrc}:${timestamp}:${codec}`;
@@ -169,6 +169,9 @@ class VideoFrames {
169
169
  }
170
170
  try {
171
171
  const complete = depacketize(payloads, codec);
172
+ // A marker and contiguous tail do not prove the beginning arrived. DAVE
173
+ // authentication must succeed before suppressing subsequent RTX fragments.
174
+ if (validate && !validate(complete)) return null;
172
175
  this.frames.delete(key);
173
176
  if (this.completed.size >= 128) this.completed.delete(this.completed.keys().next().value);
174
177
  this.completed.set(key, now);
@@ -605,7 +605,6 @@ class WebSocketShard extends EventEmitter {
605
605
  }
606
606
 
607
607
  this.debug(
608
- // eslint-disable-next-line max-len
609
608
  `[WebSocket] Close Emitted: ${this.closeEmitted} | did not close properly, assuming a zombie connection.\nEmitting close and reconnecting again.`,
610
609
  );
611
610
 
@@ -55,12 +55,10 @@ class ApplicationCommandManager extends CachedManager {
55
55
  * @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable
56
56
  */
57
57
 
58
- /* eslint-disable max-len */
59
58
  /**
60
59
  * Data that resolves to the data of an ApplicationCommand
61
60
  * @typedef {ApplicationCommandData|APIApplicationCommand|SlashCommandBuilder|ContextMenuCommandBuilder} ApplicationCommandDataResolvable
62
61
  */
63
- /* eslint-enable max-len */
64
62
 
65
63
  /**
66
64
  * Options used to fetch data from Discord
@@ -410,7 +410,6 @@ class ApplicationCommandPermissionsManager extends BaseManager {
410
410
 
411
411
  module.exports = ApplicationCommandPermissionsManager;
412
412
 
413
- /* eslint-disable max-len */
414
413
  /**
415
414
  * @external APIApplicationCommandPermissions
416
415
  * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure}
@@ -58,7 +58,7 @@ class GuildChannelManager extends CachedManager {
58
58
  get channelCountWithoutThreads() {
59
59
  return this.cache.reduce((acc, channel) => {
60
60
  if (ThreadChannelTypes.includes(channel.type)) return acc;
61
- return ++acc;
61
+ return acc + 1;
62
62
  }, 0);
63
63
  }
64
64
 
@@ -155,7 +155,7 @@ class GuildChannelManager extends CachedManager {
155
155
  ) {
156
156
  parent &&= this.client.channels.resolveId(parent);
157
157
  permissionOverwrites &&= permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
158
- const intType = typeof type === 'number' ? type : ChannelTypes[type] ?? ChannelTypes.GUILD_TEXT;
158
+ const intType = typeof type === 'number' ? type : (ChannelTypes[type] ?? ChannelTypes.GUILD_TEXT);
159
159
 
160
160
  const videoMode = typeof videoQualityMode === 'number' ? videoQualityMode : VideoQualityModes[videoQualityMode];
161
161
 
@@ -167,7 +167,6 @@ class GuildChannelManager extends CachedManager {
167
167
  if (intType === ChannelTypes.GUILD_STORE && !storeChannelDeprecationEmitted) {
168
168
  storeChannelDeprecationEmitted = true;
169
169
  process.emitWarning(
170
- // eslint-disable-next-line max-len
171
170
  'Creating store channels is deprecated by Discord and will stop working in March 2022. Check the docs for more info.',
172
171
  'DeprecationWarning',
173
172
  );
@@ -1,4 +1,3 @@
1
- /* eslint-disable newline-per-chained-call */
2
1
  'use strict';
3
2
 
4
3
  const { Buffer } = require('node:buffer');
@@ -75,7 +75,7 @@ class GuildTextThreadManager extends ThreadManager {
75
75
  if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
76
76
  path = path.messages(startMessageId);
77
77
  } else if (this.channel.type !== 'GUILD_NEWS') {
78
- resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType;
78
+ resolvedType = typeof type === 'string' ? ChannelTypes[type] : (type ?? resolvedType);
79
79
  }
80
80
 
81
81
  if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);
@@ -139,16 +139,17 @@ class MessageManager extends CachedManager {
139
139
 
140
140
  const preserveAttachments =
141
141
  (options instanceof MessagePayload ? options.options : options)?.attachments === undefined;
142
- const { data, files } = await (options instanceof MessagePayload
143
- ? options
144
- : MessagePayload.create(message instanceof Message ? message : this, options)
142
+ const { data, files } = await (
143
+ options instanceof MessagePayload
144
+ ? options
145
+ : MessagePayload.create(message instanceof Message ? message : this, options)
145
146
  )
146
147
  .resolveData()
147
148
  .resolveFiles();
148
149
 
149
150
  if (files.length && preserveAttachments) {
150
151
  const original =
151
- message instanceof Message ? message : this.cache.get(messageId) ?? (await this.fetch(messageId));
152
+ message instanceof Message ? message : (this.cache.get(messageId) ?? (await this.fetch(messageId)));
152
153
  data.attachments = [...original.attachments.values()].map(attachment => ({ id: attachment.id }));
153
154
  }
154
155
  data.attachments = await Util.resolveUploadedAttachments(this.client, this.channel.id, files, data.attachments);
@@ -221,7 +222,6 @@ class MessageManager extends CachedManager {
221
222
  ? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}`
222
223
  : encodeURIComponent(emoji.name);
223
224
 
224
- // eslint-disable-next-line newline-per-chained-call
225
225
  await this.client.api
226
226
  .channels(this.channel.id)
227
227
  .messages(message)
@@ -72,7 +72,7 @@ class RoleManager extends CachedManager {
72
72
  const data = await this.client.api.guilds(this.guild.id).roles.get();
73
73
  const roles = new Collection();
74
74
  for (const role of data) roles.set(role.id, this._add(role, cache));
75
- return id ? roles.get(id) ?? null : roles;
75
+ return id ? (roles.get(id) ?? null) : roles;
76
76
  }
77
77
 
78
78
  /**
@@ -106,7 +106,6 @@ class APIRequest {
106
106
  body.append('payload_json', JSON.stringify(this.options.data));
107
107
  }
108
108
  }
109
- // eslint-disable-next-line eqeqeq
110
109
  } else if (this.options.data != null) {
111
110
  if (this.options.usePayloadJSON) {
112
111
  body = new FormData();
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const noop = () => {}; // eslint-disable-line no-empty-function
3
+ const noop = () => {};
4
4
  const methods = ['get', 'post', 'delete', 'patch', 'put'];
5
5
  const reflectors = [
6
6
  'toString',
@@ -43,7 +43,7 @@ function buildRoute(manager) {
43
43
  return new Proxy(noop, handler);
44
44
  },
45
45
  apply(target, _, args) {
46
- route.push(...args.filter(x => x != null)); // eslint-disable-line eqeqeq
46
+ route.push(...args.filter(x => x != null));
47
47
  return new Proxy(noop, handler);
48
48
  },
49
49
  };
@@ -11,7 +11,7 @@ class DiscordAPIError extends Error {
11
11
  super();
12
12
  const flattened = this.constructor.flattenErrors(error.errors ?? error).join('\n');
13
13
  this.name = 'DiscordAPIError';
14
- this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message ?? flattened;
14
+ this.message = error.message && flattened ? `${error.message}\n${flattened}` : (error.message ?? flattened);
15
15
 
16
16
  /**
17
17
  * The HTTP method used for the request
@@ -154,7 +154,7 @@ class RequestHandler {
154
154
  }
155
155
 
156
156
  // Decide before allocating a delay: a rejected limit must not leave an unused promise/timer.
157
- await this.onRateLimit(request, limit, timeout, isGlobal); // eslint-disable-line no-await-in-loop
157
+ await this.onRateLimit(request, limit, timeout, isGlobal);
158
158
 
159
159
  if (isGlobal) {
160
160
  // If this is the first task to reach the global timeout, set the global delay
@@ -168,7 +168,7 @@ class RequestHandler {
168
168
  }
169
169
 
170
170
  // Wait for the timeout to expire in order to avoid an actual 429
171
- await delayPromise; // eslint-disable-line no-await-in-loop
171
+ await delayPromise;
172
172
  }
173
173
 
174
174
  // As the request goes out, update the global usage information
@@ -378,7 +378,7 @@ class RequestHandler {
378
378
  rqToken : [REDACTED]`,
379
379
  );
380
380
  const solved = await this.manager.client.options.captchaSolver(data, request.fullUserAgent);
381
- const captcha = typeof solved === 'string' ? solved : solved?.token ?? solved?.data;
381
+ const captcha = typeof solved === 'string' ? solved : (solved?.token ?? solved?.data);
382
382
  if (typeof captcha !== 'string' || !captcha) throw new Error('Captcha solver must return a non-empty token');
383
383
  this.manager.client.emit(
384
384
  DEBUG,
@@ -218,7 +218,7 @@ class ShardingManager extends EventEmitter {
218
218
  const shard = this.createShard(shardId);
219
219
  promises.push(shard.spawn(timeout));
220
220
  if (delay > 0 && this.shards.size !== this.shardList.length) promises.push(sleep(delay));
221
- await Promise.all(promises); // eslint-disable-line no-await-in-loop
221
+ await Promise.all(promises);
222
222
  }
223
223
 
224
224
  return this.shards;
@@ -310,7 +310,7 @@ class ShardingManager extends EventEmitter {
310
310
  for (const shard of this.shards.values()) {
311
311
  const promises = [shard.respawn({ delay: respawnDelay, timeout })];
312
312
  if (++s < this.shards.size && shardDelay > 0) promises.push(sleep(shardDelay));
313
- await Promise.all(promises); // eslint-disable-line no-await-in-loop
313
+ await Promise.all(promises);
314
314
  }
315
315
  return this.shards;
316
316
  }