rockcord 4.0.0-dev.5 → 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 (65) hide show
  1. package/README.md +4 -3
  2. package/package.json +15 -12
  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 +2 -2
  7. package/src/client/voice/dispatcher/AudioDispatcher.js +1 -1
  8. package/src/client/voice/dispatcher/BaseDispatcher.js +37 -12
  9. package/src/client/voice/dispatcher/VideoDispatcher.js +83 -0
  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 +112 -9
  13. package/src/client/voice/player/MediaPlayer.js +18 -3
  14. package/src/client/voice/player/processing/AnnexBNalSplitter.js +0 -1
  15. package/src/client/voice/receiver/AudioJitter.js +7 -4
  16. package/src/client/voice/receiver/PacketHandler.js +10 -4
  17. package/src/client/voice/receiver/Receiver.js +5 -3
  18. package/src/client/voice/receiver/Recorder.js +44 -14
  19. package/src/client/voice/util/PlayInterface.js +2 -0
  20. package/src/client/voice/util/Secretbox.js +1 -1
  21. package/src/client/websocket/WebSocketShard.js +0 -1
  22. package/src/managers/ApplicationCommandManager.js +0 -2
  23. package/src/managers/ApplicationCommandPermissionsManager.js +0 -1
  24. package/src/managers/GuildChannelManager.js +2 -3
  25. package/src/managers/GuildMemberManager.js +0 -1
  26. package/src/managers/GuildTextThreadManager.js +1 -1
  27. package/src/managers/MessageManager.js +5 -5
  28. package/src/managers/RoleManager.js +1 -1
  29. package/src/rest/APIRequest.js +0 -1
  30. package/src/rest/APIRouter.js +2 -2
  31. package/src/rest/DiscordAPIError.js +1 -1
  32. package/src/rest/RequestHandler.js +3 -3
  33. package/src/sharding/ShardingManager.js +2 -2
  34. package/src/structures/ApplicationCommand.js +2 -7
  35. package/src/structures/BaseCommandInteraction.js +1 -2
  36. package/src/structures/BaseGuildTextChannel.js +1 -1
  37. package/src/structures/BaseGuildVoiceChannel.js +1 -1
  38. package/src/structures/ClientPresence.js +0 -1
  39. package/src/structures/ClientUser.js +13 -5
  40. package/src/structures/ContainerComponent.js +1 -1
  41. package/src/structures/DMChannel.js +1 -1
  42. package/src/structures/GroupDMChannel.js +4 -4
  43. package/src/structures/Guild.js +0 -2
  44. package/src/structures/GuildAuditLogs.js +4 -4
  45. package/src/structures/GuildScheduledEvent.js +0 -2
  46. package/src/structures/Interaction.js +1 -1
  47. package/src/structures/InteractionCollector.js +2 -2
  48. package/src/structures/InteractionWebhook.js +0 -1
  49. package/src/structures/Message.js +16 -17
  50. package/src/structures/MessageComponentInteraction.js +1 -1
  51. package/src/structures/MessageEmbed.js +0 -2
  52. package/src/structures/MessagePayload.js +2 -2
  53. package/src/structures/ModalSubmitInteraction.js +2 -2
  54. package/src/structures/ThreadChannel.js +3 -3
  55. package/src/structures/ThreadOnlyChannel.js +1 -1
  56. package/src/structures/WebEmbed.js +0 -1
  57. package/src/structures/interfaces/Application.js +2 -2
  58. package/src/structures/interfaces/Collector.js +1 -4
  59. package/src/structures/interfaces/TextBasedChannel.js +6 -6
  60. package/src/util/AttachmentFlags.js +0 -1
  61. package/src/util/Constants.js +0 -2
  62. package/src/util/Options.js +3 -3
  63. package/src/util/Util.js +1 -2
  64. package/typings/index.d.ts +108 -115
  65. package/typings/rawDataTypes.d.ts +5 -15
@@ -38,7 +38,20 @@ const FFMPEG_INPUT_PREFIX = [
38
38
  '4294',
39
39
  ];
40
40
  const FFMPEG_PCM_ARGUMENTS = ['-f', 's16le', '-ar', '48000', '-ac', '2'];
41
- const FFMPEG_VP8_ARGUMENTS = ['-f', 'ivf', '-deadline', 'realtime', '-c:v', 'libvpx'];
41
+ const FFMPEG_VP8_ARGUMENTS = options => [
42
+ '-f',
43
+ 'ivf',
44
+ '-deadline',
45
+ 'realtime',
46
+ '-c:v',
47
+ 'libvpx',
48
+ '-g',
49
+ String(Math.max(1, Math.round(options?.fps || 30))),
50
+ '-lag-in-frames',
51
+ '0',
52
+ '-auto-alt-ref',
53
+ '0',
54
+ ];
42
55
  const FFMPEG_H264_ARGUMENTS = options => [
43
56
  '-c:v',
44
57
  'libx264',
@@ -209,7 +222,7 @@ class MediaPlayer extends EventEmitter {
209
222
 
210
223
  // Get stream type
211
224
  if (this.voiceConnection.videoCodec === 'VP8') {
212
- args.push(...FFMPEG_VP8_ARGUMENTS);
225
+ args.push(...FFMPEG_VP8_ARGUMENTS(options));
213
226
  // Remove '-speed', '5' bc bad quality
214
227
  }
215
228
 
@@ -271,7 +284,6 @@ class MediaPlayer extends EventEmitter {
271
284
  return dispatcher;
272
285
  }
273
286
 
274
- // eslint-disable-next-line no-unused-vars
275
287
  playAnnexBVideo(stream, options, streams, type) {
276
288
  this.destroyVideoDispatcher();
277
289
  let videoStream;
@@ -310,6 +322,7 @@ class MediaPlayer extends EventEmitter {
310
322
  streams,
311
323
  options?.fps,
312
324
  ));
325
+ dispatcher.configureCongestion(options);
313
326
  return dispatcher;
314
327
  }
315
328
  case 'H264': {
@@ -319,6 +332,7 @@ class MediaPlayer extends EventEmitter {
319
332
  streams,
320
333
  options?.fps,
321
334
  ));
335
+ dispatcher.configureCongestion(options);
322
336
  return dispatcher;
323
337
  }
324
338
  case 'H265': {
@@ -328,6 +342,7 @@ class MediaPlayer extends EventEmitter {
328
342
  streams,
329
343
  options?.fps,
330
344
  ));
345
+ dispatcher.configureCongestion(options);
331
346
  return dispatcher;
332
347
  }
333
348
  default: {
@@ -147,7 +147,6 @@ class AnnexBNalSplitter extends Transform {
147
147
  const newData = Buffer.allocUnsafe(data.length);
148
148
  let newLength = 0;
149
149
 
150
- // eslint-disable-next-line no-constant-condition
151
150
  while (true) {
152
151
  const epbsPos = data.indexOf(epbPrefix);
153
152
  if (epbsPos === -1) {
@@ -49,10 +49,13 @@ class AudioJitter {
49
49
 
50
50
  _schedule() {
51
51
  if (this.timer || !this.packets.size) return;
52
- this.timer = setTimeout(() => {
53
- this.timer = null;
54
- this._play();
55
- }, Math.max(0, this.next - performance.now())).unref();
52
+ this.timer = setTimeout(
53
+ () => {
54
+ this.timer = null;
55
+ this._play();
56
+ },
57
+ Math.max(0, this.next - performance.now()),
58
+ ).unref();
56
59
  }
57
60
 
58
61
  _play() {
@@ -23,7 +23,7 @@ const UNPADDED_NONCE_LENGTH = 4;
23
23
  const AUTH_TAG_LENGTH = 16;
24
24
 
25
25
  class Readable extends require('stream').Readable {
26
- _read() {} // eslint-disable-line no-empty-function
26
+ _read() {}
27
27
  }
28
28
 
29
29
  /**
@@ -40,6 +40,7 @@ class PacketHandler extends EventEmitter {
40
40
  this.speakingTimeouts = new Map();
41
41
  this.videoFrames = new VideoFrames();
42
42
  this.videoSequences = new Map();
43
+ this.videoPictures = new Map();
43
44
  this.audioJitters = new Map();
44
45
  this.recovery = this.connection._mediaRecovery = new MediaRecovery(this.connection);
45
46
  }
@@ -74,9 +75,10 @@ class PacketHandler extends EventEmitter {
74
75
  return stream;
75
76
  }
76
77
 
77
- makeVideoStream(user, output) {
78
+ makeVideoStream(user, output, options = {}) {
78
79
  if (this.videoStreams.has(user)) return this.videoStreams.get(user);
79
80
  const stream = new Recorder(this, {
81
+ ...options,
80
82
  userId: user,
81
83
  output,
82
84
  });
@@ -251,7 +253,9 @@ class PacketHandler extends EventEmitter {
251
253
  if (!frame) return;
252
254
  this.receiver.emit('videoFrame', userStat, plain, codec);
253
255
  if (streamInfo) {
254
- const payloads = packetize(plain, codec);
256
+ const picture = this.videoPictures.get(ssrc) ?? 0;
257
+ const payloads = packetize(plain, codec, 1200, picture);
258
+ this.videoPictures.set(ssrc, (picture + 1) & 0x7fff);
255
259
  let sequence = this.videoSequences.get(ssrc) ?? packet.header.sequenceNumber;
256
260
  for (let i = 0; i < payloads.length; i++) {
257
261
  streamInfo.feed(
@@ -307,7 +311,7 @@ class PacketHandler extends EventEmitter {
307
311
  packet.header.sequenceNumber = packet.payload.readUInt16BE();
308
312
  packet.header.payloadType = codec.payload_type;
309
313
  packet.payload = packet.payload.subarray(2);
310
- this.recovery.receive(packet);
314
+ this.recovery.receive(packet, true);
311
315
  this.videoReceiver(userStat.primarySsrc, userStat, packet);
312
316
  } else if (userStat.kind === 'video') {
313
317
  this.recovery.receive(packet);
@@ -350,6 +354,7 @@ class PacketHandler extends EventEmitter {
350
354
  this.audioJitters.delete(ssrc);
351
355
  this.recovery.sources.delete(ssrc);
352
356
  this.videoSequences.delete(ssrc);
357
+ this.videoPictures.delete(ssrc);
353
358
  clearTimeout(this.speakingTimeouts.get(ssrc));
354
359
  this.speakingTimeouts.delete(ssrc);
355
360
  for (const key of this.videoFrames.frames.keys()) {
@@ -366,6 +371,7 @@ class PacketHandler extends EventEmitter {
366
371
  this.videoFrames.frames.clear();
367
372
  this.videoFrames.completed.clear();
368
373
  this.videoSequences.clear();
374
+ this.videoPictures.clear();
369
375
  for (const stream of this.streams.values()) {
370
376
  stream.stream.destroy();
371
377
  }
@@ -92,16 +92,18 @@ class VoiceReceiver extends EventEmitter {
92
92
  /**
93
93
  * Creates a new video receiving stream. If a stream already exists for a user, then that stream will be returned
94
94
  * rather than generating a new one.
95
- * Records received H264 video and Opus audio using FFmpeg.
95
+ * Records received H264, H265 or VP8 video and Opus audio using FFmpeg.
96
96
  * Wait for `ready` before sending media and await `stop()` to finalize the output.
97
97
  * @param {UserResolvable} user The user to start listening to.
98
98
  * @param {WritableStream|string} output Output stream or file path to write the video stream to.
99
+ * @param {Object} [options] Recording options
100
+ * @param {VideoCodec} [options.codec='H264'] The codec sent by the recorded participant
99
101
  * @returns {Recorder} The video stream for the specified user.
100
102
  */
101
- createVideoStream(user, output) {
103
+ createVideoStream(user, output, options = {}) {
102
104
  user = this.connection.client.users.resolve(user);
103
105
  if (!user) throw new Error('VOICE_USER_MISSING');
104
- const stream = this.packets.makeVideoStream(user.id, output);
106
+ const stream = this.packets.makeVideoStream(user.id, output, options);
105
107
  this._updateVideoSubscriptions();
106
108
  stream.once('closed', () => this._updateVideoSubscriptions());
107
109
  return stream;
@@ -21,9 +21,12 @@ const { StreamOutput } = require('../util/Socket');
21
21
  * @extends {EventEmitter}
22
22
  */
23
23
  class Recorder extends EventEmitter {
24
- constructor(receiver, { userId, portUdpH264, portUdpOpus, output } = {}) {
24
+ constructor(receiver, { userId, codec = 'H264', portUdpVideo, portUdpH264, portUdpOpus, output } = {}) {
25
25
  super();
26
26
 
27
+ if (!['H264', 'H265', 'VP8'].includes(codec)) throw new RangeError('Unsupported recording codec');
28
+ this.codec = codec;
29
+
27
30
  Object.defineProperty(this, 'receiver', { value: receiver });
28
31
 
29
32
  /**
@@ -32,16 +35,15 @@ class Recorder extends EventEmitter {
32
35
  */
33
36
  this.userId = userId;
34
37
 
35
- this.portUdpH264 = portUdpH264 || null;
36
- this.portUdpH265 = null;
38
+ this.portUdpVideo = (portUdpVideo ?? portUdpH264) || null;
37
39
  this.portUdpOpus = portUdpOpus || null;
38
40
 
39
41
  this.promise = null;
40
42
 
41
- if (!portUdpH264 || !portUdpOpus) {
43
+ if (!this.portUdpVideo || !portUdpOpus) {
42
44
  this.promise = (async () => {
43
- const selected = new Set([this.portUdpH264, this.portUdpOpus].filter(Boolean));
44
- 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);
45
47
  const ports = [];
46
48
  for (let attempt = 0; ports.length < missing && attempt < 64; attempt++) {
47
49
  const port = await randomPort('udp4');
@@ -51,7 +53,7 @@ class Recorder extends EventEmitter {
51
53
  }
52
54
  }
53
55
  if (ports.length < missing) throw new Error('Could not allocate recording ports');
54
- this.portUdpH264 ??= ports.shift();
56
+ this.portUdpVideo ??= ports.shift();
55
57
  this.portUdpOpus ??= ports.shift();
56
58
  })();
57
59
  }
@@ -88,10 +90,25 @@ class Recorder extends EventEmitter {
88
90
  if (this.destroyed) return;
89
91
  // Keep stdin available for FFmpeg's graceful quit command. Reading SDP from
90
92
  // stdin and closing it leaves no way to write the Matroska trailer on Windows.
91
- const sdpData = Util.getSDPCodecName(this.portUdpH264, this.portUdpH265, this.portUdpOpus).replace(
92
- /;sprop-parameter-sets=[^;\r\n]+/,
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`,
93
110
  '',
94
- );
111
+ ].join('\r\n');
95
112
  this._directory = fs.mkdtempSync(path.join(os.tmpdir(), 'rockcord-recorder-'));
96
113
  this._sdpPath = path.join(this._directory, 'input.sdp');
97
114
  fs.writeFileSync(this._sdpPath, sdpData);
@@ -176,7 +193,7 @@ class Recorder extends EventEmitter {
176
193
  probe.bind(port, '127.0.0.1', () => probe.close(() => resolve(false)));
177
194
  });
178
195
  for (let attempt = 0; attempt < 120 && !this.destroyed; attempt++) {
179
- const ports = await Promise.all([bound(this.portUdpH264), bound(this.portUdpOpus)]);
196
+ const ports = await Promise.all([bound(this.portUdpVideo), bound(this.portUdpOpus)]);
180
197
  if (ports.every(Boolean)) {
181
198
  this.ready = true;
182
199
  this.emit('ready', this);
@@ -206,15 +223,28 @@ class Recorder extends EventEmitter {
206
223
  const message = payload.serialize();
207
224
  // Get port from payloadType
208
225
  let port;
226
+ let kind;
209
227
  if (payload.header.payloadType === Util.getPayloadType('opus')) {
228
+ kind = 'audio';
210
229
  port = this.portUdpOpus;
211
230
  this._lastAudioHeader = payload.header;
212
- } else if (payload.header.payloadType === Util.getPayloadType('H264')) {
213
- port = this.portUdpH264;
231
+ } else if (payload.header.payloadType === Util.getPayloadType(this.codec)) {
232
+ kind = 'video';
233
+ port = this.portUdpVideo;
214
234
  this._lastVideoHeader = payload.header;
215
235
  } else {
216
236
  return;
217
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);
218
248
  this.socket.send(message, 0, message.length, port, '127.0.0.1', callback);
219
249
  }
220
250
 
@@ -227,7 +257,7 @@ class Recorder extends EventEmitter {
227
257
  // RTCP BYE ends both local RTP inputs. Unlike an immediate quit, EOF lets
228
258
  // FFmpeg drain its parser and muxer, preserving the final buffered frame.
229
259
  for (const [port, ssrc] of [
230
- [this.portUdpH264, this._lastVideoHeader?.ssrc ?? 1],
260
+ [this.portUdpVideo, this._lastVideoHeader?.ssrc ?? 1],
231
261
  [this.portUdpOpus, this._lastAudioHeader?.ssrc ?? 2],
232
262
  ]) {
233
263
  // Include an empty, padded reason: FFmpeg's RTP demuxer requires at
@@ -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
@@ -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
  }
@@ -121,7 +121,6 @@ class ApplicationCommand extends Base {
121
121
  this.options ??= [];
122
122
  }
123
123
 
124
- /* eslint-disable max-len */
125
124
  if ('default_permission' in data) {
126
125
  /**
127
126
  * Whether the command is enabled by default when the app is added to a guild
@@ -130,7 +129,6 @@ class ApplicationCommand extends Base {
130
129
  */
131
130
  this.defaultPermission = data.default_permission;
132
131
  }
133
- /* eslint-disable max-len */
134
132
 
135
133
  if ('default_member_permissions' in data) {
136
134
  /**
@@ -307,7 +305,6 @@ class ApplicationCommand extends Base {
307
305
  return this.edit({ descriptionLocalizations });
308
306
  }
309
307
 
310
- /* eslint-disable max-len */
311
308
  /**
312
309
  * Edits the default permission of this ApplicationCommand
313
310
  * @param {boolean} [defaultPermission=true] The default permission for this command
@@ -317,7 +314,6 @@ class ApplicationCommand extends Base {
317
314
  setDefaultPermission(defaultPermission = true) {
318
315
  return this.edit({ defaultPermission });
319
316
  }
320
- /* eslint-enable max-len */
321
317
 
322
318
  /**
323
319
  * Edits the default member permissions of this ApplicationCommand
@@ -568,9 +564,9 @@ class ApplicationCommand extends Base {
568
564
  options: option.options?.map(o => this.transformOption(o, received)),
569
565
  [channelTypesKey]: received
570
566
  ? option.channel_types?.map(type => ChannelTypes[type])
571
- : option.channelTypes?.map(type => (typeof type === 'string' ? ChannelTypes[type] : type)) ??
567
+ : (option.channelTypes?.map(type => (typeof type === 'string' ? ChannelTypes[type] : type)) ??
572
568
  // When transforming to API data, accept API data
573
- option.channel_types,
569
+ option.channel_types),
574
570
  [minValueKey]: option.minValue ?? option.min_value,
575
571
  [maxValueKey]: option.maxValue ?? option.max_value,
576
572
  [minLengthKey]: option.minLength ?? option.min_length,
@@ -581,7 +577,6 @@ class ApplicationCommand extends Base {
581
577
 
582
578
  module.exports = ApplicationCommand;
583
579
 
584
- /* eslint-disable max-len */
585
580
  /**
586
581
  * @external APIApplicationCommand
587
582
  * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure}
@@ -189,7 +189,7 @@ class BaseCommandInteraction extends Interaction {
189
189
  }
190
190
 
191
191
  // These are here only for documentation purposes - they are implemented by InteractionResponses
192
- /* eslint-disable no-empty-function */
192
+
193
193
  deferReply() {}
194
194
  reply() {}
195
195
  fetchReply() {}
@@ -204,7 +204,6 @@ InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'updat
204
204
 
205
205
  module.exports = BaseCommandInteraction;
206
206
 
207
- /* eslint-disable max-len */
208
207
  /**
209
208
  * @external APIInteractionDataResolved
210
209
  * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure}
@@ -173,7 +173,7 @@ class BaseGuildTextChannel extends GuildChannel {
173
173
  }
174
174
 
175
175
  // These are here only for documentation purposes - they are implemented by TextBasedChannel
176
- /* eslint-disable no-empty-function */
176
+
177
177
  get lastMessage() {}
178
178
  get lastPinAt() {}
179
179
  send() {}
@@ -224,7 +224,7 @@ class BaseGuildVoiceChannel extends GuildChannel {
224
224
  }
225
225
 
226
226
  // These are here only for documentation purposes - they are implemented by TextBasedChannel
227
- /* eslint-disable no-empty-function */
227
+
228
228
  get lastMessage() {}
229
229
  send() {}
230
230
  sendTyping() {}
@@ -70,7 +70,6 @@ class ClientPresence extends Presence {
70
70
 
71
71
  module.exports = ClientPresence;
72
72
 
73
- /* eslint-disable max-len */
74
73
  /**
75
74
  * @external APIPresence
76
75
  * @see {@link https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields}
@@ -15,11 +15,19 @@ const Util = require('../util/Util');
15
15
  */
16
16
  class ClientUser extends User {
17
17
  #packageName = null;
18
- #intervalSamsungPresence = setInterval(() => {
19
- this.client.emit('debug', `[UPDATE] Samsung Presence: ${this.#packageName}`);
20
- if (!this.#packageName) return;
21
- this.setSamsungActivity(this.#packageName, 'UPDATE');
22
- }, 1000 * 60 * 10).unref();
18
+ #intervalSamsungPresence = setInterval(
19
+ () => {
20
+ this.client.emit('debug', `[UPDATE] Samsung Presence: ${this.#packageName}`);
21
+ if (!this.#packageName) return;
22
+ this.setSamsungActivity(this.#packageName, 'UPDATE').catch(error => this.client.emit('debug', error));
23
+ },
24
+ 1000 * 60 * 10,
25
+ ).unref();
26
+
27
+ _stopSamsungPresence() {
28
+ clearInterval(this.#intervalSamsungPresence);
29
+ this.#packageName = null;
30
+ }
23
31
 
24
32
  _patch(data) {
25
33
  super._patch(data);
@@ -49,7 +49,7 @@ class ContainerComponent extends BaseMessageComponent {
49
49
  get hexAccentColor() {
50
50
  return typeof this.accentColor === 'number'
51
51
  ? `#${this.accentColor.toString(16).padStart(6, '0')}`
52
- : this.accentColor ?? null;
52
+ : (this.accentColor ?? null);
53
53
  }
54
54
 
55
55
  /**