From 918921e8211fc16e9b12d2502f3168264246ea22 Mon Sep 17 00:00:00 2001 From: Shubham Parihar Date: Mon, 14 Jun 2021 17:27:37 +0530 Subject: [PATCH] feat: stage instances (#5749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vlad Frangu Co-authored-by: SpaceEEC Co-authored-by: Antonio Román --- src/client/actions/ActionsManager.js | 3 + src/client/actions/StageInstanceCreate.js | 28 ++++ src/client/actions/StageInstanceDelete.js | 32 ++++ src/client/actions/StageInstanceUpdate.js | 30 ++++ .../handlers/STAGE_INSTANCE_CREATE.js | 5 + .../handlers/STAGE_INSTANCE_DELETE.js | 5 + .../handlers/STAGE_INSTANCE_UPDATE.js | 5 + src/errors/Messages.js | 1 + src/managers/StageInstanceManager.js | 150 ++++++++++++++++++ src/structures/Guild.js | 14 ++ src/structures/GuildAuditLogs.js | 52 +++++- src/structures/StageChannel.js | 9 ++ src/structures/StageInstance.js | 143 +++++++++++++++++ src/util/Constants.js | 19 ++- src/util/Structures.js | 2 + typings/index.d.ts | 66 +++++++- 16 files changed, 556 insertions(+), 8 deletions(-) create mode 100644 src/client/actions/StageInstanceCreate.js create mode 100644 src/client/actions/StageInstanceDelete.js create mode 100644 src/client/actions/StageInstanceUpdate.js create mode 100644 src/client/websocket/handlers/STAGE_INSTANCE_CREATE.js create mode 100644 src/client/websocket/handlers/STAGE_INSTANCE_DELETE.js create mode 100644 src/client/websocket/handlers/STAGE_INSTANCE_UPDATE.js create mode 100644 src/managers/StageInstanceManager.js create mode 100644 src/structures/StageInstance.js diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 3a239595e..50178b0fa 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -38,6 +38,9 @@ class ActionsManager { this.register(require('./GuildIntegrationsUpdate')); this.register(require('./WebhooksUpdate')); this.register(require('./TypingStart')); + this.register(require('./StageInstanceCreate')); + this.register(require('./StageInstanceUpdate')); + this.register(require('./StageInstanceDelete')); } register(Action) { diff --git a/src/client/actions/StageInstanceCreate.js b/src/client/actions/StageInstanceCreate.js new file mode 100644 index 000000000..75b94bdd3 --- /dev/null +++ b/src/client/actions/StageInstanceCreate.js @@ -0,0 +1,28 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class StageInstanceCreateAction extends Action { + handle(data) { + const client = this.client; + const channel = this.getChannel(data); + + if (channel) { + const stageInstance = channel.guild.stageInstances.add(data); + + /** + * Emitted whenever a stage instance is created. + * @event Client#stageInstanceCreate + * @param {StageInstance} stageInstance The created stage instance + */ + client.emit(Events.STAGE_INSTANCE_CREATE, stageInstance); + + return { stageInstance }; + } + + return {}; + } +} + +module.exports = StageInstanceCreateAction; diff --git a/src/client/actions/StageInstanceDelete.js b/src/client/actions/StageInstanceDelete.js new file mode 100644 index 000000000..b665996a3 --- /dev/null +++ b/src/client/actions/StageInstanceDelete.js @@ -0,0 +1,32 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class StageInstanceDeleteAction extends Action { + handle(data) { + const client = this.client; + const channel = this.getChannel(data); + + if (channel) { + const stageInstance = channel.guild.stageInstances.add(data); + if (stageInstance) { + channel.guild.stageInstances.cache.delete(stageInstance.id); + stageInstance.deleted = true; + + /** + * Emitted whenever a stage instance is deleted. + * @event Client#stageInstanceDelete + * @param {StageInstance} stageInstance The deleted stage instance + */ + client.emit(Events.STAGE_INSTANCE_DELETE, stageInstance); + + return { stageInstance }; + } + } + + return {}; + } +} + +module.exports = StageInstanceDeleteAction; diff --git a/src/client/actions/StageInstanceUpdate.js b/src/client/actions/StageInstanceUpdate.js new file mode 100644 index 000000000..a13788e41 --- /dev/null +++ b/src/client/actions/StageInstanceUpdate.js @@ -0,0 +1,30 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class StageInstanceUpdateAction extends Action { + handle(data) { + const client = this.client; + const channel = this.getChannel(data); + + if (channel) { + const oldStageInstance = channel.guild.stageInstances.cache.get(data.id)?._clone() ?? null; + const newStageInstance = channel.guild.stageInstances.add(data); + + /** + * Emitted whenever a stage instance gets updated - e.g. change in topic or privacy level + * @event Client#stageInstanceUpdate + * @param {?StageInstance} oldStageInstance The stage instance before the update + * @param {StageInstance} newStageInstance The stage instance after the update + */ + client.emit(Events.STAGE_INSTANCE_UPDATE, oldStageInstance, newStageInstance); + + return { oldStageInstance, newStageInstance }; + } + + return {}; + } +} + +module.exports = StageInstanceUpdateAction; diff --git a/src/client/websocket/handlers/STAGE_INSTANCE_CREATE.js b/src/client/websocket/handlers/STAGE_INSTANCE_CREATE.js new file mode 100644 index 000000000..77ae2ff3c --- /dev/null +++ b/src/client/websocket/handlers/STAGE_INSTANCE_CREATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.StageInstanceCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/STAGE_INSTANCE_DELETE.js b/src/client/websocket/handlers/STAGE_INSTANCE_DELETE.js new file mode 100644 index 000000000..e2bb62750 --- /dev/null +++ b/src/client/websocket/handlers/STAGE_INSTANCE_DELETE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.StageInstanceDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/STAGE_INSTANCE_UPDATE.js b/src/client/websocket/handlers/STAGE_INSTANCE_UPDATE.js new file mode 100644 index 000000000..fabc84a63 --- /dev/null +++ b/src/client/websocket/handlers/STAGE_INSTANCE_UPDATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.StageInstanceUpdate.handle(packet.d); +}; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index f615639e0..1d0ec60b8 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -87,6 +87,7 @@ const Messages = { GUILD_OWNED: 'Guild is owned by the client.', GUILD_MEMBERS_TIMEOUT: "Members didn't arrive in time.", GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.', + STAGE_CHANNEL_RESOLVE: 'Could not resolve channel to a stage channel.', INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, INVALID_ELEMENT: (type, name, elem) => `Supplied ${type} ${name} includes an invalid element: ${elem}`, diff --git a/src/managers/StageInstanceManager.js b/src/managers/StageInstanceManager.js new file mode 100644 index 000000000..a1bdfcd60 --- /dev/null +++ b/src/managers/StageInstanceManager.js @@ -0,0 +1,150 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const { TypeError, Error } = require('../errors'); +const StageInstance = require('../structures/StageInstance'); +const { PrivacyLevels } = require('../util/Constants'); + +/** + * Manages API methods for {@link StageInstance} objects and holds their cache. + * @extends {BaseManager} + */ +class StageInstanceManager extends BaseManager { + constructor(guild, iterable) { + super(guild.client, iterable, StageInstance); + + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this Manager + * @type {Collection} + * @name StageInstanceManager#cache + */ + + /** + * Options used to create a stage instance. + * @typedef {Object} CreateStageInstanceOptions + * @property {StageChannel|Snowflake} channel The stage channel whose instance is to be created + * @property {string} topic The topic of the stage instance + * @property {PrivacyLevel|number} [privacyLevel] The privacy level of the stage instance + */ + + /** + * Creates a new stage instance. + * @param {CreateStageInstanceOptions} options The options to create the stage instance + * @returns {Promise} + * @example + * // Create a stage instance + * guild.stageInstances.create({ + * channel: '1234567890123456789', + * topic: 'A very creative topic', + * privacyLevel: 'GUILD_ONLY' + * }) + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error); + */ + async create(options) { + if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); + let { channel, topic, privacyLevel } = options; + const channelID = this.guild.channels.resolveID(channel); + if (!channelID) throw new Error('STAGE_CHANNEL_RESOLVE'); + + if (privacyLevel) privacyLevel = typeof privacyLevel === 'number' ? privacyLevel : PrivacyLevels[privacyLevel]; + + const data = await this.client.api['stage-instances'].post({ + data: { + channel_id: channelID, + topic, + privacy_level: privacyLevel, + }, + }); + + return this.add(data); + } + + /** + * Fetches the stage instance associated with a stage channel, if it exists. + * @param {StageChannel|Snowflake} channel The stage channel whose instance is to be fetched + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise} + * @example + * // Fetch a stage instance + * guild.stageInstances.fetch('1234567890123456789') + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error); + */ + async fetch(channel, { cache = true, force = false } = {}) { + const channelID = this.guild.channels.resolveID(channel); + if (!channelID) throw new Error('STAGE_CHANNEL_RESOLVE'); + + if (!force) { + const existing = this.cache.find(stageInstance => stageInstance.channelID === channelID); + if (existing) return existing; + } + + const data = await this.client.api('stage-instances', channelID).get(); + return this.add(data, cache); + } + + /** + * Options used to edit an existing stage instance. + * @typedef {Object} StageInstanceEditOptions + * @property {string} [topic] The new topic of the stage instance + * @property {PrivacyLevel|number} [privacyLevel] The new privacy level of the stage instance + */ + + /** + * Edits an existing stage instance. + * @param {StageChannel|Snowflake} channel The stage channel whose instance is to be edited + * @param {StageInstanceEditOptions} options The options to edit the stage instance + * @returns {Promise} + * @example + * // Edit a stage instance + * guild.stageInstances.edit('1234567890123456789', { topic: 'new topic' }) + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error); + */ + async edit(channel, options) { + if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); + const channelID = this.guild.channels.resolveID(channel); + if (!channelID) throw new Error('STAGE_CHANNEL_RESOLVE'); + + let { topic, privacyLevel } = options; + + if (privacyLevel) privacyLevel = typeof privacyLevel === 'number' ? privacyLevel : PrivacyLevels[privacyLevel]; + + const data = await this.client.api('stage-instances', channelID).patch({ + data: { + topic, + privacy_level: privacyLevel, + }, + }); + + if (this.cache.has(data.id)) { + const clone = this.cache.get(data.id)._clone(); + clone._patch(data); + return clone; + } + + return this.add(data); + } + + /** + * Deletes an existing stage instance. + * @param {StageChannel|Snowflake} channel The stage channel whose instance is to be deleted + * @returns {Promise} + */ + async delete(channel) { + const channelID = this.guild.channels.resolveID(channel); + if (!channelID) throw new Error('STAGE_CHANNEL_RESOLVE'); + + await this.client.api('stage-instances', channelID).delete(); + } +} + +module.exports = StageInstanceManager; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 77afa232f..dd6b0138a 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -15,6 +15,7 @@ const GuildEmojiManager = require('../managers/GuildEmojiManager'); const GuildMemberManager = require('../managers/GuildMemberManager'); const PresenceManager = require('../managers/PresenceManager'); const RoleManager = require('../managers/RoleManager'); +const StageInstanceManager = require('../managers/StageInstanceManager'); const VoiceStateManager = require('../managers/VoiceStateManager'); const Collection = require('../util/Collection'); const { @@ -83,6 +84,12 @@ class Guild extends BaseGuild { */ this.voiceStates = new VoiceStateManager(this); + /** + * A manager of the stage instances of this guild + * @type {StageInstanceManager} + */ + this.stageInstances = new StageInstanceManager(this); + /** * Whether the bot has been removed from the guild * @type {boolean} @@ -402,6 +409,13 @@ class Guild extends BaseGuild { } } + if (data.stage_instances) { + this.stageInstances.cache.clear(); + for (const stageInstance of data.stage_instances) { + this.stageInstances.add(stageInstance); + } + } + if (data.voice_states) { this.voiceStates.cache.clear(); for (const voiceState of data.voice_states) { diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 1b32f59e5..afcb68316 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -1,6 +1,7 @@ 'use strict'; const Integration = require('./Integration'); +const StageInstance = require('./StageInstance'); const Webhook = require('./Webhook'); const Collection = require('../util/Collection'); const { OverwriteTypes, PartialTypes } = require('../util/Constants'); @@ -19,6 +20,7 @@ const Util = require('../util/Util'); * * EMOJI * * MESSAGE * * INTEGRATION + * * STAGE_INSTANCE * @typedef {string} AuditLogTargetType */ @@ -38,6 +40,7 @@ const Targets = { EMOJI: 'EMOJI', MESSAGE: 'MESSAGE', INTEGRATION: 'INTEGRATION', + STAGE_INSTANCE: 'STAGE_INSTANCE', UNKNOWN: 'UNKNOWN', }; @@ -79,6 +82,9 @@ const Targets = { * * INTEGRATION_CREATE: 80 * * INTEGRATION_UPDATE: 81 * * INTEGRATION_DELETE: 82 + * * STAGE_INSTANCE_CREATE: 83 + * * STAGE_INSTANCE_UPDATE: 84 + * * STAGE_INSTANCE_DELETE: 85 * @typedef {?(number|string)} AuditLogAction */ @@ -124,6 +130,9 @@ const Actions = { INTEGRATION_CREATE: 80, INTEGRATION_UPDATE: 81, INTEGRATION_DELETE: 82, + STAGE_INSTANCE_CREATE: 83, + STAGE_INSTANCE_UPDATE: 84, + STAGE_INSTANCE_DELETE: 85, }; /** @@ -187,9 +196,11 @@ class GuildAuditLogs { * * An emoji * * A message * * An integration + * * A stage instance * * An object with an id key if target was deleted * * An object where the keys represent either the new value or the old value - * @typedef {?(Object|Guild|Channel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration)} AuditLogEntryTarget + * @typedef {?(Object|Guild|Channel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance)} + * AuditLogEntryTarget */ /** @@ -206,7 +217,8 @@ class GuildAuditLogs { if (target < 60) return Targets.WEBHOOK; if (target < 70) return Targets.EMOJI; if (target < 80) return Targets.MESSAGE; - if (target < 90) return Targets.INTEGRATION; + if (target < 83) return Targets.INTEGRATION; + if (target < 86) return Targets.STAGE_INSTANCE; return Targets.UNKNOWN; } @@ -237,6 +249,7 @@ class GuildAuditLogs { Actions.EMOJI_CREATE, Actions.MESSAGE_PIN, Actions.INTEGRATION_CREATE, + Actions.STAGE_INSTANCE_CREATE, ].includes(action) ) { return 'CREATE'; @@ -258,6 +271,7 @@ class GuildAuditLogs { Actions.MESSAGE_BULK_DELETE, Actions.MESSAGE_UNPIN, Actions.INTEGRATION_DELETE, + Actions.STAGE_INSTANCE_DELETE, ].includes(action) ) { return 'DELETE'; @@ -276,6 +290,7 @@ class GuildAuditLogs { Actions.WEBHOOK_UPDATE, Actions.EMOJI_UPDATE, Actions.INTEGRATION_UPDATE, + Actions.STAGE_INSTANCE_UPDATE, ].includes(action) ) { return 'UPDATE'; @@ -366,7 +381,7 @@ class GuildAuditLogsEntry { case Actions.MESSAGE_DELETE: case Actions.MESSAGE_BULK_DELETE: this.extra = { - channel: guild.channels.cache.get(data.options.channel_id) || { id: data.options.channel_id }, + channel: guild.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id }, count: Number(data.options.count), }; break; @@ -374,7 +389,7 @@ class GuildAuditLogsEntry { case Actions.MESSAGE_PIN: case Actions.MESSAGE_UNPIN: this.extra = { - channel: guild.client.channels.cache.get(data.options.channel_id) || { id: data.options.channel_id }, + channel: guild.client.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id }, messageID: data.options.message_id, }; break; @@ -390,7 +405,7 @@ class GuildAuditLogsEntry { case Actions.CHANNEL_OVERWRITE_DELETE: switch (Number(data.options.type)) { case OverwriteTypes.role: - this.extra = guild.roles.cache.get(data.options.id) || { + this.extra = guild.roles.cache.get(data.options.id) ?? { id: data.options.id, name: data.options.role_name, type: OverwriteTypes[OverwriteTypes.role], @@ -398,7 +413,7 @@ class GuildAuditLogsEntry { break; case OverwriteTypes.member: - this.extra = guild.members.cache.get(data.options.id) || { + this.extra = guild.members.cache.get(data.options.id) ?? { id: data.options.id, type: OverwriteTypes[OverwriteTypes.member], }; @@ -409,6 +424,14 @@ class GuildAuditLogsEntry { } break; + case Actions.STAGE_INSTANCE_CREATE: + case Actions.STAGE_INSTANCE_DELETE: + case Actions.STAGE_INSTANCE_UPDATE: + this.extra = { + channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id }, + }; + break; + default: break; } @@ -492,6 +515,23 @@ class GuildAuditLogsEntry { }, { id: data.target_id }, ); + } else if (targetType === Targets.STAGE_INSTANCE) { + this.target = + guild.stageInstances.cache.get(data.target_id) ?? + new StageInstance( + guild.client, + this.changes.reduce( + (o, c) => { + o[c.key] = c.new || c.old; + return o; + }, + { + id: data.target_id, + channel_id: data.options?.channel_id, + guild_id: guild.id, + }, + ), + ); } else if (data.target_id) { this.target = guild[`${targetType.toLowerCase()}s`]?.cache.get(data.target_id) || { id: data.target_id }; } diff --git a/src/structures/StageChannel.js b/src/structures/StageChannel.js index 7d66625cc..788bf9f40 100644 --- a/src/structures/StageChannel.js +++ b/src/structures/StageChannel.js @@ -19,6 +19,15 @@ class StageChannel extends BaseGuildVoiceChannel { } } + /** + * The instance of this stage channel, if it exists + * @type {?StageInstance} + * @readonly + */ + get instance() { + return this.guild.stageInstances.cache.find(stageInstance => stageInstance.channelID === this.id) ?? null; + } + /** * Sets the RTC region of the channel. * @name StageChannel#setRTCRegion diff --git a/src/structures/StageInstance.js b/src/structures/StageInstance.js new file mode 100644 index 000000000..8ea2c4dfa --- /dev/null +++ b/src/structures/StageInstance.js @@ -0,0 +1,143 @@ +'use strict'; + +const Base = require('./Base'); +const { PrivacyLevels } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * Represents a stage instance. + * @extends {Base} + */ +class StageInstance extends Base { + constructor(client, data) { + super(client); + + /** + * The ID of this stage instance + * @type {Snowflake} + */ + this.id = data.id; + + /** + * Whether the stage instance has been deleted + * @type {boolean} + */ + this.deleted = false; + + this._patch(data); + } + + _patch(data) { + /** + * The guild ID of the associated stage channel + * @type {Snowflake} + */ + this.guildID = data.guild_id; + + /** + * The ID of the associated stage channel + * @type {Snowflake} + */ + this.channelID = data.channel_id; + + /** + * The topic of the stage instance + * @type {string} + */ + this.topic = data.topic; + + /** + * The privacy level of the stage instance + * @type {PrivacyLevel} + */ + this.privacyLevel = PrivacyLevels[data.privacy_level]; + + /** + * Whether or not stage discovery is disabled + * @type {boolean} + */ + this.discoverableDisabled = data.discoverable_disabled; + } + + /** + * The stage channel associated with this instance + * @type {?StageChannel} + * @readonly + */ + get channel() { + return this.client.channels.resolve(this.channelID); + } + + /** + * The guild this stage instance belongs to + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildID); + } + + /** + * Edits this stage instance. + * @param {StageInstanceEditOptions} options The options to edit the stage instance + * @returns {Promise} + * @example + * // Edit a stage instance + * stageInstance.edit({ topic: 'new topic' }) + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error) + */ + edit(options) { + return this.guild.stageInstances.edit(this.channelID, options); + } + + /** + * Deletes this stage instance. + * @returns {Promise} + * @example + * // Delete a stage instance + * stageInstance.delete() + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error); + */ + async delete() { + await this.guild.stageInstances.delete(this.channelID); + const clone = this._clone(); + clone.deleted = true; + return clone; + } + + /** + * Sets the topic of this stage instance. + * @param {string} topic The topic for the stage instance + * @returns {Promise} + * @example + * // Set topic of a stage instance + * stageInstance.setTopic('new topic') + * .then(stageInstance => console.log(`Set the topic to: ${stageInstance.topic}`)) + * .catch(console.error); + */ + setTopic(topic) { + return this.guild.stageInstances.edit(this.channelID, { topic }); + } + + /** + * The timestamp this stage instances was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.deconstruct(this.id).timestamp; + } + + /** + * The time this stage instance was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } +} + +module.exports = StageInstance; diff --git a/src/util/Constants.js b/src/util/Constants.js index 80bd53e94..72f5942a5 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -279,6 +279,9 @@ exports.Events = { SHARD_RESUME: 'shardResume', INVALIDATED: 'invalidated', RAW: 'raw', + STAGE_INSTANCE_CREATE: 'stageInstanceCreate', + STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate', + STAGE_INSTANCE_DELETE: 'stageInstanceDelete', }; exports.ShardEvents = { @@ -345,6 +348,9 @@ exports.PartialTypes = keyMirror(['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', * * VOICE_SERVER_UPDATE * * WEBHOOKS_UPDATE * * INTERACTION_CREATE + * * STAGE_INSTANCE_CREATE + * * STAGE_INSTANCE_UPDATE + * * STAGE_INSTANCE_DELETE * @typedef {string} WSEventType */ exports.WSEvents = keyMirror([ @@ -388,6 +394,9 @@ exports.WSEvents = keyMirror([ 'VOICE_SERVER_UPDATE', 'WEBHOOKS_UPDATE', 'INTERACTION_CREATE', + 'STAGE_INSTANCE_CREATE', + 'STAGE_INSTANCE_UPDATE', + 'STAGE_INSTANCE_DELETE', ]); /** @@ -849,7 +858,7 @@ exports.MessageButtonStyles = createEnum([null, 'PRIMARY', 'SECONDARY', 'SUCCESS exports.MFALevels = createEnum(['NONE', 'ELEVATED']); /** - * NSFW level of a Guild + * NSFW level of a Guild: * * DEFAULT * * EXPLICIT * * SAFE @@ -858,6 +867,14 @@ exports.MFALevels = createEnum(['NONE', 'ELEVATED']); */ exports.NSFWLevels = createEnum(['DEFAULT', 'EXPLICIT', 'SAFE', 'AGE_RESTRICTED']); +/** + * Privacy level of a {@link StageInstance} object: + * * PUBLIC + * * GUILD_ONLY + * @typedef {string} PrivacyLevel + */ +exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']); + function keyMirror(arr) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; diff --git a/src/util/Structures.js b/src/util/Structures.js index bee9d6373..a8db67180 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -21,6 +21,7 @@ * * **`User`** * * **`CommandInteraction`** * * **`ButtonInteraction`** + * * **`StageInstance`** * @typedef {string} ExtendableStructure */ @@ -113,6 +114,7 @@ const structures = { User: require('../structures/User'), CommandInteraction: require('../structures/CommandInteraction'), ButtonInteraction: require('../structures/ButtonInteraction'), + StageInstance: require('../structures/StageInstance'), }; module.exports = Structures; diff --git a/typings/index.d.ts b/typings/index.d.ts index fe593b717..5aa6b15d4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -70,6 +70,11 @@ declare enum OverwriteTypes { member = 1, } +declare enum PrivacyLevels { + PUBLIC = 1, + GUILD_ONLY = 2, +} + declare enum StickerFormatTypes { PNG = 1, APNG = 2, @@ -576,6 +581,9 @@ declare module 'discord.js' { SHARD_RESUME: 'shardResume'; INVALIDATED: 'invalidated'; RAW: 'raw'; + STAGE_INSTANCE_CREATE: 'stageInstanceCreate'; + STAGE_INSTANCE_UPDATE: 'stageInstanceUpdate'; + STAGE_INSTANCE_DELETE: 'stageInstanceDelete'; }; ShardEvents: { CLOSE: 'close'; @@ -668,6 +676,7 @@ declare module 'discord.js' { MessageButtonStyles: typeof MessageButtonStyles; MFALevels: typeof MFALevels; NSFWLevels: typeof NSFWLevels; + PrivacyLevels: typeof PrivacyLevels; }; export class DataResolver { @@ -759,6 +768,7 @@ declare module 'discord.js' { public readonly shard: WebSocketShard; public shardID: number; public splash: string | null; + public stageInstances: StageInstanceManager; public readonly systemChannel: TextChannel | null; public systemChannelFlags: Readonly; public systemChannelID: Snowflake | null; @@ -855,6 +865,7 @@ declare module 'discord.js' { | Webhook | Message | Integration + | StageInstance | { id: Snowflake } | null; public targetType: GuildAuditLogsTarget; @@ -1683,6 +1694,25 @@ declare module 'discord.js' { export class StageChannel extends BaseGuildVoiceChannel { public topic: string | null; public type: 'stage'; + public readonly instance: StageInstance | null; + } + + export class StageInstance extends Base { + constructor(client: Client, data: unknown, channel: StageChannel); + public id: Snowflake; + public deleted: boolean; + public guildID: Snowflake; + public channelID: Snowflake; + public topic: string; + public privacyLevel: PrivacyLevel; + public discoverableDisabled: boolean; + public readonly channel: StageChannel | null; + public readonly guild: Guild | null; + public edit(options: StageInstanceEditOptions): Promise; + public delete(): Promise; + public setTopic(topic: string): Promise; + public readonly createdTimestamp: number; + public readonly createdAt: Date; } export class StoreChannel extends GuildChannel { @@ -2265,6 +2295,15 @@ declare module 'discord.js' { public create(options?: CreateRoleOptions): Promise; } + export class StageInstanceManager extends BaseManager { + constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; + public create(options: CreateStageInstanceOptions): Promise; + public fetch(channel: StageChannel | Snowflake, options?: BaseFetchOptions): Promise; + public edit(channel: StageChannel | Snowflake, options: StageInstanceEditOptions): Promise; + public delete(channel: StageChannel | Snowflake): Promise; + } + export class UserManager extends BaseManager { constructor(client: Client, iterable?: Iterable); public fetch(id: Snowflake, options?: BaseFetchOptions): Promise; @@ -2647,6 +2686,9 @@ declare module 'discord.js' { shardReady: [shardID: number, unavailableGuilds: Set | undefined]; shardReconnecting: [shardID: number]; shardResume: [shardID: number, replayedEvents: number]; + stageInstanceCreate: [stageInstance: StageInstance]; + stageInstanceUpdate: [oldStageInstance: StageInstance | null, newStageInstance: StageInstance]; + stageInstanceDelete: [stageInstance: StageInstance]; } interface ClientOptions { @@ -2755,6 +2797,12 @@ declare module 'discord.js' { reason?: string; } + interface CreateStageInstanceOptions { + channel: StageChannel | Snowflake; + topic: string; + privacyLevel?: PrivacyLevel | number; + } + interface CrosspostedChannel { channelID: Snowflake; guildID: Snowflake; @@ -2927,6 +2975,9 @@ declare module 'discord.js' { INTEGRATION_CREATE?: number; INTEGRATION_UPDATE?: number; INTEGRATION_DELETE?: number; + STAGE_INSTANCE_CREATE?: number; + STAGE_INSTANCE_UPDATE?: number; + STAGE_INSTANCE_DELETE?: number; } type GuildAuditLogsActionType = 'CREATE' | 'DELETE' | 'UPDATE' | 'ALL'; @@ -2951,6 +3002,7 @@ declare module 'discord.js' { EMOJI?: string; MESSAGE?: string; INTEGRATION?: string; + STAGE_INSTANCE?: string; UNKNOWN?: string; } @@ -3611,6 +3663,8 @@ declare module 'discord.js' { type PresenceStatus = PresenceStatusData | 'offline'; + type PrivacyLevel = keyof typeof PrivacyLevels; + interface RateLimitData { timeout: number; limit: number; @@ -3708,6 +3762,8 @@ declare module 'discord.js' { size?: ImageSize; } + type StageInstanceResolvable = StageInstance | Snowflake; + type Status = number; export class Sticker extends Base { @@ -3741,6 +3797,11 @@ declare module 'discord.js' { timeout: NodeJS.Timeout; } + interface StageInstanceEditOptions { + topic?: string; + privacyLevel?: PrivacyLevel | number; + } + type UserFlagsString = | 'DISCORD_EMPLOYEE' | 'PARTNERED_SERVER_OWNER' @@ -3840,7 +3901,10 @@ declare module 'discord.js' { | 'VOICE_STATE_UPDATE' | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE' - | 'INTERACTION_CREATE'; + | 'INTERACTION_CREATE' + | 'STAGE_INSTANCE_CREATE' + | 'STAGE_INSTANCE_UPDATE' + | 'STAGE_INSTANCE_DELETE'; //#endregion }