'use strict'; const Base = require('./Base'); const { Error, TypeError } = require('../errors'); /** * 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) { if ('deaf' in data) { /** * Whether this member is deafened server-wide * @type {?boolean} */ this.serverDeaf = data.deaf; } else { this.serverDeaf ??= null; } if ('mute' in data) { /** * Whether this member is muted server-wide * @type {?boolean} */ this.serverMute = data.mute; } else { this.serverMute ??= null; } if ('self_deaf' in data) { /** * Whether this member is self-deafened * @type {?boolean} */ this.selfDeaf = data.self_deaf; } else { this.selfDeaf ??= null; } if ('self_mute' in data) { /** * Whether this member is self-muted * @type {?boolean} */ this.selfMute = data.self_mute; } else { this.selfMute ??= null; } if ('self_video' in data) { /** * Whether this member's camera is enabled * @type {?boolean} */ this.selfVideo = data.self_video; } else { this.selfVideo ??= null; } if ('session_id' in data) { /** * The session id for this member's connection * @type {?string} */ this.sessionId = data.session_id; } else { this.sessionId ??= null; } // The self_stream is property is omitted if false, check for another property // here to avoid incorrectly clearing this when partial data is specified if ('self_video' in data) { /** * Whether this member is streaming using "Screen Share" * @type {boolean} */ this.streaming = data.self_stream ?? false; } else { this.streaming ??= null; } if ('channel_id' in data) { /** * The {@link VoiceChannel} or {@link StageChannel} id the member is in * @type {?Snowflake} */ this.channelId = data.channel_id; } else { this.channelId ??= null; } if ('suppress' in data) { /** * Whether this member is suppressed from speaking. This property is specific to stage channels only. * @type {boolean} */ this.suppress = data.suppress; } if ('request_to_speak_timestamp' in data) { /** * The time at which the member requested to speak. This property is specific to stage channels only. * @type {?number} */ this.requestToSpeakTimestamp = data.request_to_speak_timestamp && Date.parse(data.request_to_speak_timestamp); } else { this.requestToSpeakTimestamp ??= null; } return this; } /** * The member that this voice state belongs to * @type {?GuildMember} * @readonly */ get member() { if (!this.guild?.id) return null; return this.guild.members.cache.get(this.id) ?? null; } /** * The user that this voice state belongs to * @type {?User} * @readonly */ get user() { return this.guild.client.users.cache.get(this.id) ?? null; } /** * The channel that the member is connected to * @type {?(VoiceChannel|StageChannel)} * @readonly */ get channel() { if (!this.guild?.id) return this.guild.client.channels.cache.get(this.channelId) ?? null; return this.guild.channels.cache.get(this.channelId) ?? null; } /** * Whether this member is either self-deafened or server-deafened * @type {?boolean} * @readonly */ get deaf() { return this.serverDeaf || this.selfDeaf; } /** * Whether this member is either self-muted or server-muted * @type {?boolean} * @readonly */ get mute() { return this.serverMute || this.selfMute; } /** * Mutes/unmutes the member of this voice state. * @param {boolean} [mute=true] Whether or not the member should be muted * @param {string} [reason] Reason for muting or unmuting * @returns {Promise} */ setMute(mute = true, reason) { if (!this.guild?.id) return null; return this.guild.members.edit(this.id, { mute }, reason); } /** * Deafens/undeafens the member of this voice state. * @param {boolean} [deaf=true] Whether or not the member should be deafened * @param {string} [reason] Reason for deafening or undeafening * @returns {Promise} */ setDeaf(deaf = true, reason) { if (!this.guild?.id) return null; return this.guild.members.edit(this.id, { deaf }, reason); } /** * Disconnects the member from the channel. * @param {string} [reason] Reason for disconnecting the member from the channel * @returns {Promise} */ disconnect(reason) { if (!this.guild?.id) return this.callVoice?.disconnect(); return this.setChannel(null, reason); } /** * Moves the member to a different channel, or disconnects them from the one they're in. * @param {GuildVoiceChannelResolvable|null} channel Channel to move the member to, or `null` if you want to * disconnect them from voice. * @param {string} [reason] Reason for moving member to another channel or disconnecting * @returns {Promise} */ setChannel(channel, reason) { if (!this.guild?.id) return null; return this.guild.members.edit(this.id, { channel }, reason); } /** * Toggles the request to speak in the channel. * Only applicable for stage channels and for the client's own voice state. * @param {boolean} [request=true] Whether or not the client is requesting to become a speaker. * @example * // Making the client request to speak in a stage channel (raise its hand) * guild.me.voice.setRequestToSpeak(true); * @example * // Making the client cancel a request to speak * guild.me.voice.setRequestToSpeak(false); * @returns {Promise} */ async setRequestToSpeak(request = true) { if (this.guild?.id) { if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN'); await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({ data: { channel_id: this.channelId, request_to_speak_timestamp: request ? new Date().toISOString() : null, }, }); } } /** * Suppress/unsuppress the user. Only applicable for stage channels. * @param {boolean} [suppressed=true] Whether or not the user should be suppressed. * @example * // Making the client a speaker * guild.me.voice.setSuppressed(false); * @example * // Making the client an audience member * guild.me.voice.setSuppressed(true); * @example * // Inviting another user to speak * voiceState.setSuppressed(false); * @example * // Moving another user to the audience, or cancelling their invite to speak * voiceState.setSuppressed(true); * @returns {Promise} */ async setSuppressed(suppressed = true) { if (this.guild?.id) { if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed'); if (this.channel?.type !== 'GUILD_STAGE_VOICE') throw new Error('VOICE_NOT_STAGE_CHANNEL'); const target = this.client.user.id === this.id ? '@me' : this.id; await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({ data: { channel_id: this.channelId, suppress: suppressed, }, }); } } /** * Get URL Image of the user's streaming video (NOT STREAMING !!!) * @returns {string} URL Image of the user's streaming video */ async getPreview() { if (!this.guild?.id) return null; if (!this.streaming) throw new Error('USER_NOT_STREAMING'); // URL: https://discord.com/api/v9/streams/guild:guildid:voicechannelid:userid/preview const data = await this.client.api.streams[ `guild%3A${this.guild.id}%3A${this.channelId}%3A${this.id}` ].preview.get(); return data.url; } toJSON() { return super.toJSON({ id: true, serverDeaf: true, serverMute: true, selfDeaf: true, selfMute: true, sessionId: true, channelId: 'channel', }); } } module.exports = VoiceState;