diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index bf9f99bdb..09b23c637 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -2,6 +2,135 @@ const Collection = require('../../util/Collection'); const mergeDefault = require('../../util/MergeDefault'); const Constants = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); +const EventEmitter = require('events').EventEmitter; + +/** + * Represents a Pending Voice Connection + * @private + */ +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); + /** + * An object containing data required to connect to the voice servers with + * @type {object} + */ + this.data = {}; + + this.sendVoiceStateUpdate(); + } + + checkReady() { + if (this.data.token && this.data.endpoint && this.data.session_id) { + this.pass(); + return true; + } else { + return false; + } + } + + /** + * Set the token and endpoint required to connect to the the voice servers + * @param {string} token the token + * @param {string} endpoint the endpoint + * @returns {void} + */ + setTokenAndEndpoint(token, endpoint) { + if (!token) { + 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')); + return; + } + if (this.data.token) { + 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')); + return; + } + + endpoint = endpoint.match(/([^:]*)/)[0]; + + if (!endpoint) { + this.fail(new Error('failed to find an endpoint')); + return; + } + + this.data.token = token; + this.data.endpoint = endpoint; + + this.checkReady(); + } + + /** + * Sets the Session ID for the connection + * @param {string} sessionID the session ID + */ + setSessionID(sessionID) { + if (!sessionID) { + 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')); + return; + } + this.data.session_id = sessionID; + + this.checkReady(); + } + + clean() { + clearInterval(this.deathTimer); + this.emit('fail', new Error('Clean-up triggered :fourTriggered:')); + } + + pass() { + clearInterval(this.deathTimer); + this.emit('pass', this.upgrade()); + } + + fail(reason) { + this.emit('fail', reason); + this.clean(); + } + + sendVoiceStateUpdate() { + try { + this.voiceManager.sendVoiceStateUpdate(this.channel); + } catch (error) { + this.fail(error); + } + } + + /** + * Upgrades this Pending Connection to a full Voice Connection + * @returns {VoiceConnection} + */ + upgrade() { + return new VoiceConnection(this); + } +} /** * Manages all the voice stuff for the Client @@ -26,6 +155,9 @@ class ClientVoiceManager { * @type {Collection} */ this.pending = new Collection(); + + this.client.on('self.voiceServer', this.onVoiceServer.bind(this)); + this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); } /** @@ -47,31 +179,16 @@ class ClientVoiceManager { } } - /** - * Called when the Client receives information about this voice server update. - * @param {string} guildID The ID of the Guild - * @param {string} token The token to authorise with - * @param {string} endpoint The endpoint to connect to - */ - _receivedVoiceServer(guildID, token, endpoint) { - const pendingRequest = this.pending.get(guildID); - if (!pendingRequest) throw new Error('Guild not pending.'); - pendingRequest.token = token; - // remove the port otherwise it errors ¯\_(ツ)_/¯ - pendingRequest.endpoint = endpoint.match(/([^:]*)/)[0]; - this._checkPendingReady(guildID); + onVoiceServer(data) { + if (this.pending.has(data.guild_id)) { + this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint); + } } - /** - * Called when the Client receives information about the voice state update. - * @param {string} guildID The ID of the Guild - * @param {string} sessionID The session id to authorise with - */ - _receivedVoiceStateUpdate(guildID, sessionID) { - const pendingRequest = this.pending.get(guildID); - if (!pendingRequest) throw new Error('Guild not pending.'); - pendingRequest.sessionID = sessionID; - this._checkPendingReady(guildID); + onVoiceStateUpdate(data) { + if (this.pending.has(data.guild_id)) { + this.pending.get(data.guild_id).setSessionID(data.session_id); + } } /** @@ -79,13 +196,31 @@ class ClientVoiceManager { * @param {VoiceChannel} channel The channel to join * @param {Object} [options] The options to provide */ - _sendWSJoin(channel, options = {}) { + sendVoiceStateUpdate(channel, options = {}) { + if (!this.client.user) { + throw new Error('You cannot 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, @@ -99,26 +234,32 @@ class ClientVoiceManager { */ joinChannel(channel) { return new Promise((resolve, reject) => { - if (this.pending.get(channel.guild.id)) throw new Error(`Already connecting to a channel in guild.`); - const existingConn = this.connections.get(channel.guild.id); - if (existingConn) { - if (existingConn.channel.id !== channel.id) { - this._sendWSJoin(channel); + // 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.`); + } + + 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(existingConn); + resolve(existingConnection); return; } - this.pending.set(channel.guild.id, { - channel, - sessionID: null, - token: null, - endpoint: null, - resolve, - reject, + + 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 => { + // do stuff }); - this._sendWSJoin(channel); - this.client.setTimeout(() => reject(new Error('Connection not established within 15 seconds.')), 15000); }); } } diff --git a/src/client/websocket/packets/handlers/VoiceServerUpdate.js b/src/client/websocket/packets/handlers/VoiceServerUpdate.js index 108d54ccc..97885d6cd 100644 --- a/src/client/websocket/packets/handlers/VoiceServerUpdate.js +++ b/src/client/websocket/packets/handlers/VoiceServerUpdate.js @@ -12,9 +12,7 @@ class VoiceServerUpdate extends AbstractHandler { handle(packet) { const client = this.packetManager.client; const data = packet.d; - if (client.voice.pending.has(data.guild_id) && data.endpoint) { - client.voice._receivedVoiceServer(data.guild_id, data.token, data.endpoint); - } + client.emit('self.voiceServer', data); } } diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/websocket/packets/handlers/VoiceStateUpdate.js index 639da210b..ddbfbfcbd 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -20,8 +20,8 @@ class VoiceStateUpdateHandler extends AbstractHandler { // if the member left the voice channel, unset their speaking property if (!data.channel_id) member.speaking = null; - if (client.voice.pending.has(guild.id) && member.user.id === client.user.id && data.channel_id) { - client.voice._receivedVoiceStateUpdate(data.guild_id, data.session_id); + if (member.user.id === client.user.id && data.channel_id) { + client.emit('self.voiceStateUpdate', data); } const newChannel = client.channels.get(data.channel_id);