VoiceConnection.js (14658B)
1 'use strict'; 2 3 const EventEmitter = require('events'); 4 const VoiceUDP = require('./networking/VoiceUDPClient'); 5 const VoiceWebSocket = require('./networking/VoiceWebSocket'); 6 const AudioPlayer = require('./player/AudioPlayer'); 7 const VoiceReceiver = require('./receiver/Receiver'); 8 const PlayInterface = require('./util/PlayInterface'); 9 const Silence = require('./util/Silence'); 10 const { Error } = require('../../errors'); 11 const { OPCodes, VoiceOPCodes, VoiceStatus, Events } = require('../../util/Constants'); 12 const Speaking = require('../../util/Speaking'); 13 const Util = require('../../util/Util'); 14 15 // Workaround for Discord now requiring silence to be sent before being able to receive audio 16 class SingleSilence extends Silence { 17 _read() { 18 super._read(); 19 this.push(null); 20 } 21 } 22 23 const SUPPORTED_MODES = ['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305']; 24 25 /** 26 * Represents a connection to a guild's voice server. 27 * ```js 28 * // Obtained using: 29 * voiceChannel.join() 30 * .then(connection => { 31 * 32 * }); 33 * ``` 34 * @extends {EventEmitter} 35 * @implements {PlayInterface} 36 */ 37 class VoiceConnection extends EventEmitter { 38 constructor(voiceManager, channel) { 39 super(); 40 41 /** 42 * The voice manager that instantiated this connection 43 * @type {ClientVoiceManager} 44 */ 45 this.voiceManager = voiceManager; 46 47 /** 48 * The voice channel this connection is currently serving 49 * @type {VoiceChannel} 50 */ 51 this.channel = channel; 52 53 /** 54 * The current status of the voice connection 55 * @type {VoiceStatus} 56 */ 57 this.status = VoiceStatus.AUTHENTICATING; 58 59 /** 60 * Our current speaking state 61 * @type {Readonly<Speaking>} 62 */ 63 this.speaking = new Speaking().freeze(); 64 65 /** 66 * The authentication data needed to connect to the voice server 67 * @type {Object} 68 * @private 69 */ 70 this.authentication = {}; 71 72 /** 73 * The audio player for this voice connection 74 * @type {AudioPlayer} 75 */ 76 this.player = new AudioPlayer(this); 77 78 this.player.on('debug', m => { 79 /** 80 * Debug info from the connection. 81 * @event VoiceConnection#debug 82 * @param {string} message The debug message 83 */ 84 this.emit('debug', `audio player - ${m}`); 85 }); 86 87 this.player.on('error', e => { 88 /** 89 * Warning info from the connection. 90 * @event VoiceConnection#warn 91 * @param {string|Error} warning The warning 92 */ 93 this.emit('warn', e); 94 }); 95 96 this.once('closing', () => this.player.destroy()); 97 98 /** 99 * Map SSRC values to user IDs 100 * @type {Map<number, Snowflake>} 101 * @private 102 */ 103 this.ssrcMap = new Map(); 104 105 /** 106 * Tracks which users are talking 107 * @type {Map<Snowflake, Readonly<Speaking>>} 108 * @private 109 */ 110 this._speaking = new Map(); 111 112 /** 113 * Object that wraps contains the `ws` and `udp` sockets of this voice connection 114 * @type {Object} 115 * @private 116 */ 117 this.sockets = {}; 118 119 /** 120 * The voice receiver of this connection 121 * @type {VoiceReceiver} 122 */ 123 this.receiver = new VoiceReceiver(this); 124 } 125 126 /** 127 * The client that instantiated this connection 128 * @type {Client} 129 * @readonly 130 */ 131 get client() { 132 return this.voiceManager.client; 133 } 134 135 /** 136 * The current stream dispatcher (if any) 137 * @type {?StreamDispatcher} 138 * @readonly 139 */ 140 get dispatcher() { 141 return this.player.dispatcher; 142 } 143 144 /** 145 * Sets whether the voice connection should display as "speaking", "soundshare" or "none". 146 * @param {BitFieldResolvable} value The new speaking state 147 * @private 148 */ 149 setSpeaking(value) { 150 if (this.speaking.equals(value)) return; 151 if (this.status !== VoiceStatus.CONNECTED) return; 152 this.speaking = new Speaking(value).freeze(); 153 this.sockets.ws 154 .sendPacket({ 155 op: VoiceOPCodes.SPEAKING, 156 d: { 157 speaking: this.speaking.bitfield, 158 delay: 0, 159 ssrc: this.authentication.ssrc, 160 }, 161 }) 162 .catch(e => { 163 this.emit('debug', e); 164 }); 165 } 166 167 /** 168 * The voice state of this connection 169 * @type {VoiceState} 170 */ 171 get voice() { 172 return this.channel.guild.voice; 173 } 174 175 /** 176 * Sends a request to the main gateway to join a voice channel. 177 * @param {Object} [options] The options to provide 178 * @returns {Promise<Shard>} 179 * @private 180 */ 181 sendVoiceStateUpdate(options = {}) { 182 options = Util.mergeDefault( 183 { 184 guild_id: this.channel.guild.id, 185 channel_id: this.channel.id, 186 self_mute: this.voice ? this.voice.selfMute : false, 187 self_deaf: this.voice ? this.voice.selfDeaf : false, 188 }, 189 options, 190 ); 191 192 this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`); 193 194 return this.channel.guild.shard.send( 195 { 196 op: OPCodes.VOICE_STATE_UPDATE, 197 d: options, 198 }, 199 true, 200 ); 201 } 202 203 /** 204 * Set the token and endpoint required to connect to the voice servers. 205 * @param {string} token The voice token 206 * @param {string} endpoint The voice endpoint 207 * @private 208 * @returns {void} 209 */ 210 setTokenAndEndpoint(token, endpoint) { 211 this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`); 212 if (!endpoint) { 213 // Signifies awaiting endpoint stage 214 return; 215 } 216 217 if (!token) { 218 this.authenticateFailed('VOICE_TOKEN_ABSENT'); 219 return; 220 } 221 222 endpoint = endpoint.match(/([^:]*)/)[0]; 223 this.emit('debug', `Endpoint resolved as ${endpoint}`); 224 225 if (!endpoint) { 226 this.authenticateFailed('VOICE_INVALID_ENDPOINT'); 227 return; 228 } 229 230 if (this.status === VoiceStatus.AUTHENTICATING) { 231 this.authentication.token = token; 232 this.authentication.endpoint = endpoint; 233 this.checkAuthenticated(); 234 } else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) { 235 this.reconnect(token, endpoint); 236 } 237 } 238 239 /** 240 * Sets the Session ID for the connection. 241 * @param {string} sessionID The voice session ID 242 * @private 243 */ 244 setSessionID(sessionID) { 245 this.emit('debug', `Setting sessionID ${sessionID} (stored as "${this.authentication.sessionID}")`); 246 if (!sessionID) { 247 this.authenticateFailed('VOICE_SESSION_ABSENT'); 248 return; 249 } 250 251 if (this.status === VoiceStatus.AUTHENTICATING) { 252 this.authentication.sessionID = sessionID; 253 this.checkAuthenticated(); 254 } else if (sessionID !== this.authentication.sessionID) { 255 this.authentication.sessionID = sessionID; 256 /** 257 * Emitted when a new session ID is received. 258 * @event VoiceConnection#newSession 259 * @private 260 */ 261 this.emit('newSession', sessionID); 262 } 263 } 264 265 /** 266 * Checks whether the voice connection is authenticated. 267 * @private 268 */ 269 checkAuthenticated() { 270 const { token, endpoint, sessionID } = this.authentication; 271 this.emit('debug', `Authenticated with sessionID ${sessionID}`); 272 if (token && endpoint && sessionID) { 273 this.status = VoiceStatus.CONNECTING; 274 /** 275 * Emitted when we successfully initiate a voice connection. 276 * @event VoiceConnection#authenticated 277 */ 278 this.emit('authenticated'); 279 this.connect(); 280 } 281 } 282 283 /** 284 * Invoked when we fail to initiate a voice connection. 285 * @param {string} reason The reason for failure 286 * @private 287 */ 288 authenticateFailed(reason) { 289 this.client.clearTimeout(this.connectTimeout); 290 this.emit('debug', `Authenticate failed - ${reason}`); 291 if (this.status === VoiceStatus.AUTHENTICATING) { 292 /** 293 * Emitted when we fail to initiate a voice connection. 294 * @event VoiceConnection#failed 295 * @param {Error} error The encountered error 296 */ 297 this.emit('failed', new Error(reason)); 298 } else { 299 /** 300 * Emitted whenever the connection encounters an error. 301 * @event VoiceConnection#error 302 * @param {Error} error The encountered error 303 */ 304 this.emit('error', new Error(reason)); 305 } 306 this.status = VoiceStatus.DISCONNECTED; 307 } 308 309 /** 310 * Move to a different voice channel in the same guild. 311 * @param {VoiceChannel} channel The channel to move to 312 * @private 313 */ 314 updateChannel(channel) { 315 this.channel = channel; 316 this.sendVoiceStateUpdate(); 317 } 318 319 /** 320 * Attempts to authenticate to the voice server. 321 * @private 322 */ 323 authenticate() { 324 this.sendVoiceStateUpdate(); 325 this.connectTimeout = this.client.setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 15000); 326 } 327 328 /** 329 * Attempts to reconnect to the voice server (typically after a region change). 330 * @param {string} token The voice token 331 * @param {string} endpoint The voice endpoint 332 * @private 333 */ 334 reconnect(token, endpoint) { 335 this.authentication.token = token; 336 this.authentication.endpoint = endpoint; 337 this.speaking = new Speaking().freeze(); 338 this.status = VoiceStatus.RECONNECTING; 339 this.emit('debug', `Reconnecting to ${endpoint}`); 340 /** 341 * Emitted when the voice connection is reconnecting (typically after a region change). 342 * @event VoiceConnection#reconnecting 343 */ 344 this.emit('reconnecting'); 345 this.connect(); 346 } 347 348 /** 349 * Disconnects the voice connection, causing a disconnect and closing event to be emitted. 350 */ 351 disconnect() { 352 this.emit('closing'); 353 this.emit('debug', 'disconnect() triggered'); 354 this.client.clearTimeout(this.connectTimeout); 355 const conn = this.voiceManager.connections.get(this.channel.guild.id); 356 if (conn === this) this.voiceManager.connections.delete(this.channel.guild.id); 357 this.sendVoiceStateUpdate({ 358 channel_id: null, 359 }); 360 this._disconnect(); 361 } 362 363 /** 364 * Internally disconnects (doesn't send disconnect packet). 365 * @private 366 */ 367 _disconnect() { 368 this.cleanup(); 369 this.status = VoiceStatus.DISCONNECTED; 370 /** 371 * Emitted when the voice connection disconnects. 372 * @event VoiceConnection#disconnect 373 */ 374 this.emit('disconnect'); 375 } 376 377 /** 378 * Cleans up after disconnect. 379 * @private 380 */ 381 cleanup() { 382 this.player.destroy(); 383 this.speaking = new Speaking().freeze(); 384 const { ws, udp } = this.sockets; 385 386 this.emit('debug', 'Connection clean up'); 387 388 if (ws) { 389 ws.removeAllListeners('error'); 390 ws.removeAllListeners('ready'); 391 ws.removeAllListeners('sessionDescription'); 392 ws.removeAllListeners('speaking'); 393 ws.shutdown(); 394 } 395 396 if (udp) udp.removeAllListeners('error'); 397 398 this.sockets.ws = null; 399 this.sockets.udp = null; 400 } 401 402 /** 403 * Connect the voice connection. 404 * @private 405 */ 406 connect() { 407 this.emit('debug', `Connect triggered`); 408 if (this.status !== VoiceStatus.RECONNECTING) { 409 if (this.sockets.ws) throw new Error('WS_CONNECTION_EXISTS'); 410 if (this.sockets.udp) throw new Error('UDP_CONNECTION_EXISTS'); 411 } 412 413 if (this.sockets.ws) this.sockets.ws.shutdown(); 414 if (this.sockets.udp) this.sockets.udp.shutdown(); 415 416 this.sockets.ws = new VoiceWebSocket(this); 417 this.sockets.udp = new VoiceUDP(this); 418 419 const { ws, udp } = this.sockets; 420 421 ws.on('debug', msg => this.emit('debug', msg)); 422 udp.on('debug', msg => this.emit('debug', msg)); 423 ws.on('error', err => this.emit('error', err)); 424 udp.on('error', err => this.emit('error', err)); 425 ws.on('ready', this.onReady.bind(this)); 426 ws.on('sessionDescription', this.onSessionDescription.bind(this)); 427 ws.on('startSpeaking', this.onStartSpeaking.bind(this)); 428 429 this.sockets.ws.connect(); 430 } 431 432 /** 433 * Invoked when the voice websocket is ready. 434 * @param {Object} data The received data 435 * @private 436 */ 437 onReady(data) { 438 Object.assign(this.authentication, data); 439 for (let mode of data.modes) { 440 if (SUPPORTED_MODES.includes(mode)) { 441 this.authentication.mode = mode; 442 this.emit('debug', `Selecting the ${mode} mode`); 443 break; 444 } 445 } 446 this.sockets.udp.createUDPSocket(data.ip); 447 } 448 449 /** 450 * Invoked when a session description is received. 451 * @param {Object} data The received data 452 * @private 453 */ 454 onSessionDescription(data) { 455 Object.assign(this.authentication, data); 456 this.status = VoiceStatus.CONNECTED; 457 const ready = () => { 458 this.client.clearTimeout(this.connectTimeout); 459 this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); 460 /** 461 * Emitted once the connection is ready, when a promise to join a voice channel resolves, 462 * the connection will already be ready. 463 * @event VoiceConnection#ready 464 */ 465 this.emit('ready'); 466 }; 467 if (this.dispatcher) { 468 ready(); 469 } else { 470 // This serves to provide support for voice receive, sending audio is required to receive it. 471 const dispatcher = this.play(new SingleSilence(), { type: 'opus', volume: false }); 472 dispatcher.once('finish', ready); 473 } 474 } 475 476 onStartSpeaking({ user_id, ssrc, speaking }) { 477 this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking }); 478 } 479 480 /** 481 * Invoked when a speaking event is received. 482 * @param {Object} data The received data 483 * @private 484 */ 485 onSpeaking({ user_id, speaking }) { 486 speaking = new Speaking(speaking).freeze(); 487 const guild = this.channel.guild; 488 const user = this.client.users.cache.get(user_id); 489 const old = this._speaking.get(user_id); 490 this._speaking.set(user_id, speaking); 491 /** 492 * Emitted whenever a user changes speaking state. 493 * @event VoiceConnection#speaking 494 * @param {User} user The user that has changed speaking state 495 * @param {Readonly<Speaking>} speaking The speaking state of the user 496 */ 497 if (this.status === VoiceStatus.CONNECTED) { 498 this.emit('speaking', user, speaking); 499 if (!speaking.has(Speaking.FLAGS.SPEAKING)) { 500 this.receiver.packets._stoppedSpeaking(user_id); 501 } 502 } 503 504 if (guild && user && !speaking.equals(old)) { 505 const member = guild.member(user); 506 if (member) { 507 /** 508 * Emitted once a guild member changes speaking state. 509 * @event Client#guildMemberSpeaking 510 * @param {GuildMember} member The member that started/stopped speaking 511 * @param {Readonly<Speaking>} speaking The speaking state of the member 512 */ 513 this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking); 514 } 515 } 516 } 517 518 play() {} // eslint-disable-line no-empty-function 519 } 520 521 PlayInterface.applyToClass(VoiceConnection); 522 523 module.exports = VoiceConnection;