djs-selfbot-v13 3.7.32 → 3.7.33
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/package.json +1 -1
- package/src/client/Client.js +74 -1
- package/src/client/voice/ClientVoiceManager.js +84 -17
- package/src/client/voice/StreamSession.js +168 -0
- package/src/client/voice/VoiceConnection.js +265 -19
- package/src/client/voice/dispatcher/AnnexBDispatcher.js +64 -11
- package/src/client/voice/dispatcher/BaseDispatcher.js +13 -9
- package/src/client/voice/dispatcher/VideoDispatcher.js +33 -0
- package/src/client/voice/networking/DAVESession.js +234 -0
- package/src/client/voice/networking/VoiceWebSocket.js +284 -24
- package/src/client/voice/player/MediaPlayer.js +3 -1
- package/src/client/voice/player/processing/AnnexBBitstreamReaderWriter.js +137 -0
- package/src/client/voice/player/processing/SPSVUIRewriter.js +203 -0
- package/src/client/voice/receiver/PacketHandler.js +11 -4
- package/src/errors/Messages.js +6 -1
- package/src/util/Constants.js +12 -0
- package/typings/index.d.ts +29 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
const Davey = require('@snazzah/davey');
|
|
5
|
+
|
|
6
|
+
const TRANSITION_EXPIRY = 10;
|
|
7
|
+
const TRANSITION_EXPIRY_PENDING_DOWNGRADE = 24;
|
|
8
|
+
const DEFAULT_DECRYPTION_FAILURE_TOLERANCE = 36;
|
|
9
|
+
|
|
10
|
+
const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);
|
|
11
|
+
|
|
12
|
+
function getMaxProtocolVersion() {
|
|
13
|
+
return Davey.DAVE_PROTOCOL_VERSION ?? 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class DAVESession extends EventEmitter {
|
|
17
|
+
constructor(protocolVersion, userId, channelId, options = {}) {
|
|
18
|
+
super();
|
|
19
|
+
this.protocolVersion = protocolVersion;
|
|
20
|
+
this.userId = userId;
|
|
21
|
+
this.channelId = channelId;
|
|
22
|
+
this.failureTolerance = options.decryptionFailureTolerance ?? DEFAULT_DECRYPTION_FAILURE_TOLERANCE;
|
|
23
|
+
this.pendingTransitions = new Map();
|
|
24
|
+
this.downgraded = false;
|
|
25
|
+
this.consecutiveFailures = 0;
|
|
26
|
+
this.reinitializing = false;
|
|
27
|
+
this.lastTransitionId = undefined;
|
|
28
|
+
this.session = undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get voicePrivacyCode() {
|
|
32
|
+
if (this.protocolVersion === 0 || !this.session?.voicePrivacyCode) return null;
|
|
33
|
+
return this.session.voicePrivacyCode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getVerificationCode(userId) {
|
|
37
|
+
if (!this.session) throw new Error('Session not available');
|
|
38
|
+
return this.session.getVerificationCode(userId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
reinit() {
|
|
42
|
+
if (this.protocolVersion > 0) {
|
|
43
|
+
if (this.session) {
|
|
44
|
+
this.session.reinit(this.protocolVersion, this.userId, this.channelId);
|
|
45
|
+
this.emit('debug', `Session reinitialized for protocol version ${this.protocolVersion}`);
|
|
46
|
+
} else {
|
|
47
|
+
this.session = new Davey.DAVESession(this.protocolVersion, this.userId, this.channelId);
|
|
48
|
+
this.emit('debug', `Session initialized for protocol version ${this.protocolVersion}`);
|
|
49
|
+
}
|
|
50
|
+
this.emit('keyPackage', this.session.getSerializedKeyPackage());
|
|
51
|
+
} else if (this.session) {
|
|
52
|
+
this.session.reset();
|
|
53
|
+
this.session.setPassthroughMode(true, TRANSITION_EXPIRY);
|
|
54
|
+
this.emit('debug', 'Session reset');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setExternalSender(externalSender) {
|
|
59
|
+
if (!this.session) throw new Error('No session available');
|
|
60
|
+
try {
|
|
61
|
+
this.session.setExternalSender(externalSender);
|
|
62
|
+
this.emit('debug', 'Set MLS external sender');
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (String(error).includes('AlreadyInGroup')) {
|
|
65
|
+
this.emit('debug', 'MLS external sender already set, skipping');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
prepareTransition(data) {
|
|
73
|
+
this.emit('debug', `Preparing for transition (${data.transition_id}, v${data.protocol_version})`);
|
|
74
|
+
this.pendingTransitions.set(data.transition_id, data.protocol_version);
|
|
75
|
+
|
|
76
|
+
if (data.transition_id === 0) {
|
|
77
|
+
this.executeTransition(data.transition_id);
|
|
78
|
+
} else {
|
|
79
|
+
if (data.protocol_version === 0) this.session?.setPassthroughMode(true, TRANSITION_EXPIRY_PENDING_DOWNGRADE);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
executeTransition(transitionId) {
|
|
87
|
+
this.emit('debug', `Executing transition (${transitionId})`);
|
|
88
|
+
if (!this.pendingTransitions.has(transitionId)) {
|
|
89
|
+
this.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const oldVersion = this.protocolVersion;
|
|
94
|
+
this.protocolVersion = this.pendingTransitions.get(transitionId);
|
|
95
|
+
|
|
96
|
+
if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
|
|
97
|
+
this.downgraded = true;
|
|
98
|
+
this.emit('debug', 'Session downgraded');
|
|
99
|
+
} else if (transitionId > 0 && this.downgraded) {
|
|
100
|
+
this.downgraded = false;
|
|
101
|
+
this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
|
|
102
|
+
this.emit('debug', 'Session upgraded');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.reinitializing = false;
|
|
106
|
+
this.lastTransitionId = transitionId;
|
|
107
|
+
this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
|
|
108
|
+
this.pendingTransitions.delete(transitionId);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
prepareEpoch(data) {
|
|
113
|
+
this.emit('debug', `Preparing for epoch (${data.epoch})`);
|
|
114
|
+
if (data.epoch === 1) {
|
|
115
|
+
this.protocolVersion = data.protocol_version;
|
|
116
|
+
this.reinit();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
recoverFromInvalidTransition(transitionId) {
|
|
121
|
+
if (this.reinitializing) return;
|
|
122
|
+
this.emit('debug', `Invalidating transition ${transitionId}`);
|
|
123
|
+
this.reinitializing = true;
|
|
124
|
+
this.consecutiveFailures = 0;
|
|
125
|
+
this.emit('invalidateTransition', transitionId);
|
|
126
|
+
this.reinit();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
processProposals(payload, connectedClients) {
|
|
130
|
+
if (!this.session) throw new Error('No session available');
|
|
131
|
+
try {
|
|
132
|
+
const optype = payload.readUInt8(0);
|
|
133
|
+
const { commit, welcome } = this.session.processProposals(
|
|
134
|
+
optype,
|
|
135
|
+
payload.subarray(1),
|
|
136
|
+
Array.from(connectedClients),
|
|
137
|
+
);
|
|
138
|
+
this.emit('debug', 'MLS proposals processed');
|
|
139
|
+
if (!commit) return;
|
|
140
|
+
return welcome ? Buffer.concat([commit, welcome]) : commit;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.emit('debug', `MLS proposals errored: ${error}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
processCommit(payload) {
|
|
148
|
+
if (!this.session) throw new Error('No session available');
|
|
149
|
+
const transitionId = payload.readUInt16BE(0);
|
|
150
|
+
try {
|
|
151
|
+
this.session.processCommit(payload.subarray(2));
|
|
152
|
+
if (transitionId === 0) {
|
|
153
|
+
this.reinitializing = false;
|
|
154
|
+
this.lastTransitionId = transitionId;
|
|
155
|
+
} else {
|
|
156
|
+
this.pendingTransitions.set(transitionId, this.protocolVersion);
|
|
157
|
+
}
|
|
158
|
+
this.emit('debug', `MLS commit processed (transition id: ${transitionId})`);
|
|
159
|
+
return { transitionId, success: true };
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this.emit('debug', `MLS commit errored from transition ${transitionId}: ${error}`);
|
|
162
|
+
this.recoverFromInvalidTransition(transitionId);
|
|
163
|
+
return { transitionId, success: false };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
processWelcome(payload) {
|
|
168
|
+
if (!this.session) throw new Error('No session available');
|
|
169
|
+
const transitionId = payload.readUInt16BE(0);
|
|
170
|
+
try {
|
|
171
|
+
this.session.processWelcome(payload.subarray(2));
|
|
172
|
+
if (transitionId === 0) {
|
|
173
|
+
this.reinitializing = false;
|
|
174
|
+
this.lastTransitionId = transitionId;
|
|
175
|
+
} else {
|
|
176
|
+
this.pendingTransitions.set(transitionId, this.protocolVersion);
|
|
177
|
+
}
|
|
178
|
+
this.emit('debug', `MLS welcome processed (transition id: ${transitionId})`);
|
|
179
|
+
return { transitionId, success: true };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.emit('debug', `MLS welcome errored from transition ${transitionId}: ${error}`);
|
|
182
|
+
this.recoverFromInvalidTransition(transitionId);
|
|
183
|
+
return { transitionId, success: false };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
encrypt(packet) {
|
|
188
|
+
if (this.protocolVersion === 0 || !this.session?.ready || packet.equals(SILENCE_FRAME)) return packet;
|
|
189
|
+
return this.session.encryptOpus(packet);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
encryptVideo(packet, codec = 'H264') {
|
|
193
|
+
if (this.protocolVersion === 0 || !this.session?.ready) return packet;
|
|
194
|
+
const codecMap = {
|
|
195
|
+
H264: Davey.Codec.H264,
|
|
196
|
+
VP8: Davey.Codec.VP8,
|
|
197
|
+
H265: Davey.Codec.H265,
|
|
198
|
+
AV1: Davey.Codec.AV1,
|
|
199
|
+
};
|
|
200
|
+
return this.session.encrypt(Davey.MediaType.VIDEO, codecMap[codec] ?? Davey.Codec.UNKNOWN, packet);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
decrypt(packet, userId) {
|
|
204
|
+
const canDecrypt = this.session?.ready && (this.protocolVersion !== 0 || this.session?.canPassthrough(userId));
|
|
205
|
+
if (packet.equals(SILENCE_FRAME) || !canDecrypt || !this.session) return packet;
|
|
206
|
+
try {
|
|
207
|
+
const buffer = this.session.decrypt(userId, Davey.MediaType.AUDIO, packet);
|
|
208
|
+
this.consecutiveFailures = 0;
|
|
209
|
+
return buffer;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (!this.reinitializing && this.pendingTransitions.size === 0) {
|
|
212
|
+
this.consecutiveFailures++;
|
|
213
|
+
this.emit('debug', `Failed to decrypt a packet (${this.consecutiveFailures} consecutive fails)`);
|
|
214
|
+
if (this.consecutiveFailures > this.failureTolerance) {
|
|
215
|
+
if (this.lastTransitionId) this.recoverFromInvalidTransition(this.lastTransitionId);
|
|
216
|
+
else throw error;
|
|
217
|
+
}
|
|
218
|
+
} else if (this.reinitializing) {
|
|
219
|
+
this.emit('debug', 'Failed to decrypt a packet (reinitializing session)');
|
|
220
|
+
} else if (this.pendingTransitions.size > 0) {
|
|
221
|
+
this.emit('debug', `Failed to decrypt a packet (${this.pendingTransitions.size} pending transition[s])`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
destroy() {
|
|
228
|
+
try {
|
|
229
|
+
this.session?.reset();
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = { DAVESession, getMaxProtocolVersion };
|
|
@@ -4,7 +4,8 @@ const EventEmitter = require('events');
|
|
|
4
4
|
const { setTimeout, setInterval } = require('node:timers');
|
|
5
5
|
const WebSocket = require('../../../WebSocket');
|
|
6
6
|
const { Error } = require('../../../errors');
|
|
7
|
-
const { Opcodes, VoiceOpcodes } = require('../../../util/Constants');
|
|
7
|
+
const { Opcodes, VoiceOpcodes, VoiceStatus } = require('../../../util/Constants');
|
|
8
|
+
const { DAVESession, getMaxProtocolVersion } = require('./DAVESession');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Represents a Voice Connection's WebSocket.
|
|
@@ -29,6 +30,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
29
30
|
this._sequenceNumber = -1;
|
|
30
31
|
|
|
31
32
|
this.dead = false;
|
|
33
|
+
this._identified = false;
|
|
32
34
|
this.connection.on('closing', this.shutdown.bind(this));
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -57,6 +59,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
57
59
|
this.ws = null;
|
|
58
60
|
}
|
|
59
61
|
this.clearHeartbeat();
|
|
62
|
+
this._identified = false;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/**
|
|
@@ -67,7 +70,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
67
70
|
if (this.dead) return;
|
|
68
71
|
if (this.ws) this.reset();
|
|
69
72
|
if (this.attempts >= 5) {
|
|
70
|
-
this.emit('
|
|
73
|
+
this.emit('error', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts));
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -87,13 +90,14 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
87
90
|
|
|
88
91
|
/**
|
|
89
92
|
* Sends data to the WebSocket if it is open.
|
|
90
|
-
* @param {string} data The data to send to the WebSocket
|
|
91
|
-
* @returns {Promise<string>}
|
|
93
|
+
* @param {string|Buffer} data The data to send to the WebSocket
|
|
94
|
+
* @returns {Promise<string|Buffer>}
|
|
92
95
|
*/
|
|
93
96
|
send(data) {
|
|
94
|
-
|
|
97
|
+
const preview = typeof data === 'string' ? data : `[bin] ${data.byteLength} bytes`;
|
|
98
|
+
this.emit('debug', `[WS] >> ${preview}`);
|
|
95
99
|
return new Promise((resolve, reject) => {
|
|
96
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN',
|
|
100
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', preview);
|
|
97
101
|
this.ws.send(data, null, error => {
|
|
98
102
|
if (error) reject(error);
|
|
99
103
|
else resolve(data);
|
|
@@ -111,23 +115,46 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
111
115
|
return this.send(packet);
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Sends a binary message over the WebSocket.
|
|
120
|
+
* @param {number} opcode The opcode to use
|
|
121
|
+
* @param {Buffer} payload The payload to send
|
|
122
|
+
* @returns {Promise<Buffer>}
|
|
123
|
+
*/
|
|
124
|
+
sendBinaryMessage(opcode, payload) {
|
|
125
|
+
const message = Buffer.concat([Buffer.from([opcode]), payload]);
|
|
126
|
+
return this.send(message);
|
|
127
|
+
}
|
|
128
|
+
|
|
114
129
|
/**
|
|
115
130
|
* Called whenever the WebSocket opens.
|
|
116
131
|
*/
|
|
117
132
|
onOpen() {
|
|
118
133
|
this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
this.
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
sendIdentify() {
|
|
137
|
+
if (this._identified) return Promise.resolve();
|
|
138
|
+
this._identified = true;
|
|
139
|
+
|
|
140
|
+
const isStream = this.connection.constructor.name === 'StreamConnection';
|
|
141
|
+
const data = {
|
|
142
|
+
server_id: this.connection.serverId || this.connection.channel.guild?.id || this.connection.channel.id,
|
|
143
|
+
user_id: this.client.user.id,
|
|
144
|
+
token: this.connection.authentication.token,
|
|
145
|
+
session_id: this.connection.authentication.sessionId,
|
|
146
|
+
max_dave_protocol_version: getMaxProtocolVersion(),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (isStream) {
|
|
150
|
+
data.channel_id = this.connection.channel.id;
|
|
151
|
+
data.streams = [{ type: 'screen', rid: '100', quality: 100 }];
|
|
152
|
+
data.video = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return this.sendPacket({
|
|
156
|
+
op: VoiceOpcodes.IDENTIFY,
|
|
157
|
+
d: data,
|
|
131
158
|
});
|
|
132
159
|
}
|
|
133
160
|
|
|
@@ -138,18 +165,151 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
138
165
|
*/
|
|
139
166
|
onMessage(event) {
|
|
140
167
|
try {
|
|
141
|
-
|
|
168
|
+
const { data } = event;
|
|
169
|
+
if (data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
|
|
170
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
171
|
+
const seq = buffer.readUInt16BE(0);
|
|
172
|
+
const op = buffer.readUInt8(2);
|
|
173
|
+
const payload = buffer.subarray(3);
|
|
174
|
+
this._sequenceNumber = seq;
|
|
175
|
+
this.emit('debug', `[WS] << [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes`);
|
|
176
|
+
return this.onBinaryMessage({ op, seq, payload });
|
|
177
|
+
}
|
|
178
|
+
return this.onPacket(WebSocket.unpack(data, 'json'));
|
|
142
179
|
} catch (error) {
|
|
143
180
|
return this.onError(error);
|
|
144
181
|
}
|
|
145
182
|
}
|
|
146
183
|
|
|
184
|
+
usesSharedDaveSession() {
|
|
185
|
+
return (
|
|
186
|
+
this.connection.constructor.name === 'StreamConnection' &&
|
|
187
|
+
this.connection.voiceConnection?.dave === this.connection.dave
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onBinaryMessage(message) {
|
|
192
|
+
const { dave } = this.connection;
|
|
193
|
+
if (!dave) return;
|
|
194
|
+
if (this.usesSharedDaveSession()) {
|
|
195
|
+
this.emit('debug', `[WS] << [bin] opcode ${message.op} ignored (shared MLS session)`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
switch (message.op) {
|
|
200
|
+
case VoiceOpcodes.DAVE_MLS_EXTERNAL_SENDER:
|
|
201
|
+
dave.setExternalSender(message.payload);
|
|
202
|
+
break;
|
|
203
|
+
case VoiceOpcodes.DAVE_MLS_PROPOSALS: {
|
|
204
|
+
const payload = dave.processProposals(message.payload, this.connection.connectedClients);
|
|
205
|
+
if (payload) this.sendBinaryMessage(VoiceOpcodes.DAVE_MLS_COMMIT_WELCOME, payload);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case VoiceOpcodes.DAVE_MLS_ANNOUNCE_COMMIT_TRANSITION: {
|
|
209
|
+
const { transitionId, success } = dave.processCommit(message.payload);
|
|
210
|
+
if (success) {
|
|
211
|
+
if (transitionId === 0) {
|
|
212
|
+
this.emit('transitioned', transitionId);
|
|
213
|
+
} else {
|
|
214
|
+
this.sendPacket({
|
|
215
|
+
op: VoiceOpcodes.DAVE_TRANSITION_READY,
|
|
216
|
+
d: { transition_id: transitionId },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case VoiceOpcodes.DAVE_MLS_WELCOME: {
|
|
223
|
+
const { transitionId, success } = dave.processWelcome(message.payload);
|
|
224
|
+
if (success) {
|
|
225
|
+
if (transitionId === 0) {
|
|
226
|
+
this.emit('transitioned', transitionId);
|
|
227
|
+
} else {
|
|
228
|
+
this.sendPacket({
|
|
229
|
+
op: VoiceOpcodes.DAVE_TRANSITION_READY,
|
|
230
|
+
d: { transition_id: transitionId },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
default:
|
|
237
|
+
this.emit('unknownPacket', message);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
147
241
|
/**
|
|
148
242
|
* Called whenever the connection to the WebSocket server is lost.
|
|
149
243
|
* @param {CloseEvent} event The WebSocket close event
|
|
150
244
|
*/
|
|
151
245
|
onClose(event) {
|
|
152
246
|
this.emit('debug', `[WS] closed with code ${event.code} and reason: ${event.reason}`);
|
|
247
|
+
if (event.code === 4017) {
|
|
248
|
+
this.dead = true;
|
|
249
|
+
this.emit(
|
|
250
|
+
'error',
|
|
251
|
+
new Error('VOICE_DAVE_REQUIRED', 'Discord requires DAVE/E2EE protocol support. Ensure @snazzah/davey is installed.'),
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (event.code === 4006) {
|
|
256
|
+
const isStream = this.connection.constructor.name === 'StreamConnection';
|
|
257
|
+
this.connection._sessionRetries ??= 0;
|
|
258
|
+
if (this.connection._sessionRetries < 3) {
|
|
259
|
+
this.connection._sessionRetries++;
|
|
260
|
+
this._identified = false;
|
|
261
|
+
this.reset();
|
|
262
|
+
this.connection.authentication = {};
|
|
263
|
+
this.connection.status = VoiceStatus.AUTHENTICATING;
|
|
264
|
+
if (isStream) {
|
|
265
|
+
this.connection.serverId = null;
|
|
266
|
+
if (this.connection.sockets.ws) {
|
|
267
|
+
this.connection.sockets.ws.shutdown();
|
|
268
|
+
this.connection.sockets.ws = null;
|
|
269
|
+
}
|
|
270
|
+
if (this.connection.sockets.udp) {
|
|
271
|
+
this.connection.sockets.udp.shutdown();
|
|
272
|
+
this.connection.sockets.udp = null;
|
|
273
|
+
}
|
|
274
|
+
if (this.connection.voiceConnection) {
|
|
275
|
+
this.connection.voiceConnection.streamConnection = this.connection;
|
|
276
|
+
}
|
|
277
|
+
clearTimeout(this.connection.connectTimeout);
|
|
278
|
+
this.connection.connectTimeout = setTimeout(
|
|
279
|
+
() => this.connection.authenticateFailed('VOICE_CONNECTION_TIMEOUT'),
|
|
280
|
+
30_000,
|
|
281
|
+
).unref();
|
|
282
|
+
this.connection.channel.client.ws.broadcast({
|
|
283
|
+
op: Opcodes.STREAM_DELETE,
|
|
284
|
+
d: { stream_key: this.connection.streamKey },
|
|
285
|
+
});
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
this.connection.sendSignalScreenshare();
|
|
288
|
+
}, 1500).unref();
|
|
289
|
+
} else {
|
|
290
|
+
if (this.connection.sockets.ws) {
|
|
291
|
+
this.connection.sockets.ws.shutdown();
|
|
292
|
+
this.connection.sockets.ws = null;
|
|
293
|
+
}
|
|
294
|
+
if (this.connection.sockets.udp) {
|
|
295
|
+
this.connection.sockets.udp.shutdown();
|
|
296
|
+
this.connection.sockets.udp = null;
|
|
297
|
+
}
|
|
298
|
+
this.connection.status = VoiceStatus.RECONNECTING;
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
if (this.connection.authentication.token && this.connection.authentication.endpoint) {
|
|
301
|
+
this.connection.connect();
|
|
302
|
+
} else {
|
|
303
|
+
this.connection.sendVoiceStateUpdate();
|
|
304
|
+
}
|
|
305
|
+
}, 500).unref();
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.dead = true;
|
|
310
|
+
this.emit('error', new Error('VOICE_SESSION_EXPIRED'));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
153
313
|
if (!this.dead) setTimeout(this.connect.bind(this), this.attempts * 1000).unref();
|
|
154
314
|
}
|
|
155
315
|
|
|
@@ -162,6 +322,90 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
162
322
|
this.emit('error', error);
|
|
163
323
|
}
|
|
164
324
|
|
|
325
|
+
setupDaveSession(protocolVersion) {
|
|
326
|
+
const isStream = this.connection.constructor.name === 'StreamConnection';
|
|
327
|
+
const parentDave = this.connection.voiceConnection?.dave;
|
|
328
|
+
|
|
329
|
+
if (isStream && parentDave?.session) {
|
|
330
|
+
this.connection.dave = parentDave;
|
|
331
|
+
this.connection.connectedClients.add(this.client.user.id);
|
|
332
|
+
for (const id of this.connection.voiceConnection.connectedClients) {
|
|
333
|
+
this.connection.connectedClients.add(id);
|
|
334
|
+
}
|
|
335
|
+
this.emit('debug', '[DAVE] Reusing parent voice connection MLS session');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (this.connection.dave) {
|
|
340
|
+
const isSharedDave = isStream && this.connection.dave === parentDave;
|
|
341
|
+
if (!isSharedDave) {
|
|
342
|
+
this.connection.dave.destroy();
|
|
343
|
+
this.connection.dave.removeAllListeners();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!protocolVersion || getMaxProtocolVersion() === 0) {
|
|
348
|
+
this.connection.dave = null;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const session = new DAVESession(
|
|
353
|
+
protocolVersion,
|
|
354
|
+
this.client.user.id,
|
|
355
|
+
this.connection.channel.id,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
session.on('debug', msg => this.emit('debug', `[DAVE] ${msg}`));
|
|
359
|
+
session.on('keyPackage', keyPackage => {
|
|
360
|
+
this.sendBinaryMessage(VoiceOpcodes.DAVE_MLS_KEY_PACKAGE, keyPackage).catch(e => this.emit('error', e));
|
|
361
|
+
});
|
|
362
|
+
session.on('invalidateTransition', transitionId => {
|
|
363
|
+
this.sendPacket({
|
|
364
|
+
op: VoiceOpcodes.DAVE_MLS_INVALID_COMMIT_WELCOME,
|
|
365
|
+
d: { transition_id: transitionId },
|
|
366
|
+
}).catch(e => this.emit('error', e));
|
|
367
|
+
});
|
|
368
|
+
session.on('error', err => this.emit('error', err));
|
|
369
|
+
|
|
370
|
+
this.connection.dave = session;
|
|
371
|
+
this.connection.connectedClients.add(this.client.user.id);
|
|
372
|
+
if (this.connection.voiceConnection?.connectedClients) {
|
|
373
|
+
for (const id of this.connection.voiceConnection.connectedClients) {
|
|
374
|
+
this.connection.connectedClients.add(id);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
session.reinit();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
handleDavePacket(packet) {
|
|
381
|
+
const { dave } = this.connection;
|
|
382
|
+
if (!dave) return;
|
|
383
|
+
|
|
384
|
+
switch (packet.op) {
|
|
385
|
+
case VoiceOpcodes.DAVE_PREPARE_TRANSITION: {
|
|
386
|
+
const sendReady = dave.prepareTransition(packet.d);
|
|
387
|
+
if (sendReady) {
|
|
388
|
+
this.sendPacket({
|
|
389
|
+
op: VoiceOpcodes.DAVE_TRANSITION_READY,
|
|
390
|
+
d: { transition_id: packet.d.transition_id },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (packet.d.transition_id === 0) this.emit('transitioned', 0);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
case VoiceOpcodes.DAVE_EXECUTE_TRANSITION: {
|
|
397
|
+
const transitioned = dave.executeTransition(packet.d.transition_id);
|
|
398
|
+
if (transitioned) this.emit('transitioned', packet.d.transition_id);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case VoiceOpcodes.DAVE_PREPARE_EPOCH:
|
|
402
|
+
dave.prepareEpoch(packet.d);
|
|
403
|
+
break;
|
|
404
|
+
default:
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
165
409
|
/**
|
|
166
410
|
* Called whenever a valid packet is received from the WebSocket.
|
|
167
411
|
* @param {Object} packet The received packet
|
|
@@ -169,9 +413,24 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
169
413
|
onPacket(packet) {
|
|
170
414
|
this.emit('debug', `[WS] << ${JSON.stringify(packet)}`);
|
|
171
415
|
if (packet.seq) this._sequenceNumber = packet.seq;
|
|
416
|
+
|
|
417
|
+
if (
|
|
418
|
+
[
|
|
419
|
+
VoiceOpcodes.DAVE_PREPARE_TRANSITION,
|
|
420
|
+
VoiceOpcodes.DAVE_EXECUTE_TRANSITION,
|
|
421
|
+
VoiceOpcodes.DAVE_PREPARE_EPOCH,
|
|
422
|
+
].includes(packet.op)
|
|
423
|
+
) {
|
|
424
|
+
if (!this.usesSharedDaveSession()) this.handleDavePacket(packet);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
172
428
|
switch (packet.op) {
|
|
173
429
|
case VoiceOpcodes.HELLO:
|
|
174
430
|
this.setHeartbeat(packet.d.heartbeat_interval);
|
|
431
|
+
this.sendIdentify().catch(() => {
|
|
432
|
+
this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED'));
|
|
433
|
+
});
|
|
175
434
|
break;
|
|
176
435
|
case VoiceOpcodes.READY:
|
|
177
436
|
/**
|
|
@@ -180,11 +439,11 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
180
439
|
* @event VoiceWebSocket#ready
|
|
181
440
|
*/
|
|
182
441
|
this.emit('ready', packet.d);
|
|
183
|
-
this.connection.setVideoStatus(false);
|
|
184
442
|
break;
|
|
185
443
|
/* eslint-disable no-case-declarations */
|
|
186
444
|
case VoiceOpcodes.SESSION_DESCRIPTION:
|
|
187
445
|
packet.d.secret_key = new Uint8Array(packet.d.secret_key);
|
|
446
|
+
this.setupDaveSession(packet.d.dave_protocol_version ?? 0);
|
|
188
447
|
/**
|
|
189
448
|
* Emitted once the Voice Websocket receives a description of this voice session.
|
|
190
449
|
* @param {Object} packet The received packet
|
|
@@ -192,6 +451,9 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
192
451
|
*/
|
|
193
452
|
this.emit('sessionDescription', packet.d);
|
|
194
453
|
break;
|
|
454
|
+
case VoiceOpcodes.CLIENTS_CONNECT:
|
|
455
|
+
for (const id of packet.d.user_ids) this.connection.connectedClients.add(id);
|
|
456
|
+
break;
|
|
195
457
|
case VoiceOpcodes.CLIENT_CONNECT:
|
|
196
458
|
this.connection.ssrcMap.set(+packet.d.audio_ssrc, {
|
|
197
459
|
userId: packet.d.user_id,
|
|
@@ -200,6 +462,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
200
462
|
});
|
|
201
463
|
break;
|
|
202
464
|
case VoiceOpcodes.CLIENT_DISCONNECT:
|
|
465
|
+
this.connection.connectedClients.delete(packet.d.user_id);
|
|
203
466
|
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);
|
|
204
467
|
if (streamInfo) {
|
|
205
468
|
this.connection.receiver.packets.streams.delete(packet.d.user_id);
|
|
@@ -258,10 +521,7 @@ class VoiceWebSocket extends EventEmitter {
|
|
|
258
521
|
* Clears a heartbeat interval, if one exists.
|
|
259
522
|
*/
|
|
260
523
|
clearHeartbeat() {
|
|
261
|
-
if (!this.heartbeatInterval)
|
|
262
|
-
this.emit('warn', 'Tried to clear a heartbeat interval that does not exist');
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
524
|
+
if (!this.heartbeatInterval) return;
|
|
265
525
|
clearInterval(this.heartbeatInterval);
|
|
266
526
|
this.heartbeatInterval = null;
|
|
267
527
|
}
|
|
@@ -214,7 +214,9 @@ class MediaPlayer extends EventEmitter {
|
|
|
214
214
|
args.push(...FFMPEG_H265_ARGUMENTS(options));
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
if (options?.fps) {
|
|
218
|
+
args.push('-g', String(options.fps), '-keyint_min', String(options.fps), '-sc_threshold', '0');
|
|
219
|
+
}
|
|
218
220
|
|
|
219
221
|
if (options?.inputFFmpegArgs) {
|
|
220
222
|
args.unshift(...options.inputFFmpegArgs);
|