From 526430b51aeaa34257a406aa39980522888ac61b Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 25 Oct 2016 18:49:12 -0400 Subject: [PATCH 1/6] Revert "Fix #837" This reverts commit add52ce62d24a03e127b5c2deee5812fd9b0ad0d. --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 0fbb88722..6b0ae77c3 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -396,7 +396,7 @@ class Message { content = `${prepend}${content}`; if (options.split) { - if (typeof options.split !== 'object' && typeof options.split !== 'boolean') options.split = {}; + if (typeof options.split !== 'object') options.split = {}; if (!options.split.prepend) options.split.prepend = prepend; } From c96d5ad30ef1d9f183fbf5a8a7e877d7d7361cf3 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 25 Oct 2016 19:01:56 -0400 Subject: [PATCH 2/6] Optimise everyone/here replacing --- src/client/rest/RESTMethods.js | 2 +- src/structures/Message.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index a2bf2800b..7cc0abde2 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -59,7 +59,7 @@ class RESTMethods { if (content) { if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) { - content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere'); + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); } if (split) content = splitMessage(content, typeof split === 'object' ? split : {}); diff --git a/src/structures/Message.js b/src/structures/Message.js index 6b0ae77c3..e9e093ac5 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -236,8 +236,7 @@ class Message { */ get cleanContent() { return this.content - .replace(/@everyone/g, '@\u200Beveryone') - .replace(/@here/g, '@\u200Bhere') + .replace(/@(everyone|here)/g, '@\u200b$1') .replace(/<@!?[0-9]+>/g, (input) => { const id = input.replace(/<|!|>|@/g, ''); if (this.channel.type === 'dm' || this.channel.type === 'group') { From a04094f0ff648dc12997c5dd13132fcc3ce2a161 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 25 Oct 2016 19:59:22 -0400 Subject: [PATCH 3/6] Rename VoiceConnection.disconnected event -> disconnect --- src/client/voice/ClientVoiceManager.js | 2 +- src/client/voice/VoiceConnection.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 6a572edf1..97cef5288 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -243,7 +243,7 @@ class ClientVoiceManager { this.connections.set(channel.guild.id, voiceConnection); voiceConnection.once('ready', () => resolve(voiceConnection)); voiceConnection.once('error', reject); - voiceConnection.once('disconnected', () => this.connections.delete(channel.guild.id)); + voiceConnection.once('disconnect', () => this.connections.delete(channel.guild.id)); }); }); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index d3fc2149f..be3fe798f 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -104,7 +104,7 @@ class VoiceConnection extends EventEmitter { } /** - * Disconnect the voice connection, causing a disconnected and closing event to be emitted. + * Disconnect the voice connection, causing a disconnect and closing event to be emitted. */ disconnect() { this.emit('closing'); @@ -119,9 +119,9 @@ class VoiceConnection extends EventEmitter { }); /** * Emitted when the voice connection disconnects - * @event VoiceConnection#disconnected + * @event VoiceConnection#disconnect */ - this.emit('disconnected'); + this.emit('disconnect'); } /** From d1e9d15a1c29301c615c4718db35a8394b0aa2bb Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 25 Oct 2016 20:26:57 -0400 Subject: [PATCH 4/6] Clean up a bunch of new voice stuff --- src/client/voice/ClientVoiceManager.js | 39 ++++++++----------- src/client/voice/VoiceConnection.js | 9 +---- src/client/voice/VoiceUDPClient.js | 34 +++++++--------- src/client/voice/VoiceWebSocket.js | 23 +++++------ src/client/voice/pcm/FfmpegConverterEngine.js | 1 - src/client/voice/player/AudioPlayer.js | 13 +++---- src/client/voice/player/BasePlayer.js | 3 +- src/client/voice/receiver/VoiceReceiver.js | 7 +++- src/client/voice/util/SecretKey.js | 4 +- 9 files changed, 57 insertions(+), 76 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 97cef5288..06cc44941 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -11,22 +11,26 @@ const EventEmitter = require('events').EventEmitter; class PendingVoiceConnection extends EventEmitter { constructor(voiceManager, channel) { super(); + /** * The ClientVoiceManager that instantiated this pending connection * @type {ClientVoiceManager} */ this.voiceManager = voiceManager; + /** * The channel that this pending voice connection will attempt to join * @type {VoiceChannel} */ this.channel = channel; + /** * The timeout that will be invoked after 15 seconds signifying a failure to connect * @type {Timeout} */ this.deathTimer = this.voiceManager.client.setTimeout( - () => this.fail(new Error('Automatic failure after 15 seconds')), 15000); + () => this.fail(new Error('Connection not established within 15 seconds.')), 15000); + /** * An object containing data required to connect to the voice servers with * @type {object} @@ -53,26 +57,26 @@ class PendingVoiceConnection extends EventEmitter { */ setTokenAndEndpoint(token, endpoint) { if (!token) { - this.fail(new Error('Token not provided from voice server packet')); + this.fail(new Error('Token not provided from voice server packet.')); return; } if (!endpoint) { - this.fail(new Error('Endpoint not provided from voice server packet')); + this.fail(new Error('Endpoint not provided from voice server packet.')); return; } if (this.data.token) { - this.fail(new Error('There is already a registered token for this connection')); + this.fail(new Error('There is already a registered token for this connection.')); return; } if (this.data.endpoint) { - this.fail(new Error('There is already a registered endpoint for this connection')); + this.fail(new Error('There is already a registered endpoint for this connection.')); return; } endpoint = endpoint.match(/([^:]*)/)[0]; if (!endpoint) { - this.fail(new Error('failed to find an endpoint')); + this.fail(new Error('Failed to find an endpoint.')); return; } @@ -88,11 +92,11 @@ class PendingVoiceConnection extends EventEmitter { */ setSessionID(sessionID) { if (!sessionID) { - this.fail(new Error('Session ID not supplied')); + this.fail(new Error('Session ID not supplied.')); return; } if (this.data.session_id) { - this.fail(new Error('There is already a registered session ID for this connection')); + this.fail(new Error('There is already a registered session ID for this connection.')); return; } this.data.session_id = sessionID; @@ -161,15 +165,11 @@ class ClientVoiceManager { } onVoiceServer(data) { - if (this.pending.has(data.guild_id)) { - this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint); - } + if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint); } onVoiceStateUpdate(data) { - if (this.pending.has(data.guild_id)) { - this.pending.get(data.guild_id).setSessionID(data.session_id); - } + if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setSessionID(data.session_id); } /** @@ -178,15 +178,13 @@ class ClientVoiceManager { * @param {Object} [options] The options to provide */ sendVoiceStateUpdate(channel, options = {}) { - if (!this.client.user) { - throw new Error('You cannot join because there is no client user'); - } + if (!this.client.user) throw new Error('Unable to join because there is no client user.'); if (channel.permissionsFor) { const permissions = channel.permissionsFor(this.client.user); if (permissions) { if (!permissions.hasPermission('CONNECT')) { - throw new Error('You do not have permission to connect to this voice channel'); + throw new Error('You do not have permission to connect to this voice channel.'); } } else { throw new Error('There is no permission set for the client user in this channel - are they part of the guild?'); @@ -215,10 +213,7 @@ class ClientVoiceManager { */ joinChannel(channel) { return new Promise((resolve, reject) => { - // if already connecting to this voice server, error - if (this.pending.get(channel.guild.id)) { - throw new Error(`Already connecting to this guild's voice server.`); - } + if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.'); const existingConnection = this.connections.get(channel.guild.id); if (existingConnection) { diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index be3fe798f..c6188ffc7 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -129,12 +129,8 @@ class VoiceConnection extends EventEmitter { * @private */ connect() { - if (this.sockets.ws) { - throw new Error('There is already an existing WebSocket connection!'); - } - if (this.sockets.udp) { - throw new Error('There is already an existing UDP connection!'); - } + if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.'); + if (this.sockets.udp) throw new Error('There is already an existing UDP connection.'); this.sockets.ws = new VoiceWebSocket(this); this.sockets.udp = new VoiceUDP(this); this.sockets.ws.on('error', e => this.emit('error', e)); @@ -260,7 +256,6 @@ class VoiceConnection extends EventEmitter { this.receivers.push(receiver); return receiver; } - } module.exports = VoiceConnection; diff --git a/src/client/voice/VoiceUDPClient.js b/src/client/voice/VoiceUDPClient.js index 3a13b675a..d0e7e7bec 100644 --- a/src/client/voice/VoiceUDPClient.js +++ b/src/client/voice/VoiceUDPClient.js @@ -7,9 +7,7 @@ function parseLocalPacket(message) { try { const packet = new Buffer(message); let address = ''; - for (let i = 4; i < packet.indexOf(0, i); i++) { - address += String.fromCharCode(packet[i]); - } + for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]); const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10); return { address, port }; } catch (error) { @@ -24,33 +22,40 @@ function parseLocalPacket(message) { class VoiceConnectionUDPClient extends EventEmitter { constructor(voiceConnection) { super(); + /** * The voice connection that this UDP client serves * @type {VoiceConnection} */ this.voiceConnection = voiceConnection; + /** * The UDP socket * @type {?Socket} */ this.socket = null; + /** * The address of the discord voice server * @type {?string} */ this.discordAddress = null; + /** * The local IP address * @type {?string} */ this.localAddress = null; + /** * The local port * @type {?string} */ this.localPort = null; + this.voiceConnection.on('closing', this.shutdown.bind(this)); } + shutdown() { if (this.socket) { try { @@ -61,6 +66,7 @@ class VoiceConnectionUDPClient extends EventEmitter { this.socket = null; } } + /** * The port of the discord voice server * @type {number} @@ -69,6 +75,7 @@ class VoiceConnectionUDPClient extends EventEmitter { get discordPort() { return this.voiceConnection.authentication.port; } + /** * Tries to resolve the voice server endpoint to an address * @returns {Promise} @@ -93,22 +100,11 @@ class VoiceConnectionUDPClient extends EventEmitter { */ send(packet) { return new Promise((resolve, reject) => { - if (this.socket) { - if (!this.discordAddress || !this.discordPort) { - reject(new Error('malformed UDP address or port')); - return; - } - // console.log('sendin', packet); - this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { - if (error) { - reject(error); - } else { - resolve(packet); - } - }); - } else { - reject(new Error('tried to send a UDP packet but there is no socket available')); - } + if (!this.socket) throw new Error('Tried to send a UDP packet, but there is no socket available.'); + if (!this.discordAddress || !this.discordPort) throw new Error('Malformed UDP address or port.'); + this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { + if (error) reject(error); else resolve(packet); + }); }); } diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js index 290dbf8ea..1c3cef6fc 100644 --- a/src/client/voice/VoiceWebSocket.js +++ b/src/client/voice/VoiceWebSocket.js @@ -11,20 +11,21 @@ const EventEmitter = require('events').EventEmitter; class VoiceWebSocket extends EventEmitter { constructor(voiceConnection) { super(); + /** * The Voice Connection that this WebSocket serves * @type {VoiceConnection} */ this.voiceConnection = voiceConnection; + /** * How many connection attempts have been made * @type {number} */ this.attempts = 0; + this.connect(); - this.dead = false; - this.voiceConnection.on('closing', this.shutdown.bind(this)); } @@ -47,9 +48,7 @@ class VoiceWebSocket extends EventEmitter { */ reset() { if (this.ws) { - if (this.ws.readyState !== WebSocket.CLOSED) { - this.ws.close(); - } + if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close(); this.ws = null; } this.clearHeartbeat(); @@ -59,17 +58,15 @@ class VoiceWebSocket extends EventEmitter { * Starts connecting to the Voice WebSocket Server. */ connect() { - if (this.dead) { - return; - } - if (this.ws) { - this.reset(); - } + if (this.dead) return; + if (this.ws) this.reset(); if (this.attempts > 5) { this.emit('error', new Error(`too many connection attempts (${this.attempts})`)); return; } + this.attempts++; + /** * The actual WebSocket used to connect to the Voice WebSocket Server. * @type {WebSocket} @@ -97,7 +94,7 @@ class VoiceWebSocket extends EventEmitter { } }); } else { - reject(new Error(`voice websocket not open to send ${data}`)); + reject(new Error(`Voice websocket not open to send ${data}.`)); } }); } @@ -150,7 +147,7 @@ class VoiceWebSocket extends EventEmitter { * Called whenever the connection to the WebSocket Server is lost */ onClose() { - // #todo see if the connection is open before reconnecting + // TODO see if the connection is open before reconnecting if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000); } diff --git a/src/client/voice/pcm/FfmpegConverterEngine.js b/src/client/voice/pcm/FfmpegConverterEngine.js index e49798ed8..34c5d509a 100644 --- a/src/client/voice/pcm/FfmpegConverterEngine.js +++ b/src/client/voice/pcm/FfmpegConverterEngine.js @@ -3,7 +3,6 @@ const ChildProcess = require('child_process'); const EventEmitter = require('events').EventEmitter; class PCMConversionProcess extends EventEmitter { - constructor(process) { super(); this.process = process; diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index dc7e3b8c9..b5c338675 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -4,7 +4,6 @@ const EventEmitter = require('events').EventEmitter; const StreamDispatcher = require('../dispatcher/StreamDispatcher'); class AudioPlayer extends EventEmitter { - constructor(voiceConnection) { super(); this.voiceConnection = voiceConnection; @@ -20,13 +19,13 @@ class AudioPlayer extends EventEmitter { timestamp: 0, pausedTime: 0, }; - this.voiceConnection.on('closing', () => this.cleanup(null, 'voice connection is closing')); + this.voiceConnection.on('closing', () => this.cleanup(null, 'voice connection closing')); } playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { const options = { seek, volume, passes }; stream.on('end', () => { - this.emit('debug', 'input stream to converter has ended'); + this.emit('debug', 'Input stream to converter has ended'); }); stream.on('error', e => this.emit('error', e)); const conversionProcess = this.audioToPCM.createConvertStream(options.seek); @@ -37,7 +36,7 @@ class AudioPlayer extends EventEmitter { cleanup(checkStream, reason) { // cleanup is a lot less aggressive than v9 because it doesn't try to kill every single stream it is aware of - this.emit('debug', `clean up triggered due to ${reason}`); + this.emit('debug', `Clean up triggered due to ${reason}`); const filter = checkStream && this.dispatcher && this.dispatcher.stream === checkStream; if (this.currentConverter && (checkStream ? filter : true)) { this.currentConverter.destroy(); @@ -47,7 +46,7 @@ class AudioPlayer extends EventEmitter { playPCMStream(stream, converter, { seek = 0, volume = 1, passes = 1 } = {}) { const options = { seek, volume, passes }; - stream.on('end', () => this.emit('debug', 'pcm input stream ended')); + stream.on('end', () => this.emit('debug', 'PCM input stream ended')); this.cleanup(null, 'outstanding play stream'); this.currentConverter = converter; if (this.dispatcher) { @@ -56,10 +55,10 @@ class AudioPlayer extends EventEmitter { stream.on('error', e => this.emit('error', e)); const dispatcher = new StreamDispatcher(this, stream, this.streamingData, options); dispatcher.on('error', e => this.emit('error', e)); - dispatcher.on('end', () => this.cleanup(dispatcher.stream, 'disp ended')); + dispatcher.on('end', () => this.cleanup(dispatcher.stream, 'dispatcher ended')); dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); this.dispatcher = dispatcher; - dispatcher.on('debug', m => this.emit('debug', `stream dispatch - ${m}`)); + dispatcher.on('debug', m => this.emit('debug', `Stream dispatch - ${m}`)); return dispatcher; } diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index a38e4906d..d5285cd34 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -101,8 +101,7 @@ class VoiceConnectionPlayer extends EventEmitter { speaking: true, delay: 0, }, - }) - .catch(e => { + }).catch(e => { this.emit('debug', e); }); } diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index cf7582e50..e27ad8d1a 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -25,17 +25,20 @@ class VoiceReceiver extends EventEmitter { this.queues = new Map(); this.pcmStreams = new Map(); this.opusStreams = new Map(); + /** * Whether or not this receiver has been destroyed. * @type {boolean} */ this.destroyed = false; + /** * The VoiceConnection that instantiated this * @type {VoiceConnection} */ this.voiceConnection = connection; - this._listener = (msg => { + + this._listener = msg => { const ssrc = +msg.readUInt32BE(8).toString(10); const user = this.voiceConnection.ssrcMap.get(ssrc); if (!user) { @@ -50,7 +53,7 @@ class VoiceReceiver extends EventEmitter { } this.handlePacket(msg, user); } - }).bind(this); + }; this.voiceConnection.sockets.udp.socket.on('message', this._listener); } diff --git a/src/client/voice/util/SecretKey.js b/src/client/voice/util/SecretKey.js index 5e1df7a99..42a0da0cb 100644 --- a/src/client/voice/util/SecretKey.js +++ b/src/client/voice/util/SecretKey.js @@ -8,9 +8,7 @@ class SecretKey { * @type {Uint8Array} */ this.key = new Uint8Array(new ArrayBuffer(key.length)); - for (const index in key) { - this.key[index] = key[index]; - } + for (const index in key) this.key[index] = key[index]; } } From b2a4545c16e34a0625715d9847acd40bd078a745 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 25 Oct 2016 20:34:57 -0400 Subject: [PATCH 5/6] Clean up more voice stuff --- src/client/voice/ClientVoiceManager.js | 216 ++++++++++++------------- src/client/voice/VoiceWebSocket.js | 34 ++-- 2 files changed, 122 insertions(+), 128 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 06cc44941..ee088d8e3 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -4,6 +4,114 @@ const Constants = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); const EventEmitter = require('events').EventEmitter; +/** + * Manages all the voice stuff for the Client + * @private + */ +class ClientVoiceManager { + constructor(client) { + /** + * The client that instantiated this voice manager + * @type {Client} + */ + this.client = client; + + /** + * A collection mapping connection IDs to the Connection objects + * @type {Collection} + */ + this.connections = new Collection(); + + /** + * Pending connection attempts, maps Guild ID to VoiceChannel + * @type {Collection} + */ + this.pending = new Collection(); + + this.client.on('self.voiceServer', this.onVoiceServer.bind(this)); + this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); + } + + onVoiceServer(data) { + if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint); + } + + onVoiceStateUpdate(data) { + if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setSessionID(data.session_id); + } + + /** + * Sends a request to the main gateway to join a voice channel + * @param {VoiceChannel} channel The channel to join + * @param {Object} [options] The options to provide + */ + sendVoiceStateUpdate(channel, options = {}) { + if (!this.client.user) throw new Error('Unable to join because there is no client user.'); + + if (channel.permissionsFor) { + const permissions = channel.permissionsFor(this.client.user); + if (permissions) { + if (!permissions.hasPermission('CONNECT')) { + throw new Error('You do not have permission to connect to this voice channel.'); + } + } else { + throw new Error('There is no permission set for the client user in this channel - are they part of the guild?'); + } + } else { + throw new Error('Channel does not support permissionsFor; is it really a voice channel?'); + } + + options = mergeDefault({ + guild_id: channel.guild.id, + channel_id: channel.id, + self_mute: false, + self_deaf: false, + }, options); + + this.client.ws.send({ + op: Constants.OPCodes.VOICE_STATE_UPDATE, + d: options, + }); + } + + /** + * Sets up a request to join a voice channel + * @param {VoiceChannel} channel The voice channel to join + * @returns {Promise} + */ + joinChannel(channel) { + return new Promise((resolve, reject) => { + if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.'); + + const existingConnection = this.connections.get(channel.guild.id); + if (existingConnection) { + if (existingConnection.channel.id !== channel.id) { + this.sendVoiceStateUpdate(channel); + this.connections.get(channel.guild.id).channel = channel; + } + resolve(existingConnection); + return; + } + + const pendingConnection = new PendingVoiceConnection(this, channel); + this.pending.set(channel.guild.id, pendingConnection); + + pendingConnection.on('fail', reason => { + this.pending.delete(channel.guild.id); + reject(reason); + }); + + pendingConnection.on('pass', voiceConnection => { + this.pending.delete(channel.guild.id); + this.connections.set(channel.guild.id, voiceConnection); + voiceConnection.once('ready', () => resolve(voiceConnection)); + voiceConnection.once('error', reject); + voiceConnection.once('disconnect', () => this.connections.delete(channel.guild.id)); + }); + }); + } +} + /** * Represents a Pending Voice Connection * @private @@ -136,112 +244,4 @@ class PendingVoiceConnection extends EventEmitter { } } -/** - * Manages all the voice stuff for the Client - * @private - */ -class ClientVoiceManager { - constructor(client) { - /** - * The client that instantiated this voice manager - * @type {Client} - */ - this.client = client; - - /** - * A collection mapping connection IDs to the Connection objects - * @type {Collection} - */ - this.connections = new Collection(); - - /** - * Pending connection attempts, maps Guild ID to VoiceChannel - * @type {Collection} - */ - this.pending = new Collection(); - - this.client.on('self.voiceServer', this.onVoiceServer.bind(this)); - this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); - } - - onVoiceServer(data) { - if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint); - } - - onVoiceStateUpdate(data) { - if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setSessionID(data.session_id); - } - - /** - * Sends a request to the main gateway to join a voice channel - * @param {VoiceChannel} channel The channel to join - * @param {Object} [options] The options to provide - */ - sendVoiceStateUpdate(channel, options = {}) { - if (!this.client.user) throw new Error('Unable to join because there is no client user.'); - - if (channel.permissionsFor) { - const permissions = channel.permissionsFor(this.client.user); - if (permissions) { - if (!permissions.hasPermission('CONNECT')) { - throw new Error('You do not have permission to connect to this voice channel.'); - } - } else { - throw new Error('There is no permission set for the client user in this channel - are they part of the guild?'); - } - } else { - throw new Error('Channel does not support permissionsFor; is it really a voice channel?'); - } - - options = mergeDefault({ - guild_id: channel.guild.id, - channel_id: channel.id, - self_mute: false, - self_deaf: false, - }, options); - - this.client.ws.send({ - op: Constants.OPCodes.VOICE_STATE_UPDATE, - d: options, - }); - } - - /** - * Sets up a request to join a voice channel - * @param {VoiceChannel} channel The voice channel to join - * @returns {Promise} - */ - joinChannel(channel) { - return new Promise((resolve, reject) => { - if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.'); - - const existingConnection = this.connections.get(channel.guild.id); - if (existingConnection) { - if (existingConnection.channel.id !== channel.id) { - this.sendVoiceStateUpdate(channel); - this.connections.get(channel.guild.id).channel = channel; - } - resolve(existingConnection); - return; - } - - const pendingConnection = new PendingVoiceConnection(this, channel); - this.pending.set(channel.guild.id, pendingConnection); - - pendingConnection.on('fail', reason => { - this.pending.delete(channel.guild.id); - reject(reason); - }); - - pendingConnection.on('pass', voiceConnection => { - this.pending.delete(channel.guild.id); - this.connections.set(channel.guild.id, voiceConnection); - voiceConnection.once('ready', () => resolve(voiceConnection)); - voiceConnection.once('error', reject); - voiceConnection.once('disconnect', () => this.connections.delete(channel.guild.id)); - }); - }); - } -} - module.exports = ClientVoiceManager; diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js index 1c3cef6fc..1c421a71b 100644 --- a/src/client/voice/VoiceWebSocket.js +++ b/src/client/voice/VoiceWebSocket.js @@ -61,7 +61,7 @@ class VoiceWebSocket extends EventEmitter { if (this.dead) return; if (this.ws) this.reset(); if (this.attempts > 5) { - this.emit('error', new Error(`too many connection attempts (${this.attempts})`)); + this.emit('error', new Error(`Too many connection attempts (${this.attempts}).`)); return; } @@ -85,17 +85,12 @@ class VoiceWebSocket extends EventEmitter { */ send(data) { return new Promise((resolve, reject) => { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.ws.send(data, null, error => { - if (error) { - reject(error); - } else { - resolve(data); - } - }); - } else { - reject(new Error(`Voice websocket not open to send ${data}.`)); + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error(`Voice websocket not open to send ${data}.`); } + this.ws.send(data, null, error => { + if (error) reject(error); else resolve(data); + }); }); } @@ -126,7 +121,7 @@ class VoiceWebSocket extends EventEmitter { session_id: this.voiceConnection.authentication.session_id, }, }).catch(() => { - this.emit('error', new Error('tried to send join packet but WebSocket not open')); + this.emit('error', new Error('Tried to send join packet, but the WebSocket is not open.')); }); } @@ -208,7 +203,7 @@ class VoiceWebSocket extends EventEmitter { */ setHeartbeat(interval) { if (!interval || isNaN(interval)) { - this.onError(new Error('tried to set voice heartbeat but no valid interval was specified')); + this.onError(new Error('Tried to set voice heartbeat but no valid interval was specified.')); return; } if (this.heartbeatInterval) { @@ -217,7 +212,7 @@ class VoiceWebSocket extends EventEmitter { * @param {string} warn the warning * @event VoiceWebSocket#warn */ - this.emit('warn', 'a voice heartbeat interval is being overwritten'); + this.emit('warn', 'A voice heartbeat interval is being overwritten'); clearInterval(this.heartbeatInterval); } this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval); @@ -228,7 +223,7 @@ class VoiceWebSocket extends EventEmitter { */ clearHeartbeat() { if (!this.heartbeatInterval) { - this.emit('warn', 'tried to clear a heartbeat interval that does not exist'); + this.emit('warn', 'Tried to clear a heartbeat interval that does not exist'); return; } clearInterval(this.heartbeatInterval); @@ -239,11 +234,10 @@ class VoiceWebSocket extends EventEmitter { * Sends a heartbeat packet */ sendHeartbeat() { - this.sendPacket({ op: Constants.VoiceOPCodes.HEARTBEAT, d: null }) - .catch(() => { - this.emit('warn', 'tried to send heartbeat, but connection is not open'); - this.clearHeartbeat(); - }); + this.sendPacket({ op: Constants.VoiceOPCodes.HEARTBEAT, d: null }).catch(() => { + this.emit('warn', 'Tried to send heartbeat, but connection is not open'); + this.clearHeartbeat(); + }); } } From 05f73c3edfe9f856db431f1b3893593ae2dfb0fb Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 25 Oct 2016 20:41:23 -0400 Subject: [PATCH 6/6] Clean up voice channel join permissions check --- src/client/voice/ClientVoiceManager.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index ee088d8e3..eea0dc8a8 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -47,19 +47,16 @@ class ClientVoiceManager { */ sendVoiceStateUpdate(channel, options = {}) { if (!this.client.user) throw new Error('Unable to join because there is no client user.'); - - if (channel.permissionsFor) { - const permissions = channel.permissionsFor(this.client.user); - if (permissions) { - if (!permissions.hasPermission('CONNECT')) { - throw new Error('You do not have permission to connect to this voice channel.'); - } - } else { - throw new Error('There is no permission set for the client user in this channel - are they part of the guild?'); - } - } else { + if (!channel.permissionsFor) { throw new Error('Channel does not support permissionsFor; is it really a voice channel?'); } + const permissions = channel.permissionsFor(this.client.user); + if (!permissions) { + throw new Error('There is no permission set for the client user in this channel - are they part of the guild?'); + } + if (!permissions.hasPermission('CONNECT')) { + throw new Error('You do not have permission to connect to this voice channel.'); + } options = mergeDefault({ guild_id: channel.guild.id,