@sonata-sdk/voice 0.1.1 → 0.1.3

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.
@@ -19,6 +19,7 @@ export class VoiceConnection extends EventEmitter {
19
19
  #token = '';
20
20
  #endpoint = '';
21
21
  #destroyed = false;
22
+ #daveReady = false;
22
23
  constructor(opts) {
23
24
  super();
24
25
  this.guildId = opts.guildId;
@@ -26,6 +27,7 @@ export class VoiceConnection extends EventEmitter {
26
27
  this.channelId = opts.channelId;
27
28
  this.encryption = opts.encryption ?? null;
28
29
  this.#gateway = new VoiceGateway(opts.guildId, opts.userId);
30
+ this.#gateway.setDaveSession(opts.channelId);
29
31
  this.#udp = new UdpSocket();
30
32
  this.#setupListeners();
31
33
  }
@@ -36,14 +38,15 @@ export class VoiceConnection extends EventEmitter {
36
38
  }).catch((err) => this.emit('error', err));
37
39
  });
38
40
  this.#gateway.on('sessionDescription', (payload) => {
39
- this.#udp.setSecretKey(payload.secret_key);
41
+ const secretKey = Buffer.from(payload.secret_key);
42
+ this.#udp.setSecretKey(secretKey);
40
43
  this.udpInfo = {
41
44
  ssrc: this.#gateway.ssrc,
42
45
  ip: this.#gateway.ip,
43
46
  port: this.#gateway.port,
44
- secretKey: payload.secret_key,
47
+ secretKey,
45
48
  };
46
- this.#encryption = new AudioEncryption(payload.mode, payload.secret_key, this.#gateway.ssrc);
49
+ this.#encryption = new AudioEncryption(payload.mode, secretKey, this.#gateway.ssrc);
47
50
  this.state = { status: 'connected', reason: null, code: null };
48
51
  this.emit('stateChange', { status: 'connecting' }, this.state);
49
52
  this.emit('ready');
@@ -58,6 +61,60 @@ export class VoiceConnection extends EventEmitter {
58
61
  this.ping = Date.now() - d.t;
59
62
  });
60
63
  this.#udp.on('error', (err) => this.emit('error', err));
64
+ // DAVE handshake handlers
65
+ this.#gateway.on('dave_external_sender', (data) => {
66
+ const session = this.#gateway.daveSession;
67
+ if (!session)
68
+ return this.emit('error', new Error('No DAVE session'));
69
+ try {
70
+ session.setExternalSender(data);
71
+ const keyPackage = session.getSerializedKeyPackage();
72
+ this.#gateway.sendDaveOp(26, keyPackage); // dave_mls_key_package
73
+ }
74
+ catch (e) {
75
+ this.emit('error', new Error(`DAVE external sender: ${e.message}`));
76
+ }
77
+ });
78
+ this.#gateway.on('dave_proposals', (d) => {
79
+ const session = this.#gateway.daveSession;
80
+ if (!session)
81
+ return this.emit('error', new Error('No DAVE session'));
82
+ try {
83
+ const { operationType, proposals } = d;
84
+ const result = session.processProposals(operationType, Buffer.from(proposals));
85
+ if (result.commit && result.welcome) {
86
+ this.#gateway.sendDaveOp(28, Buffer.concat([result.commit, result.welcome]));
87
+ }
88
+ else if (result.commit) {
89
+ this.#gateway.sendDaveOp(28, result.commit);
90
+ }
91
+ }
92
+ catch (e) {
93
+ this.emit('error', new Error(`DAVE proposals: ${e.message}`));
94
+ }
95
+ });
96
+ this.#gateway.on('dave_commit', (data) => {
97
+ const session = this.#gateway.daveSession;
98
+ if (!session)
99
+ return this.emit('error', new Error('No DAVE session'));
100
+ try {
101
+ session.processCommit(data);
102
+ }
103
+ catch (e) {
104
+ this.emit('error', new Error(`DAVE commit: ${e.message}`));
105
+ }
106
+ });
107
+ this.#gateway.on('dave_welcome', (data) => {
108
+ const session = this.#gateway.daveSession;
109
+ if (!session)
110
+ return this.emit('error', new Error('No DAVE session'));
111
+ try {
112
+ session.processWelcome(data);
113
+ }
114
+ catch (e) {
115
+ this.emit('error', new Error(`DAVE welcome: ${e.message}`));
116
+ }
117
+ });
61
118
  }
62
119
  voiceStateUpdate(obj) {
63
120
  this.#sessionId = obj.sessionId ?? obj.session_id ?? this.#sessionId;
package/dist/gateway.d.ts CHANGED
@@ -26,11 +26,13 @@ export declare class VoiceGateway extends EventEmitter {
26
26
  get secretKey(): Buffer<ArrayBufferLike> | null;
27
27
  get daveProtocolVersion(): number;
28
28
  constructor(guildId: string, userId: string);
29
+ setDaveSession(channelId: string): void;
29
30
  voiceStateUpdate(sessionId: string): void;
30
31
  voiceServerUpdate(token: string, endpoint: string, channelId?: string): void;
31
32
  connect(): void;
32
33
  sendBinary(data: Buffer): void;
33
34
  sendSelectProtocol(address: string, port: number, mode: string): void;
35
+ sendDaveOp(op: number, data: Buffer): void;
34
36
  setSpeaking(value: number, delay?: number): void;
35
37
  close(): void;
36
38
  }
package/dist/gateway.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import WebSocket from 'ws';
2
2
  import { EventEmitter } from 'node:events';
3
+ import { DAVESession, DAVE_PROTOCOL_VERSION, generateP256Keypair } from '@snazzah/davey';
3
4
  const DISCORD_VOICE_VERSION = 8;
4
5
  const DISCORD_VOICE_URL_TEMPLATE = 'wss://{endpoint}/?v={v}';
5
6
  export class VoiceGateway extends EventEmitter {
@@ -19,6 +20,9 @@ export class VoiceGateway extends EventEmitter {
19
20
  #secretKey = null;
20
21
  #connected = false;
21
22
  #daveProtocolVersion = 0;
23
+ #daveSession = null;
24
+ #daveKeypair = null;
25
+ #pendingBinary = [];
22
26
  get connected() { return this.#connected; }
23
27
  get ssrc() { return this.#ssrc; }
24
28
  get ip() { return this.#ip; }
@@ -31,14 +35,27 @@ export class VoiceGateway extends EventEmitter {
31
35
  this.#guildId = guildId;
32
36
  this.#userId = userId;
33
37
  }
38
+ setDaveSession(channelId) {
39
+ try {
40
+ this.#daveKeypair = generateP256Keypair();
41
+ this.#channelId = channelId;
42
+ this.#daveSession = new DAVESession(Number(DAVE_PROTOCOL_VERSION), this.#userId, channelId, this.#daveKeypair);
43
+ }
44
+ catch (e) {
45
+ this.emit('error', new Error(`DAVE init failed: ${e.message}`));
46
+ }
47
+ }
34
48
  voiceStateUpdate(sessionId) {
35
49
  this.#sessionId = sessionId;
36
50
  }
37
51
  voiceServerUpdate(token, endpoint, channelId) {
38
52
  this.#token = token;
39
53
  this.#endpoint = endpoint.replace(/^wss:\/\//, '').replace(/\/\?v=\d+$/, '');
40
- if (channelId)
54
+ if (channelId) {
41
55
  this.#channelId = channelId;
56
+ if (!this.#daveSession)
57
+ this.setDaveSession(channelId);
58
+ }
42
59
  }
43
60
  connect() {
44
61
  const url = DISCORD_VOICE_URL_TEMPLATE
@@ -51,12 +68,15 @@ export class VoiceGateway extends EventEmitter {
51
68
  this.#ws.on('error', (err) => this.emit('error', err));
52
69
  }
53
70
  #onOpen() {
54
- this.#sendOp(0, {
71
+ const identify = {
55
72
  server_id: this.#guildId,
56
73
  user_id: this.#userId,
57
74
  session_id: this.#sessionId,
58
75
  token: this.#token,
59
- });
76
+ max_dave_protocol_version: Number(DAVE_PROTOCOL_VERSION),
77
+ supported_dave_versions: [Number(DAVE_PROTOCOL_VERSION)],
78
+ };
79
+ this.#sendOp(0, identify);
60
80
  }
61
81
  #onMessage(data) {
62
82
  try {
@@ -64,12 +84,22 @@ export class VoiceGateway extends EventEmitter {
64
84
  this.#handleOp(json.op, json.d);
65
85
  }
66
86
  catch {
67
- // binary op (DAVE)
87
+ // binary op (DAVE protocol)
68
88
  if (data.length > 0) {
69
- this.emit('binary', data);
89
+ this.#handleDaveBinary(data);
70
90
  }
71
91
  }
72
92
  }
93
+ #handleDaveBinary(data) {
94
+ if (!this.#daveSession) {
95
+ this.#pendingBinary.push(data);
96
+ return;
97
+ }
98
+ // Try to parse DAVE binary protocol messages
99
+ // First byte after the opcode determines the message type
100
+ // DAVE messages from Discord: external sender package, proposals, commit, welcome
101
+ this.emit('binary', data);
102
+ }
73
103
  #handleOp(op, d) {
74
104
  switch (op) {
75
105
  case 2: { // Ready
@@ -106,6 +136,22 @@ export class VoiceGateway extends EventEmitter {
106
136
  this.emit('resumed');
107
137
  break;
108
138
  }
139
+ case 25: { // DAVE MLS External Sender Package
140
+ this.emit('dave_external_sender', Buffer.from(d));
141
+ break;
142
+ }
143
+ case 27: { // DAVE MLS Proposals
144
+ this.emit('dave_proposals', d);
145
+ break;
146
+ }
147
+ case 29: { // DAVE MLS Announce Commit Transition
148
+ this.emit('dave_commit', Buffer.from(d));
149
+ break;
150
+ }
151
+ case 30: { // DAVE MLS Welcome
152
+ this.emit('dave_welcome', Buffer.from(d));
153
+ break;
154
+ }
109
155
  }
110
156
  }
111
157
  #onClose(code, reason) {
@@ -139,6 +185,9 @@ export class VoiceGateway extends EventEmitter {
139
185
  data: { address, port, mode },
140
186
  });
141
187
  }
188
+ sendDaveOp(op, data) {
189
+ this.#sendOp(op, [...data]);
190
+ }
142
191
  setSpeaking(value, delay = 0) {
143
192
  this.#sendOp(5, { speaking: value, delay, ssrc: this.#ssrc });
144
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sonata-sdk/voice",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Discord voice connection library — WebSocket gateway, UDP, RTP, encryption, DAVE/MLS",
5
5
  "license": "MIT",
6
6
  "type": "module",