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.
- package/README.md +5 -3
- package/package.json +17 -14
- package/src/client/Client.js +3 -3
- package/src/client/WebhookClient.js +1 -1
- package/src/client/actions/VoiceStateUpdate.js +2 -2
- package/src/client/voice/VoiceConnection.js +98 -24
- package/src/client/voice/dispatcher/AudioDispatcher.js +1 -1
- package/src/client/voice/dispatcher/BaseDispatcher.js +49 -61
- package/src/client/voice/dispatcher/VideoDispatcher.js +86 -2
- package/src/client/voice/networking/CongestionControl.js +84 -0
- package/src/client/voice/networking/DAVE.js +26 -1
- package/src/client/voice/networking/MediaRecovery.js +300 -0
- package/src/client/voice/networking/VoiceUDPClient.js +10 -1
- package/src/client/voice/networking/VoiceWebSocket.js +4 -1
- package/src/client/voice/player/MediaPlayer.js +45 -11
- package/src/client/voice/player/processing/AnnexBNalSplitter.js +0 -1
- package/src/client/voice/receiver/AudioJitter.js +101 -0
- package/src/client/voice/receiver/PacketHandler.js +66 -9
- package/src/client/voice/receiver/Receiver.js +9 -4
- package/src/client/voice/receiver/Recorder.js +186 -57
- package/src/client/voice/util/PlayInterface.js +2 -0
- package/src/client/voice/util/Secretbox.js +1 -1
- package/src/client/voice/util/TransportCrypto.js +54 -0
- package/src/client/voice/util/VideoFrames.js +4 -1
- package/src/client/websocket/WebSocketShard.js +0 -1
- package/src/managers/ApplicationCommandManager.js +0 -2
- package/src/managers/ApplicationCommandPermissionsManager.js +0 -1
- package/src/managers/GuildChannelManager.js +2 -3
- package/src/managers/GuildMemberManager.js +0 -1
- package/src/managers/GuildTextThreadManager.js +1 -1
- package/src/managers/MessageManager.js +5 -5
- package/src/managers/RoleManager.js +1 -1
- package/src/rest/APIRequest.js +0 -1
- package/src/rest/APIRouter.js +2 -2
- package/src/rest/DiscordAPIError.js +1 -1
- package/src/rest/RequestHandler.js +3 -3
- package/src/sharding/ShardingManager.js +2 -2
- package/src/structures/ApplicationCommand.js +2 -7
- package/src/structures/BaseCommandInteraction.js +1 -2
- package/src/structures/BaseGuildTextChannel.js +1 -1
- package/src/structures/BaseGuildVoiceChannel.js +1 -1
- package/src/structures/ClientPresence.js +0 -1
- package/src/structures/ClientUser.js +13 -5
- package/src/structures/ContainerComponent.js +1 -1
- package/src/structures/DMChannel.js +1 -1
- package/src/structures/GroupDMChannel.js +4 -4
- package/src/structures/Guild.js +0 -2
- package/src/structures/GuildAuditLogs.js +4 -4
- package/src/structures/GuildScheduledEvent.js +0 -2
- package/src/structures/Interaction.js +1 -1
- package/src/structures/InteractionCollector.js +2 -2
- package/src/structures/InteractionWebhook.js +0 -1
- package/src/structures/Message.js +16 -17
- package/src/structures/MessageComponentInteraction.js +1 -1
- package/src/structures/MessageEmbed.js +0 -2
- package/src/structures/MessagePayload.js +2 -2
- package/src/structures/ModalSubmitInteraction.js +2 -2
- package/src/structures/ThreadChannel.js +3 -3
- package/src/structures/ThreadOnlyChannel.js +1 -1
- package/src/structures/WebEmbed.js +0 -1
- package/src/structures/interfaces/Application.js +2 -2
- package/src/structures/interfaces/Collector.js +1 -4
- package/src/structures/interfaces/TextBasedChannel.js +6 -6
- package/src/util/AttachmentFlags.js +0 -1
- package/src/util/Constants.js +0 -2
- package/src/util/Options.js +3 -3
- package/src/util/Util.js +1 -2
- package/typings/index.d.ts +109 -115
- 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.
|
|
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 (!
|
|
43
|
+
if (!this.portUdpVideo || !portUdpOpus) {
|
|
37
44
|
this.promise = (async () => {
|
|
38
|
-
const selected = new Set([this.
|
|
39
|
-
const missing = Number(!this.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
87
|
-
'
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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('
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
port = this.
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
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
|
|
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
|
);
|
|
@@ -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 (
|
|
143
|
-
|
|
144
|
-
|
|
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
|
/**
|
package/src/rest/APIRequest.js
CHANGED
package/src/rest/APIRouter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const noop = () => {};
|
|
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));
|
|
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);
|
|
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;
|
|
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);
|
|
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);
|
|
313
|
+
await Promise.all(promises);
|
|
314
314
|
}
|
|
315
315
|
return this.shards;
|
|
316
316
|
}
|