From be5efea461660c8fa38a57f21019af4b37746d17 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 10 Aug 2018 14:44:59 +0100 Subject: [PATCH] rewrite voice state handling --- docs/topics/voice.md | 4 +- .../packets/handlers/VoiceStateUpdate.js | 19 ++- src/errors/Messages.js | 2 + src/index.js | 1 + src/stores/VoiceStateStore.js | 20 +++ src/structures/Guild.js | 38 +---- src/structures/GuildMember.js | 90 +----------- src/structures/VoiceChannel.js | 23 ++- src/structures/VoiceState.js | 131 ++++++++++++++++++ src/util/Structures.js | 1 + test/voice.js | 2 +- 11 files changed, 187 insertions(+), 144 deletions(-) create mode 100644 src/stores/VoiceStateStore.js create mode 100644 src/structures/VoiceState.js diff --git a/docs/topics/voice.md b/docs/topics/voice.md index d08583495..e48b8b325 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -31,8 +31,8 @@ client.on('message', async message => { if (message.content === '/join') { // Only try to join the sender's voice channel if they are in one themselves - if (message.member.voiceChannel) { - const connection = await message.member.voiceChannel.join(); + if (message.member.voice.channel) { + const connection = await message.member.voice.channel.join(); } else { message.reply('You need to join a voice channel first!'); } diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/websocket/packets/handlers/VoiceStateUpdate.js index 5b81cc1d7..f76f51d9b 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -9,28 +9,27 @@ class VoiceStateUpdateHandler extends AbstractHandler { const guild = client.guilds.get(data.guild_id); if (guild) { + // Update the state + const oldState = guild.voiceStates.get(data.user_id); + if (oldState) oldState._patch(data); + else guild.voiceStates.add(data); + const member = guild.members.get(data.user_id); if (member) { - const oldMember = member._clone(); - oldMember._frozenVoiceState = oldMember.voiceState; - if (member.user.id === client.user.id && data.channel_id) { client.emit('self.voiceStateUpdate', data); } - - guild.voiceStates.set(member.user.id, data); - - client.emit(Events.VOICE_STATE_UPDATE, oldMember, member); + client.emit(Events.VOICE_STATE_UPDATE, oldState, member.voiceState); } } } } /** - * Emitted whenever a user changes voice state - e.g. joins/leaves a channel, mutes/unmutes. + * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate - * @param {GuildMember} oldMember The member before the voice state update - * @param {GuildMember} newMember The member after the voice state update + * @param {VoiceState} oldState The voice state before the update + * @param {VoiceState} newState The voice state after the update */ module.exports = VoiceStateUpdateHandler; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 97f6af280..86bd54cd1 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -53,6 +53,8 @@ const Messages = { VOICE_PLAY_INTERFACE_BAD_TYPE: 'Unknown stream type', VOICE_PRISM_DEMUXERS_NEED_STREAM: 'To play a webm/ogg stream, you need to pass a ReadableStream.', + VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.', + OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', diff --git a/src/index.js b/src/index.js index aad9c9433..8c7f5fb19 100644 --- a/src/index.js +++ b/src/index.js @@ -83,6 +83,7 @@ module.exports = { UserConnection: require('./structures/UserConnection'), VoiceChannel: require('./structures/VoiceChannel'), VoiceRegion: require('./structures/VoiceRegion'), + VoiceState: require('./structures/VoiceState'), Webhook: require('./structures/Webhook'), WebSocket: require('./WebSocket'), diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js new file mode 100644 index 000000000..bc14c8f4b --- /dev/null +++ b/src/stores/VoiceStateStore.js @@ -0,0 +1,20 @@ +const DataStore = require('./DataStore'); +const VoiceState = require('../structures/VoiceState'); + +class VoiceStateStore extends DataStore { + constructor(guild, iterable) { + super(guild.client, iterable, VoiceState); + this.guild = guild; + } + + add(data, cache = true) { + const existing = this.get(data.user_id); + if (existing) return existing; + + const entry = new VoiceState(this.guild, data); + if (cache) this.set(data.user_id, entry); + return entry; + } +} + +module.exports = VoiceStateStore; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 050feb7c2..260819251 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -12,6 +12,7 @@ const RoleStore = require('../stores/RoleStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); const GuildChannelStore = require('../stores/GuildChannelStore'); const PresenceStore = require('../stores/PresenceStore'); +const VoiceStateStore = require('../stores/VoiceStateStore'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); @@ -229,9 +230,13 @@ class Guild extends Base { } } - if (!this.voiceStates) this.voiceStates = new VoiceStateCollection(this); + if (!this.voiceStates) this.voiceStates = new VoiceStateStore(this); if (data.voice_states) { - for (const voiceState of data.voice_states) this.voiceStates.set(voiceState.user_id, voiceState); + for (const voiceState of data.voice_states) { + const existing = this.voiceStates.get(voiceState.user_id); + if (existing) existing._patch(voiceState); + else this.voiceStates.add(voiceState); + } } if (!this.emojis) { @@ -881,33 +886,4 @@ class Guild extends Base { } } -// TODO: Document this thing -class VoiceStateCollection extends Collection { - constructor(guild) { - super(); - this.guild = guild; - } - - set(id, voiceState) { - const member = this.guild.members.get(id); - if (member) { - if (member.voiceChannel && member.voiceChannel.id !== voiceState.channel_id) { - member.voiceChannel.members.delete(member.id); - } - const newChannel = this.guild.channels.get(voiceState.channel_id); - if (newChannel) newChannel.members.set(member.user.id, member); - } - super.set(id, voiceState); - } - - delete(id) { - const voiceState = this.get(id); - if (voiceState && voiceState.channel_id) { - const channel = this.guild.channels.get(voiceState.channel_id); - if (channel) channel.members.delete(id); - } - return super.delete(id); - } -} - module.exports = Guild; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8046b981b..8048481d5 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -56,18 +56,6 @@ class GuildMember extends Base { if (data) this._patch(data); } - /** - * Whether this member is speaking. If the client isn't sure, then this will be undefined. Otherwise it will be - * true/false - * @type {?boolean} - * @name GuildMember#speaking - */ - get speaking() { - return this.voiceChannel && this.voiceChannel.connection ? - Boolean(this.voiceChannel.connection._speaking.get(this.id)) : - null; - } - _patch(data) { /** * The nickname of this member, if they have one @@ -107,52 +95,10 @@ class GuildMember extends Base { return (channel && channel.messages.get(this.lastMessageID)) || null; } - get voiceState() { - return this._frozenVoiceState || this.guild.voiceStates.get(this.id) || {}; + get voice() { + return this.guild.voiceStates.get(this.id); } - /** - * Whether this member is deafened server-wide - * @type {boolean} - * @readonly - */ - get serverDeaf() { return this.voiceState.deaf; } - - /** - * Whether this member is muted server-wide - * @type {boolean} - * @readonly - */ - get serverMute() { return this.voiceState.mute; } - - /** - * Whether this member is self-muted - * @type {boolean} - * @readonly - */ - get selfMute() { return this.voiceState.self_mute; } - - /** - * Whether this member is self-deafened - * @type {boolean} - * @readonly - */ - get selfDeaf() { return this.voiceState.self_deaf; } - - /** - * The voice session ID of this member (if any) - * @type {?Snowflake} - * @readonly - */ - get voiceSessionID() { return this.voiceState.session_id; } - - /** - * The voice channel ID of this member, (if any) - * @type {?Snowflake} - * @readonly - */ - get voiceChannelID() { return this.voiceState.channel_id; } - /** * The time this member joined the guild * @type {?Date} @@ -191,33 +137,6 @@ class GuildMember extends Base { return (role && role.hexColor) || '#000000'; } - /** - * Whether this member is muted in any way - * @type {boolean} - * @readonly - */ - get mute() { - return this.selfMute || this.serverMute; - } - - /** - * Whether this member is deafened in any way - * @type {boolean} - * @readonly - */ - get deaf() { - return this.selfDeaf || this.serverDeaf; - } - - /** - * The voice channel this member is in, if any - * @type {?VoiceChannel} - * @readonly - */ - get voiceChannel() { - return this.guild.channels.get(this.voiceChannelID) || null; - } - /** * The ID of this member * @type {Snowflake} @@ -344,11 +263,6 @@ class GuildMember extends Base { const clone = this._clone(); data.user = this.user; clone._patch(data); - clone._frozenVoiceState = {}; - Object.assign(clone._frozenVoiceState, this.voiceState); - if (typeof data.mute !== 'undefined') clone._frozenVoiceState.mute = data.mute; - if (typeof data.deaf !== 'undefined') clone._frozenVoiceState.mute = data.deaf; - if (typeof data.channel_id !== 'undefined') clone._frozenVoiceState.channel_id = data.channel_id; return clone; }); } diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 716440051..c8b431076 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -1,5 +1,4 @@ const GuildChannel = require('./GuildChannel'); -const Collection = require('../util/Collection'); const { browser } = require('../util/Constants'); const Permissions = require('../util/Permissions'); const { Error } = require('../errors'); @@ -9,17 +8,6 @@ const { Error } = require('../errors'); * @extends {GuildChannel} */ class VoiceChannel extends GuildChannel { - constructor(guild, data) { - super(guild, data); - - /** - * The members in this voice channel - * @type {Collection} - * @name VoiceChannel#members - */ - Object.defineProperty(this, 'members', { value: new Collection() }); - } - _patch(data) { super._patch(data); /** @@ -35,6 +23,17 @@ class VoiceChannel extends GuildChannel { this.userLimit = data.user_limit; } + /** + * The members in this voice channel + * @type {Collection} + * @name VoiceChannel#members + */ + get members() { + return this.guild.voiceStates + .filter(state => state.channelID === this.id && state.member) + .map(state => state.member); + } + /** * The voice connection for this voice channel, if the client is connected * @type {?VoiceConnection} diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js new file mode 100644 index 000000000..faafc62c1 --- /dev/null +++ b/src/structures/VoiceState.js @@ -0,0 +1,131 @@ +const Base = require('./Base'); + +/** + * Represents the voice state for a Guild Member. + */ +class VoiceState extends Base { + constructor(guild, data) { + super(guild.client); + /** + * The guild of this voice state + * @type {Guild} + */ + this.guild = guild; + /** + * The ID of the member of this voice state + * @type {Snowflake} + */ + this.id = data.user_id; + this._patch(data); + } + + _patch(data) { + /** + * Whether this member is deafened server-wide + * @type {boolean} + */ + this.serverDeaf = data.deaf; + /** + * Whether this member is muted server-wide + * @type {boolean} + */ + this.serverMute = data.mute; + /** + * Whether this member is self-deafened + * @type {boolean} + */ + this.selfDeaf = data.self_deaf; + /** + * Whether this member is self-muted + * @type {boolean} + */ + this.selfMute = data.self_mute; + /** + * The session ID of this member's connection + * @type {String} + */ + this.sessionID = data.session_id; + /** + * The ID of the voice channel that this member is in + * @type {Snowflake} + */ + this.channelID = data.channel_id; + } + + /** + * The member that this voice state belongs to + * @type {GuildMember} + */ + get member() { + return this.guild.members.get(this.id); + } + + /** + * The channel that the member is connected to + * @type {VoiceChannel} + */ + get channel() { + return this.guild.channels.get(this.channelID); + } + + /** + * Whether this member is either self-deafened or server-deafened + * @type {boolean} + */ + get deaf() { + return this.serverDeaf || this.selfDeaf; + } + + /** + * Whether this member is either self-muted or server-muted + * @type {boolean} + */ + get mute() { + return this.serverMute || this.selfMute; + } + + /** + * Whether this member is currently speaking. A boolean if the information is available (aka + * the bot is connected to any voice channel in the guild), otherwise this is null + * @type {boolean|null} + */ + get speaking() { + return this.channel && this.channel.connection ? + Boolean(this.channel.connection._speaking.get(this.id)) : + null; + } + + /** + * Mutes/unmutes the member of this voice state. + * @param {boolean} mute Whether or not the member should be muted + * @param {string} [reason] Reason for muting or unmuting + * @returns {Promise} + */ + setMute(mute, reason) { + return this.member ? this.member.edit({ mute }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + + /** + * Deafens/undeafens the member of this voice state. + * @param {boolean} deaf Whether or not the member should be deafened + * @param {string} [reason] Reason for deafening or undeafening + * @returns {Promise} + */ + setDeaf(deaf, reason) { + return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + + toJSON() { + return super.toJSON({ + id: true, + serverDeaf: true, + serverMute: true, + selfDeaf: true, + selfMute: true, + sessionID: true, + channelID: 'channel', + }); + } +} + +module.exports = VoiceState; diff --git a/src/util/Structures.js b/src/util/Structures.js index e7f615c79..c81235033 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -73,6 +73,7 @@ const structures = { Message: require('../structures/Message'), MessageReaction: require('../structures/MessageReaction'), Presence: require('../structures/Presence').Presence, + VoiceState: require('../structures/VoiceState'), Role: require('../structures/Role'), User: require('../structures/User'), }; diff --git a/test/voice.js b/test/voice.js index 07cc3a4bf..8494425ca 100644 --- a/test/voice.js +++ b/test/voice.js @@ -33,7 +33,7 @@ client.on('message', m => { if (!m.guild) return; if (m.author.id !== '66564597481480192') return; if (m.content.startsWith('/join')) { - const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voiceChannel; + const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voice.channel; if (channel && channel.type === 'voice') { channel.join().then(conn => { const receiver = conn.createReceiver();