From eec7cf7634653fc02ee4f94e970960174a0e6d1b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 14 Apr 2021 13:35:55 +0100 Subject: [PATCH] feat: stage channels (#5456) * feat: add stage channel type * feat: initialise stage channel structure * feat: add STAGE_MODERATOR permissions bitfield * fix: typo in permissions * fix(Channel): type selection logic * feat: add rtcRegion to StageChannel and VoiceChannel * feat: rtc region editing for stage and voice channels * feat: stage channel userLimit * feat: add stage channels to exports * feat: add computed properties to stage channel * feat(VoiceState): include stage channel in docs * feat: allow ability to join stage channels * feat(StageChannel): join and leave methods * docs: add StageChannel link in GuildChannel docs * feat(VoiceState): suppress and requestToSpeakTimestamp * feat(StageChannel): setRequestToSpeak * refactor(StageChannel): update setRequestToSpeak * feat(VoiceState): add moveToSpeakers and moveToAudience * feat(VoiceState): add methods to move in/out of speakers * feat(VoiceState): add stage channel sanity checks * feat(Permissions): add REQUEST_TO_SPEAK * feat(VoiceState): simpler methods * docs(VoiceState): add documentation for new methods * refactor: remove unused error message * chore: remove debug statements * chore: revert changes to package-lock.json * docs(VoiceState): clarify suppress * docs(VoiceState): add missing @type param * feat(StageChannel): remove nsfw property * fix(VoiceState): check permissions in channel Co-authored-by: Advaith * fix(VoiceState): instantiate error with new Co-authored-by: BannerBomb * refactor(VoiceState): more readable API route builder Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> * style(VoiceState): fix lint errors * docs(VoiceState): add example usage for new methods * docs: setRTCRegion examples * chore: update typings * fix(VoiceState): calculate permissions for self * refactor(VoiceState): tidy up implementation * Update src/structures/VoiceState.js Co-authored-by: Jan <66554238+vaporox@users.noreply.github.com> * refactor: vaporox's suggestions * style(VoiceState): fix linter errors * chore: update typings * chore: remove unused error message * refactor(VoiceState): use optional chaining Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> * chore: move getters below constructor in typings * refactor(StageChannel): optional chaining Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> * style(VoiceState): fix lint errors * docs: fix incorrect types Co-authored-by: izexi <43889168+izexi@users.noreply.github.com> * Update src/structures/VoiceChannel.js Co-authored-by: izexi <43889168+izexi@users.noreply.github.com> * Update src/structures/VoiceChannel.js Co-authored-by: izexi <43889168+izexi@users.noreply.github.com> * refactor(VoiceState): use optional chaining Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> * refactor(StageChannel): remove permission override check in joinable * refactor: make ChannelTypes a proper enum * Use createEnum Co-authored-by: izexi <43889168+izexi@users.noreply.github.com> * chore: remove unused code from Constants * refactor(StageChannel): remove unnecessary getters * chore: update typings * refactor: introduce BaseGuildVoiceChannel class * refactor(VoiceChannel): reduce code duplication * feat: export BaseGuildVoiceChannel * chore: update typings * docs: fix typos * refactor: move setRTCRegion to BaseGuildVoiceChannel * feat(VoiceState): remove permission checks * chore: update typings * Apply suggestions from code review Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> Co-authored-by: Jan <66554238+vaporox@users.noreply.github.com> * chore: update esm exports and typings * Update src/structures/VoiceState.js Co-authored-by: Vlad Frangu Co-authored-by: Advaith Co-authored-by: BannerBomb Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> Co-authored-by: Jan <66554238+vaporox@users.noreply.github.com> Co-authored-by: izexi <43889168+izexi@users.noreply.github.com> Co-authored-by: Vlad Frangu --- esm/discord.mjs | 2 + src/client/voice/ClientVoiceManager.js | 4 +- src/client/voice/VoiceConnection.js | 8 +- src/errors/Messages.js | 4 +- src/index.js | 2 + src/structures/BaseGuildVoiceChannel.js | 109 ++++++++++++++++++++++++ src/structures/Channel.js | 8 +- src/structures/GuildChannel.js | 5 +- src/structures/StageChannel.js | 34 ++++++++ src/structures/VoiceChannel.js | 76 +++-------------- src/structures/VoiceState.js | 77 ++++++++++++++++- src/util/Constants.js | 22 +++-- src/util/Permissions.js | 9 ++ src/util/Structures.js | 2 + typings/index.d.ts | 66 +++++++++----- 15 files changed, 320 insertions(+), 108 deletions(-) create mode 100644 src/structures/BaseGuildVoiceChannel.js create mode 100644 src/structures/StageChannel.js diff --git a/esm/discord.mjs b/esm/discord.mjs index 560120878492..ccafcae94ec1 100644 --- a/esm/discord.mjs +++ b/esm/discord.mjs @@ -54,6 +54,7 @@ export const { Activity, APIMessage, BaseGuildEmoji, + BaseGuildVoiceChannel, CategoryChannel, Channel, ClientApplication, @@ -86,6 +87,7 @@ export const { RichPresenceAssets, Role, StoreChannel, + StageChannel, Team, TeamMember, TextChannel, diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 38c9f1dd145a..d9360f8c34bc 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -67,8 +67,8 @@ class ClientVoiceManager { } /** - * Sets up a request to join a voice channel. - * @param {VoiceChannel} channel The voice channel to join + * Sets up a request to join a voice or stage channel. + * @param {VoiceChannel|StageChannel} channel The channel to join * @returns {Promise} * @private */ diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 06362f949d0f..b5d6b3432342 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -45,8 +45,8 @@ class VoiceConnection extends EventEmitter { this.voiceManager = voiceManager; /** - * The voice channel this connection is currently serving - * @type {VoiceChannel} + * The voice channel or stage channel this connection is currently serving + * @type {VoiceChannel|StageChannel} */ this.channel = channel; @@ -306,8 +306,8 @@ class VoiceConnection extends EventEmitter { } /** - * Move to a different voice channel in the same guild. - * @param {VoiceChannel} channel The channel to move to + * Move to a different voice channel or stage channel in the same guild. + * @param {VoiceChannel|StageChannel} channel The channel to move to * @private */ updateChannel(channel) { diff --git a/src/errors/Messages.js b/src/errors/Messages.js index eed78f1f9aed..c84006503647 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -57,9 +57,11 @@ const Messages = { VOICE_PLAY_INTERFACE_NO_BROADCAST: 'A broadcast cannot be played in this context.', 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_NOT_STAGE_CHANNEL: 'You are only allowed to do this in stage channels.', VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.', - VOICE_STATE_NOT_OWN: 'You cannot self-deafen/mute on VoiceStates that do not belong to the ClientUser.', + VOICE_STATE_NOT_OWN: + 'You cannot self-deafen/mute/request to speak on VoiceStates that do not belong to the ClientUser.', VOICE_STATE_INVALID_TYPE: name => `${name} must be a boolean.`, 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 7255b7f95d8f..1dc667d9c2a7 100644 --- a/src/index.js +++ b/src/index.js @@ -63,6 +63,7 @@ module.exports = { Activity: require('./structures/Presence').Activity, APIMessage: require('./structures/APIMessage'), BaseGuildEmoji: require('./structures/BaseGuildEmoji'), + BaseGuildVoiceChannel: require('./structures/BaseGuildVoiceChannel'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), ClientApplication: require('./structures/ClientApplication'), @@ -98,6 +99,7 @@ module.exports = { RichPresenceAssets: require('./structures/Presence').RichPresenceAssets, Role: require('./structures/Role'), StoreChannel: require('./structures/StoreChannel'), + StageChannel: require('./structures/StageChannel'), Team: require('./structures/Team'), TeamMember: require('./structures/TeamMember'), TextChannel: require('./structures/TextChannel'), diff --git a/src/structures/BaseGuildVoiceChannel.js b/src/structures/BaseGuildVoiceChannel.js new file mode 100644 index 000000000000..be747affece6 --- /dev/null +++ b/src/structures/BaseGuildVoiceChannel.js @@ -0,0 +1,109 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const Collection = require('../util/Collection'); +const Permissions = require('../util/Permissions'); + +/** + * Represents a voice-based guild channel on Discord. + * @extends {GuildChannel} + */ +class BaseGuildVoiceChannel extends GuildChannel { + _patch(data) { + super._patch(data); + + /** + * The RTC region for this voice-based channel. This region is automatically selected if `null`. + * @type {?string} + */ + this.rtcRegion = data.rtc_region; + + /** + * The bitrate of this voice-based channel + * @type {number} + */ + this.bitrate = data.bitrate; + + /** + * The maximum amount of users allowed in this channel. + * @type {number} + */ + this.userLimit = data.user_limit; + } + + /** + * The members in this voice-based channel + * @type {Collection} + * @readonly + */ + get members() { + const coll = new Collection(); + for (const state of this.guild.voiceStates.cache.values()) { + if (state.channelID === this.id && state.member) { + coll.set(state.id, state.member); + } + } + return coll; + } + + /** + * Checks if the voice-based channel is full + * @type {boolean} + * @readonly + */ + get full() { + return this.userLimit > 0 && this.members.size >= this.userLimit; + } + + /** + * Whether the channel is joinable by the client user + * @type {boolean} + * @readonly + */ + get joinable() { + if (!this.viewable) return false; + if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) return false; + return true; + } + + /** + * Attempts to join this voice-based channel. + * @returns {Promise} + * @example + * // Join a voice-based channel + * channel.join() + * .then(connection => console.log('Connected!')) + * .catch(console.error); + */ + join() { + return this.client.voice.joinChannel(this); + } + + /** + * Leaves this voice-based channel. + * @example + * // Leave a voice-based channel + * channel.leave(); + */ + leave() { + const connection = this.client.voice.connections.get(this.guild.id); + if (connection?.channel.id === this.id) connection.disconnect(); + } + + /** + * Sets the RTC region of the channel. + * @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel + * @returns {Promise} + * @example + * // Set the RTC region to europe + * channel.setRTCRegion('europe'); + * @example + * // Remove a fixed region for this channel - let Discord decide automatically + * channel.setRTCRegion(null); + */ + setRTCRegion(region) { + return this.edit({ rtcRegion: region }); + } +} + +module.exports = BaseGuildVoiceChannel; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index baa02ee64edb..63ce08b444e6 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -13,7 +13,7 @@ class Channel extends Base { constructor(client, data) { super(client); - const type = Object.keys(ChannelTypes)[data.type]; + const type = ChannelTypes[data.type]; /** * The type of the channel, either: * * `dm` - a DM channel @@ -22,6 +22,7 @@ class Channel extends Base { * * `category` - a guild category channel * * `news` - a guild news channel * * `store` - a guild store channel + * * `stage` - a guild stage channel * * `unknown` - a generic channel of unknown type, could be Channel or GuildChannel * @type {string} */ @@ -146,6 +147,11 @@ class Channel extends Base { channel = new StoreChannel(guild, data); break; } + case ChannelTypes.STAGE: { + const StageChannel = Structures.get('StageChannel'); + channel = new StageChannel(guild, data); + break; + } } if (channel) guild.channels.cache.set(channel.id, channel); } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index fb0979cc3329..3d3188753d44 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -18,6 +18,7 @@ const Util = require('../util/Util'); * - {@link CategoryChannel} * - {@link NewsChannel} * - {@link StoreChannel} + * - {@link StageChannel} * @extends {Channel} * @abstract */ @@ -319,6 +320,7 @@ class GuildChannel extends Channel { * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] * Permission overwrites for the channel * @property {number} [rateLimitPerUser] The ratelimit per user for the channel in seconds + * @property {?string} [rtcRegion] The RTC region of the channel */ /** @@ -374,6 +376,7 @@ class GuildChannel extends Channel { nsfw: data.nsfw, bitrate: data.bitrate || this.bitrate, user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit, + rtc_region: typeof data.rtcRegion !== 'undefined' ? data.rtcRegion : this.rtcRegion, parent_id: data.parentID, lock_permissions: data.lockPermissions, rate_limit_per_user: data.rateLimitPerUser, @@ -594,7 +597,7 @@ class GuildChannel extends Channel { */ get manageable() { if (this.client.user.id === this.guild.ownerID) return true; - if (this.type === 'voice') { + if (this.type === 'voice' || this.type === 'stage') { if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) { return false; } diff --git a/src/structures/StageChannel.js b/src/structures/StageChannel.js new file mode 100644 index 000000000000..ce2b9bd11c10 --- /dev/null +++ b/src/structures/StageChannel.js @@ -0,0 +1,34 @@ +'use strict'; + +const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); + +/** + * Represents a guild stage channel on Discord. + * @extends {BaseGuildVoiceChannel} + */ +class StageChannel extends BaseGuildVoiceChannel { + _patch(data) { + super._patch(data); + + /** + * The topic of the stage channel + * @type {?string} + */ + this.topic = data.topic; + } + + /** + * Sets the RTC region of the channel. + * @name StageChannel#setRTCRegion + * @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel + * @returns {Promise} + * @example + * // Set the RTC region to europe + * stageChannel.setRTCRegion('europe'); + * @example + * // Remove a fixed region for this channel - let Discord decide automatically + * stageChannel.setRTCRegion(null); + */ +} + +module.exports = StageChannel; diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index fcad7dffe165..58122c1b14c0 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -1,53 +1,13 @@ 'use strict'; -const GuildChannel = require('./GuildChannel'); -const Collection = require('../util/Collection'); +const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); const Permissions = require('../util/Permissions'); /** * Represents a guild voice channel on Discord. - * @extends {GuildChannel} + * @extends {BaseGuildVoiceChannel} */ -class VoiceChannel extends GuildChannel { - _patch(data) { - super._patch(data); - /** - * The bitrate of this voice channel - * @type {number} - */ - this.bitrate = data.bitrate; - - /** - * The maximum amount of users allowed in this channel - 0 means unlimited. - * @type {number} - */ - this.userLimit = data.user_limit; - } - - /** - * The members in this voice channel - * @type {Collection} - * @readonly - */ - get members() { - const coll = new Collection(); - for (const state of this.guild.voiceStates.cache.values()) { - if (state.channelID === this.id && state.member) { - coll.set(state.id, state.member); - } - } - return coll; - } - - /** - * Checks if the voice channel is full - * @type {boolean} - * @readonly - */ - get full() { - return this.userLimit > 0 && this.members.size >= this.userLimit; - } - +class VoiceChannel extends BaseGuildVoiceChannel { /** * Whether the channel is deletable by the client user * @type {boolean} @@ -72,8 +32,7 @@ class VoiceChannel extends GuildChannel { * @readonly */ get joinable() { - if (!this.viewable) return false; - if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) return false; + if (!super.joinable) return false; if (this.full && !this.permissionsFor(this.client.user).has(Permissions.FLAGS.MOVE_MEMBERS, false)) return false; return true; } @@ -118,28 +77,17 @@ class VoiceChannel extends GuildChannel { } /** - * Attempts to join this voice channel. - * @returns {Promise} + * Sets the RTC region of the channel. + * @name VoiceChannel#setRTCRegion + * @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel + * @returns {Promise} * @example - * // Join a voice channel - * voiceChannel.join() - * .then(connection => console.log('Connected!')) - * .catch(console.error); - */ - join() { - return this.client.voice.joinChannel(this); - } - - /** - * Leaves this voice channel. + * // Set the RTC region to europe + * voiceChannel.setRTCRegion('europe'); * @example - * // Leave a voice channel - * voiceChannel.leave(); + * // Remove a fixed region for this channel - let Discord decide automatically + * voiceChannel.setRTCRegion(null); */ - leave() { - const connection = this.client.voice.connections.get(this.guild.id); - if (connection && connection.channel.id === this.id) connection.disconnect(); - } } module.exports = VoiceChannel; diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 3b4a95494b2d..cd559cc25182 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -63,10 +63,22 @@ class VoiceState extends Base { */ this.streaming = data.self_stream || false; /** - * The ID of the voice channel that this member is in + * The ID of the voice or stage channel that this member is in * @type {?Snowflake} */ this.channelID = data.channel_id || null; + /** + * Whether this member is suppressed from speaking. This property is specific to stage channels only. + * @type {boolean} + */ + this.suppress = data.suppress; + /** + * 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 + ? new Date(data.request_to_speak_timestamp).getTime() + : null; return this; } @@ -81,7 +93,7 @@ class VoiceState extends Base { /** * The channel that the member is connected to - * @type {?VoiceChannel} + * @type {?VoiceChannel|StageChannel} * @readonly */ get channel() { @@ -118,7 +130,7 @@ class VoiceState extends Base { /** * 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 + * the bot is connected to any voice channel or stage channel in the guild), otherwise this is `null` * @type {?boolean} * @readonly */ @@ -147,7 +159,7 @@ class VoiceState extends Base { } /** - * Kicks the member from the voice channel. + * Kicks the member from the channel. * @param {string} [reason] Reason for kicking member from the channel * @returns {Promise} */ @@ -196,6 +208,63 @@ class VoiceState extends Base { return true; } + /** + * Toggles the request to speak in the channel. + * Only applicable for stage channels and for the client's own voice state. + * @param {boolean} request 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); + */ + async setRequestToSpeak(request) { + const channel = this.channel; + if (channel?.type !== 'stage') 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 - 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); + */ + async setSuppressed(suppressed) { + if (typeof suppressed !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'suppressed'); + + const channel = this.channel; + if (channel?.type !== 'stage') 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, + }, + }); + } + toJSON() { return super.toJSON({ id: true, diff --git a/src/util/Constants.js b/src/util/Constants.js index cc978604206a..735b6981cf17 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -477,15 +477,19 @@ exports.SystemMessageTypes = exports.MessageTypes.filter(type => type && type != */ exports.ActivityTypes = ['PLAYING', 'STREAMING', 'LISTENING', 'WATCHING', 'CUSTOM_STATUS', 'COMPETING']; -exports.ChannelTypes = { - TEXT: 0, - DM: 1, - VOICE: 2, - GROUP: 3, - CATEGORY: 4, - NEWS: 5, - STORE: 6, -}; +exports.ChannelTypes = createEnum([ + 'TEXT', + 'DM', + 'VOICE', + 'GROUP', + 'CATEGORY', + 'NEWS', + // 6 + 'STORE', + ...Array(6).fill(null), + // 13 + 'STAGE', +]); exports.ClientApplicationAssetTypes = { SMALL: 1, diff --git a/src/util/Permissions.js b/src/util/Permissions.js index efaf1e72daca..249c8ba767ea 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -78,6 +78,7 @@ class Permissions extends BitField { * * `MANAGE_ROLES` * * `MANAGE_WEBHOOKS` * * `MANAGE_EMOJIS` + * * `REQUEST_TO_SPEAK` * @type {Object} * @see {@link https://discord.com/developers/docs/topics/permissions} */ @@ -113,6 +114,7 @@ Permissions.FLAGS = { MANAGE_ROLES: 1n << 28n, MANAGE_WEBHOOKS: 1n << 29n, MANAGE_EMOJIS: 1n << 30n, + REQUEST_TO_SPEAK: 1n << 32n, }; /** @@ -127,6 +129,13 @@ Permissions.ALL = Object.values(Permissions.FLAGS).reduce((all, p) => all | p, 0 */ Permissions.DEFAULT = BigInt(104324673); +/** + * Bitfield representing the permissions required for moderators of stage channels + * @type {bigint} + */ +Permissions.STAGE_MODERATOR = + Permissions.FLAGS.MANAGE_CHANNELS | Permissions.FLAGS.MUTE_MEMBERS | Permissions.FLAGS.MOVE_MEMBERS; + Permissions.defaultBit = BigInt(0); module.exports = Permissions; diff --git a/src/util/Structures.js b/src/util/Structures.js index 0c6ab35c7c1d..e850821b4f84 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -9,6 +9,7 @@ * * **`CategoryChannel`** * * **`NewsChannel`** * * **`StoreChannel`** + * * **`StageChannel`** * * **`GuildMember`** * * **`Guild`** * * **`Message`** @@ -98,6 +99,7 @@ const structures = { CategoryChannel: require('../structures/CategoryChannel'), NewsChannel: require('../structures/NewsChannel'), StoreChannel: require('../structures/StoreChannel'), + StageChannel: require('../structures/StageChannel'), GuildMember: require('../structures/GuildMember'), Guild: require('../structures/Guild'), Message: require('../structures/Message'), diff --git a/typings/index.d.ts b/typings/index.d.ts index ea41f7e95631..6603e96d1d77 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -7,6 +7,18 @@ declare enum ChannelType { news = 5, store = 6, unknown = 7, + stage = 13, +} + +declare enum ChannelTypes { + TEXT = 0, + DM = 1, + VOICE = 2, + GROUP = 3, + CATEGORY = 4, + NEWS = 5, + STORE = 6, + STAGE = 13, } declare enum OverwriteTypes { @@ -166,6 +178,19 @@ declare module 'discord.js' { public requiresColons: boolean | null; } + export class BaseGuildVoiceChannel extends GuildChannel { + constructor(guild: Guild, data?: object); + public readonly members: Collection; + public readonly full: boolean; + public readonly joinable: boolean; + public rtcRegion: string | null; + public bitrate: number; + public userLimit: number; + public join(): Promise; + public leave(): void; + public setRTCRegion(region: string | null): Promise; + } + class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { public broadcast: VoiceBroadcast; } @@ -293,7 +318,7 @@ declare module 'discord.js' { public connections: Collection; public broadcasts: VoiceBroadcast[]; - private joinChannel(channel: VoiceChannel): Promise; + private joinChannel(channel: VoiceChannel | StageChannel): Promise; public createBroadcast(): VoiceBroadcast; } @@ -520,15 +545,7 @@ declare module 'discord.js' { CLIENT_CONNECT: 12; CLIENT_DISCONNECT: 13; }; - ChannelTypes: { - TEXT: 0; - DM: 1; - VOICE: 2; - GROUP: 3; - CATEGORY: 4; - NEWS: 5; - STORE: 6; - }; + ChannelTypes: typeof ChannelTypes; ClientApplicationAssetTypes: { SMALL: 1; BIG: 2; @@ -1213,6 +1230,7 @@ declare module 'discord.js' { public static ALL: bigint; public static DEFAULT: bigint; + public static STAGE_MODERATOR: bigint; public static FLAGS: PermissionFlags; public static resolve(permission?: PermissionResolvable): bigint; } @@ -1428,6 +1446,11 @@ declare module 'discord.js' { public static resolve(bit?: BitFieldResolvable): number; } + export class StageChannel extends BaseGuildVoiceChannel { + public topic: string | null; + public type: 'stage'; + } + export class StoreChannel extends GuildChannel { constructor(guild: Guild, data?: object); public nsfw: boolean; @@ -1623,17 +1646,10 @@ declare module 'discord.js' { public once(event: string, listener: (...args: any[]) => void): this; } - export class VoiceChannel extends GuildChannel { - constructor(guild: Guild, data?: object); - public bitrate: number; + export class VoiceChannel extends BaseGuildVoiceChannel { public readonly editable: boolean; - public readonly full: boolean; - public readonly joinable: boolean; public readonly speakable: boolean; public type: 'voice'; - public userLimit: number; - public join(): Promise; - public leave(): void; public setBitrate(bitrate: number, reason?: string): Promise; public setUserLimit(userLimit: number, reason?: string): Promise; } @@ -1657,9 +1673,9 @@ declare module 'discord.js' { private sendVoiceStateUpdate(options: object): Promise; private setSessionID(sessionID: string): void; private setTokenAndEndpoint(token: string, endpoint: string): void; - private updateChannel(channel: VoiceChannel): void; + private updateChannel(channel: VoiceChannel | StageChannel): void; - public channel: VoiceChannel; + public channel: VoiceChannel | StageChannel; public readonly client: Client; public readonly dispatcher: StreamDispatcher | null; public player: object; @@ -1717,7 +1733,7 @@ declare module 'discord.js' { export class VoiceState extends Base { constructor(guild: Guild, data: object); - public readonly channel: VoiceChannel | null; + public readonly channel: VoiceChannel | StageChannel | null; public channelID: Snowflake | null; public readonly connection: VoiceConnection | null; public readonly deaf: boolean | null; @@ -1732,6 +1748,8 @@ declare module 'discord.js' { public sessionID: string | null; public streaming: boolean; public selfVideo: boolean; + public suppress: boolean; + public requestToSpeakTimestamp: number | null; public readonly speaking: boolean | null; public setDeaf(deaf: boolean, reason?: string): Promise; @@ -1740,6 +1758,8 @@ declare module 'discord.js' { public setChannel(channel: ChannelResolvable | null, reason?: string): Promise; public setSelfDeaf(deaf: boolean): Promise; public setSelfMute(mute: boolean): Promise; + public setRequestToSpeak(request: boolean): Promise; + public setSuppressed(suppressed: boolean): Promise; } class VolumeInterface extends EventEmitter { @@ -2411,6 +2431,7 @@ declare module 'discord.js' { rateLimitPerUser?: number; lockPermissions?: boolean; permissionOverwrites?: readonly OverwriteResolvable[] | Collection; + rtcRegion?: string | null; } interface ChannelLogsQueryOptions { @@ -3104,7 +3125,8 @@ declare module 'discord.js' { | 'MANAGE_NICKNAMES' | 'MANAGE_ROLES' | 'MANAGE_WEBHOOKS' - | 'MANAGE_EMOJIS'; + | 'MANAGE_EMOJIS' + | 'REQUEST_TO_SPEAK'; interface RecursiveArray extends ReadonlyArray> {}