mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
rewrite voice state handling
This commit is contained in:
@@ -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!');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
20
src/stores/VoiceStateStore.js
Normal file
20
src/stores/VoiceStateStore.js
Normal 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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
131
src/structures/VoiceState.js
Normal file
131
src/structures/VoiceState.js
Normal 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;
|
||||||
@@ -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'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user