rewrite voice state handling

This commit is contained in:
Amish Shah
2018-08-10 14:44:59 +01:00
parent 0f63c50c06
commit be5efea461
11 changed files with 187 additions and 144 deletions

View File

@@ -31,8 +31,8 @@ client.on('message', async message => {
if (message.content === '/join') { if (message.content === '/join') {
// Only try to join the sender's voice channel if they are in one themselves // Only try to join the sender's voice channel if they are in one themselves
if (message.member.voiceChannel) { if (message.member.voice.channel) {
const connection = await message.member.voiceChannel.join(); const connection = await message.member.voice.channel.join();
} else { } else {
message.reply('You need to join a voice channel first!'); message.reply('You need to join a voice channel first!');
} }

View File

@@ -9,28 +9,27 @@ class VoiceStateUpdateHandler extends AbstractHandler {
const guild = client.guilds.get(data.guild_id); const guild = client.guilds.get(data.guild_id);
if (guild) { 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); const member = guild.members.get(data.user_id);
if (member) { if (member) {
const oldMember = member._clone();
oldMember._frozenVoiceState = oldMember.voiceState;
if (member.user.id === client.user.id && data.channel_id) { if (member.user.id === client.user.id && data.channel_id) {
client.emit('self.voiceStateUpdate', data); client.emit('self.voiceStateUpdate', data);
} }
client.emit(Events.VOICE_STATE_UPDATE, oldState, member.voiceState);
guild.voiceStates.set(member.user.id, data);
client.emit(Events.VOICE_STATE_UPDATE, oldMember, member);
} }
} }
} }
} }
/** /**
* 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 * @event Client#voiceStateUpdate
* @param {GuildMember} oldMember The member before the voice state update * @param {VoiceState} oldState The voice state before the update
* @param {GuildMember} newMember The member after the voice state update * @param {VoiceState} newState The voice state after the update
*/ */
module.exports = VoiceStateUpdateHandler; module.exports = VoiceStateUpdateHandler;

View File

@@ -53,6 +53,8 @@ const Messages = {
VOICE_PLAY_INTERFACE_BAD_TYPE: 'Unknown stream type', 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_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.', OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.',
UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.',

View File

@@ -83,6 +83,7 @@ module.exports = {
UserConnection: require('./structures/UserConnection'), UserConnection: require('./structures/UserConnection'),
VoiceChannel: require('./structures/VoiceChannel'), VoiceChannel: require('./structures/VoiceChannel'),
VoiceRegion: require('./structures/VoiceRegion'), VoiceRegion: require('./structures/VoiceRegion'),
VoiceState: require('./structures/VoiceState'),
Webhook: require('./structures/Webhook'), Webhook: require('./structures/Webhook'),
WebSocket: require('./WebSocket'), WebSocket: require('./WebSocket'),

View File

@@ -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;

View File

@@ -12,6 +12,7 @@ const RoleStore = require('../stores/RoleStore');
const GuildEmojiStore = require('../stores/GuildEmojiStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore');
const GuildChannelStore = require('../stores/GuildChannelStore'); const GuildChannelStore = require('../stores/GuildChannelStore');
const PresenceStore = require('../stores/PresenceStore'); const PresenceStore = require('../stores/PresenceStore');
const VoiceStateStore = require('../stores/VoiceStateStore');
const Base = require('./Base'); const Base = require('./Base');
const { Error, TypeError } = require('../errors'); 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) { 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) { 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; module.exports = Guild;

View File

@@ -56,18 +56,6 @@ class GuildMember extends Base {
if (data) this._patch(data); 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) { _patch(data) {
/** /**
* The nickname of this member, if they have one * 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; return (channel && channel.messages.get(this.lastMessageID)) || null;
} }
get voiceState() { get voice() {
return this._frozenVoiceState || this.guild.voiceStates.get(this.id) || {}; 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 * The time this member joined the guild
* @type {?Date} * @type {?Date}
@@ -191,33 +137,6 @@ class GuildMember extends Base {
return (role && role.hexColor) || '#000000'; 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 * The ID of this member
* @type {Snowflake} * @type {Snowflake}
@@ -344,11 +263,6 @@ class GuildMember extends Base {
const clone = this._clone(); const clone = this._clone();
data.user = this.user; data.user = this.user;
clone._patch(data); 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; return clone;
}); });
} }

View File

@@ -1,5 +1,4 @@
const GuildChannel = require('./GuildChannel'); const GuildChannel = require('./GuildChannel');
const Collection = require('../util/Collection');
const { browser } = require('../util/Constants'); const { browser } = require('../util/Constants');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const { Error } = require('../errors'); const { Error } = require('../errors');
@@ -9,17 +8,6 @@ const { Error } = require('../errors');
* @extends {GuildChannel} * @extends {GuildChannel}
*/ */
class VoiceChannel extends GuildChannel { class VoiceChannel extends GuildChannel {
constructor(guild, data) {
super(guild, data);
/**
* The members in this voice channel
* @type {Collection<Snowflake, GuildMember>}
* @name VoiceChannel#members
*/
Object.defineProperty(this, 'members', { value: new Collection() });
}
_patch(data) { _patch(data) {
super._patch(data); super._patch(data);
/** /**
@@ -35,6 +23,17 @@ class VoiceChannel extends GuildChannel {
this.userLimit = data.user_limit; this.userLimit = data.user_limit;
} }
/**
* The members in this voice channel
* @type {Collection<Snowflake, GuildMember>}
* @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 * The voice connection for this voice channel, if the client is connected
* @type {?VoiceConnection} * @type {?VoiceConnection}

View File

@@ -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<GuildMember>}
*/
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<GuildMember>}
*/
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;

View File

@@ -73,6 +73,7 @@ const structures = {
Message: require('../structures/Message'), Message: require('../structures/Message'),
MessageReaction: require('../structures/MessageReaction'), MessageReaction: require('../structures/MessageReaction'),
Presence: require('../structures/Presence').Presence, Presence: require('../structures/Presence').Presence,
VoiceState: require('../structures/VoiceState'),
Role: require('../structures/Role'), Role: require('../structures/Role'),
User: require('../structures/User'), User: require('../structures/User'),
}; };

View File

@@ -33,7 +33,7 @@ client.on('message', m => {
if (!m.guild) return; if (!m.guild) return;
if (m.author.id !== '66564597481480192') return; if (m.author.id !== '66564597481480192') return;
if (m.content.startsWith('/join')) { 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') { if (channel && channel.type === 'voice') {
channel.join().then(conn => { channel.join().then(conn => {
const receiver = conn.createReceiver(); const receiver = conn.createReceiver();