From 801c3a545fd1c1355aef840409dad6847897c205 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Wed, 26 May 2021 07:38:47 +1000 Subject: [PATCH 01/55] chore: new branch without broken rebasing --- .../websocket/handlers/INTERACTION_CREATE.js | 35 ++- src/index.js | 3 + src/structures/APIMessage.js | 12 +- src/structures/BaseMessageComponent.js | 72 ++++++ src/structures/ButtonInteraction.js | 233 ++++++++++++++++++ src/structures/ButtonInteractionCollector.js | 145 +++++++++++ src/structures/Interaction.js | 8 + src/structures/Message.js | 55 +++++ src/structures/MessageActionRow.js | 55 +++++ src/structures/MessageButton.js | 129 ++++++++++ src/util/Constants.js | 26 +- src/util/Structures.js | 2 + typings/index.d.ts | 105 +++++++- 13 files changed, 863 insertions(+), 17 deletions(-) create mode 100644 src/structures/BaseMessageComponent.js create mode 100644 src/structures/ButtonInteraction.js create mode 100644 src/structures/ButtonInteractionCollector.js create mode 100644 src/structures/MessageActionRow.js create mode 100644 src/structures/MessageButton.js diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js index cef671b97d88..37de3e1b9a65 100644 --- a/src/client/websocket/handlers/INTERACTION_CREATE.js +++ b/src/client/websocket/handlers/INTERACTION_CREATE.js @@ -4,20 +4,31 @@ const { Events, InteractionTypes } = require('../../../util/Constants'); let Structures; module.exports = (client, { d: data }) => { - if (data.type === InteractionTypes.APPLICATION_COMMAND) { - if (!Structures) Structures = require('../../../util/Structures'); - const CommandInteraction = Structures.get('CommandInteraction'); + let interaction; + switch (data.type) { + case InteractionTypes.APPLICATION_COMMAND: { + if (!Structures) Structures = require('../../../util/Structures'); + const CommandInteraction = Structures.get('CommandInteraction'); - const interaction = new CommandInteraction(client, data); + interaction = new CommandInteraction(client, data); + break; + } + case InteractionTypes.MESSAGE_COMPONENT: { + if (!Structures) Structures = require('../../../util/Structures'); + const ButtonInteraction = Structures.get('ButtonInteraction'); - /** - * Emitted when an interaction is created. - * @event Client#interaction - * @param {Interaction} interaction The interaction which was created - */ - client.emit(Events.INTERACTION_CREATE, interaction); - return; + interaction = new ButtonInteraction(client, data); + break; + } + default: + client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`); + return; } - client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`); + /** + * Emitted when an interaction is created. + * @event Client#interaction + * @param {Interaction} interaction The interaction which was created + */ + client.emit(Events.INTERACTION_CREATE, interaction); }; diff --git a/src/index.js b/src/index.js index 4d224c2e578c..59308276e0ae 100644 --- a/src/index.js +++ b/src/index.js @@ -68,6 +68,7 @@ module.exports = { BaseGuild: require('./structures/BaseGuild'), BaseGuildEmoji: require('./structures/BaseGuildEmoji'), BaseGuildVoiceChannel: require('./structures/BaseGuildVoiceChannel'), + BaseMessageComponent: require('./structures/BaseMessageComponent'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), ClientApplication: require('./structures/ClientApplication'), @@ -92,7 +93,9 @@ module.exports = { Interaction: require('./structures/Interaction'), Invite: require('./structures/Invite'), Message: require('./structures/Message'), + MessageActionRow: require('./structures/MessageActionRow'), MessageAttachment: require('./structures/MessageAttachment'), + MessageButton: require('./structures/MessageButton'), MessageCollector: require('./structures/MessageCollector'), MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 094195353583..bcb5916a2ddd 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -1,5 +1,6 @@ 'use strict'; +const BaseMessageComponent = require('./BaseMessageComponent'); const MessageAttachment = require('./MessageAttachment'); const MessageEmbed = require('./MessageEmbed'); const { RangeError } = require('../errors'); @@ -151,6 +152,12 @@ class APIMessage { } const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON()); + let components; + if (this.options.components) { + components = []; + components.push(...this.options.components.map(BaseMessageComponent.transform)); + } + let username; let avatarURL; if (isWebhook) { @@ -194,8 +201,9 @@ class APIMessage { content, tts, nonce, - embed: !isWebhookLike ? (this.options.embed === null ? null : embeds[0]) : undefined, - embeds: isWebhookLike ? embeds : undefined, + embed: this.options.embed === null ? null : embeds[0], + embeds, + components, username, avatar_url: avatarURL, allowed_mentions: diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js new file mode 100644 index 000000000000..d847fdf9e9e4 --- /dev/null +++ b/src/structures/BaseMessageComponent.js @@ -0,0 +1,72 @@ +'use strict'; + +const { MessageComponentTypes, MessageButtonStyles } = require('../util/Constants'); + +/** + * Represents an interactive component of a Message. It should not be necessary to construct this directly. + */ +class BaseMessageComponent { + /** + * @typedef {Object} BaseMessageComponentOptions + * @property {MessageComponentType} type The type of this component + */ + + /** + * @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component + */ + constructor(data) { + /** + * The type of this component + * @type {?MessageComponentType} + */ + this.type = 'type' in data ? data.type : null; + } + + /** + * Sets the type of this component; + * @param {MessageComponentType} type The type of this component + * @returns {BaseMessageComponent} + */ + setType(type) { + this.type = type; + return this; + } + + static create(data) { + let component; + let type = data.type; + + if (typeof type === 'string') type = MessageComponentTypes[type]; + + switch (type) { + case MessageComponentTypes.ACTION_ROW: { + const MessageActionRow = require('./MessageActionRow'); + component = new MessageActionRow(data); + break; + } + case MessageComponentTypes.BUTTON: { + const MessageButton = require('./MessageButton'); + component = new MessageButton(data); + break; + } + } + return component; + } + + static transform(component) { + const { type, components, label, customID, style, emoji, url, disabled } = component; + + return { + components: components?.map(BaseMessageComponent.transform), + custom_id: customID, + disabled, + emoji, + label, + style: MessageButtonStyles[style], + type: MessageComponentTypes[type], + url, + }; + } +} + +module.exports = BaseMessageComponent; diff --git a/src/structures/ButtonInteraction.js b/src/structures/ButtonInteraction.js new file mode 100644 index 000000000000..76a7db0238ec --- /dev/null +++ b/src/structures/ButtonInteraction.js @@ -0,0 +1,233 @@ +'use strict'; + +const APIMessage = require('./APIMessage'); +const Interaction = require('./Interaction'); +const WebhookClient = require('../client/WebhookClient'); +const { Error } = require('../errors'); +const { InteractionResponseTypes } = require('../util/Constants'); +const MessageFlags = require('../util/MessageFlags'); + +/** + * Represents a message button interaction. + * @extends {Interaction} + */ +class ButtonInteraction extends Interaction { + // eslint-disable-next-line no-useless-constructor + constructor(client, data) { + super(client, data); + + /** + * The message to which the button was attached + * @type {?Message|Object} + */ + this.message = data.message ? this.channel?.messages.add(data.message) ?? data.message : null; + + /** + * The custom ID of the button which was clicked + * @type {string} + */ + this.customID = data.data.custom_id; + + /** + * Whether the reply to this interaction has been deferred + * @type {boolean} + */ + this.deferred = false; + + /** + * Whether this interaction has already been replied to + * @type {boolean} + */ + this.replied = false; + + /** + * An associated webhook client, can be used to create deferred replies + * @type {WebhookClient} + */ + this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options); + } + + /** + * Defers the reply to this interaction. + * @param {boolean} [ephemeral] Whether the reply should be ephemeral + * @returns {Promise} + * @example + * // Defer the reply to this interaction + * interaction.defer() + * .then(console.log) + * .catch(console.error) + * @example + * // Defer to send an ephemeral reply later + * interaction.defer(true) + * .then(console.log) + * .catch(console.error); + */ + async defer(ephemeral) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, + data: { + flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined, + }, + }, + }); + this.deferred = true; + } + + /** + * Options for a reply to an interaction. + * @typedef {WebhookMessageOptions} InteractionReplyOptions + * @property {boolean} [ephemeral] Whether the reply should be ephemeral + */ + + /** + * Creates a reply to this interaction. + * @param {string|APIMessage|MessageAdditions} content The content for the reply + * @param {InteractionReplyOptions} [options] Additional options for the reply + * @returns {Promise} + * @example + * // Reply to the interaction with an embed + * const embed = new MessageEmbed().setDescription('Pong!'); + * + * interaction.reply(embed) + * .then(console.log) + * .catch(console.error); + * @example + * // Create an ephemeral reply + * interaction.reply('Pong!', { ephemeral: true }) + * .then(console.log) + * .catch(console.error); + */ + async reply(content, options) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); + const { data, files } = await apiMessage.resolveData().resolveFiles(); + + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE, + data, + }, + files, + }); + this.replied = true; + } + + /** + * Defers an update to the message to which the button was attached + * @returns {Promise} + * @example + * // Defer to update the button to a loading state + * interaction.defer() + * .then(console.log) + * .catch(console.error); + */ + async deferUpdate() { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.DEFERRED_MESSAGE_UPDATE, + }, + }); + this.deferred = true; + } + + /** + * Updates the message to which the button was attached + * @param {string|APIMessage|MessageAdditions} content The content for the reply + * @param {WebhookEditMessageOptions} [options] Additional options for the reply + * @returns {Promise} + * @example + * // Remove the buttons from the message * + * interaction.reply("A button was clicked", { components: [] }) + * .then(console.log) + * .catch(console.error); + */ + async update(content, options) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); + const { data, files } = await apiMessage.resolveData().resolveFiles(); + + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.UPDATE_MESSAGE, + data, + }, + files, + }); + this.replied = true; + } + + /** + * Fetches the initial reply to this interaction. + * * For `defer` and `reply` this is the new message + * * For `deferUpdate` and `update` this is the message to which the buttons are attached + * @see Webhook#fetchMessage + * @returns {Promise} + * @example + * // Fetch the reply to this interaction + * interaction.fetchReply() + * .then(reply => console.log(`Replied with ${reply.content}`)) + * .catch(console.error); + */ + async fetchReply() { + const raw = await this.webhook.fetchMessage('@original'); + return this.channel?.messages.add(raw) ?? raw; + } + + /** + * Edits the initial reply to this interaction. + * * For `defer` and `reply` this is the new message sent + * * For `deferUpdate` and `update` this is the message to which the buttons are attached + * @see Webhook#editMessage + * @param {string|APIMessage|MessageEmbed|MessageEmbed[]} content The new content for the message + * @param {WebhookEditMessageOptions} [options] The options to provide + * @returns {Promise} + * @example + * // Edit the reply to this interaction + * interaction.editReply('New content') + * .then(console.log) + * .catch(console.error); + */ + async editReply(content, options) { + const raw = await this.webhook.editMessage('@original', content, options); + return this.channel?.messages.add(raw) ?? raw; + } + + /** + * Deletes the initial reply to this interaction. + * * For `defer` and `reply` this is the new message + * * For `deferUpdate` and `update` this is the message to which the buttons are attached + * @see Webhook#deleteMessage + * @returns {Promise} + * @example + * // Delete the reply to this interaction + * interaction.deleteReply() + * .then(console.log) + * .catch(console.error); + */ + async deleteReply() { + await this.webhook.deleteMessage('@original'); + } + + /** + * Send a follow-up message to this interaction. + * @param {string|APIMessage|MessageAdditions} content The content for the reply + * @param {InteractionReplyOptions} [options] Additional options for the reply + * @returns {Promise} + */ + async followUp(content, options) { + const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); + const { data, files } = await apiMessage.resolveData().resolveFiles(); + + const raw = await this.client.api.webhooks(this.applicationID, this.token).post({ + data, + files, + }); + + return this.channel?.messages.add(raw) ?? raw; + } +} + +module.exports = ButtonInteraction; diff --git a/src/structures/ButtonInteractionCollector.js b/src/structures/ButtonInteractionCollector.js new file mode 100644 index 000000000000..be13d87d6cd1 --- /dev/null +++ b/src/structures/ButtonInteractionCollector.js @@ -0,0 +1,145 @@ +'use strict'; + +const Collector = require('./interfaces/Collector'); +const Collection = require('../util/Collection'); +const { Events } = require('../util/Constants'); + +/** + * @typedef {CollectorOptions} ButtonInteractionCollectorOptions + * @property {number} max The maximum total amount of interactions to collect + * @property {number} maxButtons The maximum number of buttons to collect + * @property {number} maxUsers The maximum number of users to interact + */ + +/** + * Collects interaction on message buttons. + * Will automatically stop if the message (`'messageDelete'`), + * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. + * @extends {Collector} + */ +class ButtonInteractionCollector extends Collector { + /** + * @param {Message} message The message upon which to collect button interactions + * @param {CollectorFilter} filter The filter to apply to this collector + * @param {ButtonInteractionCollectorOptions} [options={}] The options to apply to this collector + */ + constructor(message, filter, options = {}) { + super(message.client, filter, options); + + /** + * The message upon which to collect button interactions + * @type {Message} + */ + this.message = message; + + /** + * The users which have interacted to buttons on this message + * @type {Collection} + */ + this.users = new Collection(); + + /** + * The total number of interactions collected + * @type {number} + */ + this.total = 0; + + this.empty = this.empty.bind(this); + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + this._handleMessageDeletion = this._handleMessageDeletion.bind(this); + + this.client.incrementMaxListeners(); + this.client.on(Events.INTERACTION_CREATE, this.handleCollect); + this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); + + this.once('end', () => { + this.client.removeListener(Events.INTERACTION_CREATE, this.handleCollect); + this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); + this.client.decrementMaxListeners(); + }); + + this.on('collect', interaction => { + this.total++; + this.users.set(interaction.user.id, interaction.user); + }); + } + + /** + * Handles an incoming interaction for possible collection. + * @param {Interaction} interaction The interaction to possibly collect + * @returns {?Snowflake|string} + * @private + */ + collect(interaction) { + /** + * Emitted whenever a reaction is collected. + * @event ButtonInteractionCollector#collect + * @param {ButtonInteraction} interaction The reaction that was collected + */ + if (!interaction.isButton()) return null; + + if (interaction.message.id !== this.message.id) return null; + + return interaction.id; + } + + /** + * Empties this reaction collector. + */ + empty() { + this.total = 0; + this.collected.clear(); + this.users.clear(); + this.checkEnd(); + } + + get endReason() { + if (this.options.max && this.total >= this.options.max) return 'limit'; + if (this.options.maxButtons && this.collected.size >= this.options.maxButtons) return 'buttonLimit'; + if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; + return null; + } + + /** + * Handles checking if the message has been deleted, and if so, stops the collector with the reason 'messageDelete'. + * @private + * @param {Message} message The message that was deleted + * @returns {void} + */ + _handleMessageDeletion(message) { + if (message.id === this.message.id) { + this.stop('messageDelete'); + } + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.message.channel.id) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (this.message.guild && guild.id === this.message.guild.id) { + this.stop('guildDelete'); + } + } +} + +module.exports = ButtonInteractionCollector; diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 87a8c3ddfcf5..771c36d9070e 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -112,6 +112,14 @@ class Interaction extends Base { isCommand() { return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND; } + + /** + * Indicates whether this interaction is a button interacion. + * @returns {boolean} + */ + isButton() { + return InteractionTypes[this.type] === InteractionTypes.BUTTON; + } } module.exports = Interaction; diff --git a/src/structures/Message.js b/src/structures/Message.js index a40fb9242975..199c94e2d067 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -2,6 +2,8 @@ const APIMessage = require('./APIMessage'); const Base = require('./Base'); +const BaseMessageComponent = require('./BaseMessageComponent'); +const ButtonInteractionCollector = require('./ButtonInteractionCollector'); const ClientApplication = require('./ClientApplication'); const MessageAttachment = require('./MessageAttachment'); const Embed = require('./MessageEmbed'); @@ -123,6 +125,12 @@ class Message extends Base { */ this.embeds = (data.embeds || []).map(e => new Embed(e, true)); + /** + * A list of component in the message e.g. ActionRows, Buttons + * @type {MessageComponent[]} + */ + this.components = (data.components || []).map(BaseMessageComponent.create); + /** * A collection of attachments in the message - e.g. Pictures - mapped by their ID * @type {Collection} @@ -282,6 +290,8 @@ class Message extends Base { if ('tts' in data) this.tts = data.tts; if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(e, true)); else this.embeds = this.embeds.slice(); + if ('components' in data) this.components = data.components.map(BaseMessageComponent.create); + else this.components = this.components.slice(); if ('attachments' in data) { this.attachments = new Collection(); @@ -407,6 +417,51 @@ class Message extends Base { }); } + /** + * Creates a button interaction collector. + * @param {CollectorFilter} filter The filter to apply + * @param {ButtonInteractionCollectorOptions} [options={}] Options to send to the collector + * @returns {ButtonInteractionCollector} + * @example + * // Create a button interaction collector + * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; + * const collector = message.createButtonInteractionCollector(filter, { time: 15000 }); + * collector.on('collect', i => console.log(`Collected ${i.customID}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createButtonInteractionCollector(filter, options = {}) { + return new ButtonInteractionCollector(this, filter, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {ButtonInteractionCollectorOptions} AwaitButtonInteractionsOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createButtonInteractionCollector but in promise form. + * Resolves with a collection of interactions that pass the specified filter. + * @param {CollectorFilter} filter The filter function to use + * @param {AwaitButtonInteractionsOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise>} + * @example + * // Create a button interaction collector + * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; + * message.awaitButtonInteraction(filter, { time: 15000 }) + * .then(collected => console.log(`Collected ${collected.size} interactions`)) + * .catch(console.error); + */ + awaitButtonInteractions(filter, options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createButtonInteractionCollector(filter, options); + collector.once('end', (interactions, reason) => { + if (options.errors && options.errors.includes(reason)) reject(interactions); + else resolve(interactions); + }); + }); + } + /** * Whether the message is editable by the client user * @type {boolean} diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js new file mode 100644 index 000000000000..5c3de0b86ca0 --- /dev/null +++ b/src/structures/MessageActionRow.js @@ -0,0 +1,55 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); + +/** + * Represents an ActionRow containing message components. + */ +class MessageActionRow extends BaseMessageComponent { + /** + * @typedef {BaseMessageComponentOptions|MessageActionRowOptions|MessageButtonOptions} MessageComponentOptions + */ + + /** + * @typedef {BaseMessageComponent|MessageActionRow|MessageButton} MessageComponent + */ + + /** + * @typedef {MessageComponentOptions|MessageComponent} MessageComponentResolvable + */ + + /** + * @typedef {Object} MessageActionRowOptions + * @property {MessageComponent[]|MessageComponentOptions[]} [components] The components to place in this ActionRow + */ + + /** + * @param {MessageActionRow|MessageActionRowOptions} [data={}] MessageActionRow to clone or raw data + */ + constructor(data = {}) { + super({ type: 'ACTION_ROW' }); + + this.components = (data.components ?? []).map(BaseMessageComponent.create); + } + + /** + * Adds a component to the row (max 5). + * @param {MessageComponent|MessageComponentOptions} component The component to add + * @returns {MessageEmbed} + */ + addComponent(component) { + return this.addComponents({ ...component }); + } + + /** + * Adds components to the row (max 5). + * @param {...(MessageComponent[]|MessageComponentOptions[])} components The components to add + * @returns {MessageEmbed} + */ + addComponents(...components) { + this.components.push(...components.map(BaseMessageComponent.create)); + return this; + } +} + +module.exports = MessageActionRow; diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js new file mode 100644 index 000000000000..7bcc39cf9415 --- /dev/null +++ b/src/structures/MessageButton.js @@ -0,0 +1,129 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const { MessageButtonStyles } = require('../util/Constants.js'); +const Util = require('../util/Util'); +class MessageButton extends BaseMessageComponent { + /** + * @typedef {Object} MessageButtonOptions + * @property {string} [label] The text to be displayed on this button + * @property {string} [customID] A unique string to be sent in the interaction when clicked + * @property {MessageButtonStyle} [style] The style of this button + * @property {Emoji} [emoji] The emoji to be displayed to the left of the text + * @property {string} [url] Optional URL for link-style buttons + * @property {boolean} [disabled=false] Disables the button to prevent interactions + */ + + /** + * @param {MessageButton|MessageButtonOptions} [data={}] MessageButton to clone or raw data + */ + constructor(data = {}) { + super({ type: 'BUTTON' }); + + this.setup(data); + } + + setup(data) { + /** + * The text to be displayed on this button + * @type {?string} + */ + this.label = data.label ?? null; + + /** + * A unique string to be sent in the interaction when clicked + * @type {?string} + */ + this.customID = data.custom_id ?? data.customID ?? null; + + /** + * The style of this button + * @type {MessageButtonStyle} + */ + this.style = data.style ? MessageButton.resolveStyle(data.style) : null; + + /** + * Emoji for this button + * @type {Emoji|string} + */ + this.emoji = data.emoji ?? null; + + /** + * The URL this button links to, if it is a Link style button + * @type {?string} + */ + this.url = data.url ?? null; + + /** + * Whether this button is currently disabled + * @type {?boolean} + */ + this.disabled = data.disabled ?? false; + } + + /** + * Sets the custom ID of this button + * @param {string} customID A unique string to be sent in the interaction when clicked + * @returns {MessageButton} + */ + setCustomID(customID) { + this.customID = Util.resolveString(customID); + return this; + } + + /** + * Sets the interactive status of the button + * @param {boolean} disabled Whether this emoji should be disabled + * @returns {MessageEmbed} + */ + setDisabled(disabled) { + this.disabled = disabled; + return this; + } + + /** + * Set the emoji of this button + * @param {EmojiIdentifierResolvable} emoji The emoji to be displayed on this button + * @returns {MessageButton} + */ + setEmoji(emoji) { + this.emoji = typeof emoji === 'string' ? { name: emoji } : emoji; + return this; + } + + /** + * Sets the label of this button + * @param {string} label The text to be displayed on this button + * @returns {MessageButton} + */ + setLabel(label) { + this.label = Util.resolveString(label); + return this; + } + + /** + * Sets the style of this button + * @param {MessageButtonStyle} style The style of this button + * @returns {MessageButton} + */ + setStyle(style) { + this.style = MessageButton.resolveStyle(style); + return this; + } + + /** + * Sets the URL of this button. MessageButton#style should be LINK + * @param {string} url The URL of this button + * @returns {MessageButton} + */ + setURL(url) { + this.url = Util.resolveString(url); + return this; + } + + static resolveStyle(style) { + return typeof style === 'string' ? style : MessageButtonStyles[style]; + } +} + +module.exports = MessageButton; diff --git a/src/util/Constants.js b/src/util/Constants.js index 7b1d9401665d..a97dba1391b8 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -799,15 +799,18 @@ exports.ApplicationCommandPermissionTypes = createEnum([null, 'ROLE', 'USER']); * The type of an {@link Interaction} object: * * PING * * APPLICATION_COMMAND + * * MESSAGE_COMPONENT * @typedef {string} InteractionType */ -exports.InteractionTypes = createEnum([null, 'PING', 'APPLICATION_COMMAND']); +exports.InteractionTypes = createEnum([null, 'PING', 'APPLICATION_COMMAND', 'MESSAGE_COMPONENT']); /** * The type of an interaction response: * * PONG * * CHANNEL_MESSAGE_WITH_SOURCE * * DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE + * * DEFERRED_MESSAGE_UPDATE + * * UPDATE_MESSAGE * @typedef {string} InteractionResponseType */ exports.InteractionResponseTypes = createEnum([ @@ -817,8 +820,29 @@ exports.InteractionResponseTypes = createEnum([ null, 'CHANNEL_MESSAGE_WITH_SOURCE', 'DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE', + 'DEFERRED_MESSAGE_UPDATE', + 'UPDATE_MESSAGE', ]); +/** + * The type of a message component + * ACTION_ROW + * BUTTON + * @typedef {string} MessageComponentType + */ +exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON']); + +/** + * The style of a message button + * PRIMARY + * SECONDARY + * SUCCESS + * DANGER + * LINK + * @typedef {string} MessageButtonStyle + */ +exports.MessageButtonStyles = createEnum([null, 'PRIMARY', 'SECONDARY', 'SUCCESS', 'DANGER', 'LINK']); + /** * NSFW level of a Guild * * DEFAULT diff --git a/src/util/Structures.js b/src/util/Structures.js index 1fcac228313d..bee9d6373cc2 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -20,6 +20,7 @@ * * **`Role`** * * **`User`** * * **`CommandInteraction`** + * * **`ButtonInteraction`** * @typedef {string} ExtendableStructure */ @@ -111,6 +112,7 @@ const structures = { Role: require('../structures/Role'), User: require('../structures/User'), CommandInteraction: require('../structures/CommandInteraction'), + ButtonInteraction: require('../structures/ButtonInteraction'), }; module.exports = Structures; diff --git a/typings/index.d.ts b/typings/index.d.ts index 668c446475db..293ac5efabcf 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -25,11 +25,14 @@ declare enum InteractionResponseTypes { PONG = 1, CHANNEL_MESSAGE_WITH_SOURCE = 4, DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5, + DEFERRED_MESSAGE_UPDATE = 6, + UPDATE_MESSAGE = 7, } declare enum InteractionTypes { PING = 1, APPLICATION_COMMAND = 2, + MESSAGE_COMPONENT = 3, } declare enum InviteTargetType { @@ -37,6 +40,19 @@ declare enum InviteTargetType { EMBEDDED_APPLICATION = 2, } +declare enum MessageButtonStyles { + PRIMARY = 1, + SECONDARY = 2, + SUCCESS = 3, + DANGER = 4, + LINK = 5, +} + +declare enum MessageComponentTypes { + ACTION_ROW = 1, + BUTTON = 2, +} + declare enum NSFWLevels { DEFAULT = 0, EXPLICIT = 1, @@ -61,15 +77,16 @@ declare module 'discord.js' { import BaseCollection from '@discordjs/collection'; import { ChildProcess } from 'child_process'; import { - ApplicationCommandOptionType as ApplicationCommandOptionTypes, - ApplicationCommandPermissionType as ApplicationCommandPermissionTypes, APIInteractionDataResolvedChannel as RawInteractionDataResolvedChannel, APIInteractionDataResolvedGuildMember as RawInteractionDataResolvedGuildMember, APIInteractionGuildMember as RawInteractionGuildMember, APIMessage as RawMessage, APIOverwrite as RawOverwrite, + APIPartialEmoji as RawEmoji, APIRole as RawRole, Snowflake as APISnowflake, + ApplicationCommandOptionType as ApplicationCommandOptionTypes, + ApplicationCommandPermissionType as ApplicationCommandPermissionTypes, } from 'discord-api-types/v8'; import { EventEmitter } from 'events'; import { PathLike } from 'fs'; @@ -269,6 +286,16 @@ declare module 'discord.js' { public setRTCRegion(region: string | null): Promise; } + export class BaseMessageComponent { + constructor(data?: BaseMessageComponent | BaseMessageComponentOptions); + public type: MessageComponentType | null; + public setType(type: MessageComponentType | number): this; + } + + interface BaseMessageComponentOptions { + type?: MessageComponentType; + } + class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { public broadcast: VoiceBroadcast; } @@ -292,6 +319,27 @@ declare module 'discord.js' { public static resolve(bit?: BitFieldResolvable): number | bigint; } + export class ButtonInteraction extends Interaction { + public customID: string; + public deferred: boolean; + public message: Message | RawMessage; + public replied: boolean; + public webhook: WebhookClient; + public defer(ephemeral?: boolean): Promise; + public deleteReply(): Promise; + public editReply( + content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], + ): Promise; + public editReply(content: string, options?: WebhookEditMessageOptions): Promise; + public fetchReply(): Promise; + public followUp( + content: string | APIMessage | InteractionReplyOptions | MessageAdditions, + ): Promise; + public followUp(content: string, options?: InteractionReplyOptions): Promise; + public reply(content: string | APIMessage | InteractionReplyOptions | MessageAdditions): Promise; + public reply(content: string, options?: InteractionReplyOptions): Promise; + } + export class CategoryChannel extends GuildChannel { public readonly children: Collection; public type: 'category'; @@ -1110,6 +1158,7 @@ declare module 'discord.js' { public user: User; public version: number; public isCommand(): this is CommandInteraction; + public isButton(): this is ButtonInteraction; } export class Invite extends Base { @@ -1149,6 +1198,7 @@ declare module 'discord.js' { public author: User; public channel: TextChannel | DMChannel | NewsChannel; public readonly cleanContent: string; + public components: MessageComponent[]; public content: string; public readonly createdAt: Date; public createdTimestamp: number; @@ -1221,6 +1271,14 @@ declare module 'discord.js' { public unpin(): Promise; } + export class MessageActionRow extends BaseMessageComponent { + constructor(data?: MessageActionRow | MessageActionRowOptions); + public type: 'ACTION_ROW'; + public components: MessageComponent[]; + public addComponent(component: MessageComponentOptions): this; + public addComponents(...components: MessageComponentOptions[] | MessageComponentOptions[][]): this; + } + export class MessageAttachment { constructor(attachment: BufferResolvable | Stream, name?: string, data?: unknown); @@ -1239,6 +1297,23 @@ declare module 'discord.js' { public toJSON(): unknown; } + export class MessageButton extends BaseMessageComponent { + constructor(data?: MessageButton | MessageButtonOptions); + public customID: string | null; + public disabled: boolean; + public emoji: unknown | null; + public label: string | null; + public style: MessageButtonStyle | null; + public type: 'BUTTON'; + public url: string | null; + public setCustomID(customID: string): this; + public setDisabled(disabled: boolean): this; + public setEmoji(emoji: EmojiIdentifierResolvable): this; + public setLabel(label: string): this; + public setStyle(style: MessageButtonStyle | number): this; + public setURL(url: string): this; + } + export class MessageCollector extends Collector { constructor( channel: TextChannel | DMChannel, @@ -3201,16 +3276,40 @@ declare module 'discord.js' { type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; + interface MessageActionRowOptions extends BaseMessageComponentOptions { + type: 'ACTION_ROW'; + components?: MessageComponentResolvable[]; + } + interface MessageActivity { partyID: string; type: number; } + interface MessageButtonOptions extends BaseMessageComponentOptions { + customID?: string; + disabled?: boolean; + emoji?: RawEmoji; + label?: string; + style?: MessageButtonStyle; + url?: string; + } + + type MessageButtonStyle = keyof typeof MessageButtonStyles; + interface MessageCollectorOptions extends CollectorOptions { max?: number; maxProcessed?: number; } + type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton; + + type MessageComponentOptions = BaseMessageComponentOptions | MessageActionRowOptions | MessageButtonOptions; + + type MessageComponentResolvable = MessageComponent | MessageComponentOptions; + + type MessageComponentType = keyof typeof MessageComponentTypes; + interface MessageEditOptions { attachments?: MessageAttachment[]; content?: string | null; @@ -3219,6 +3318,7 @@ declare module 'discord.js' { files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; + components?: BaseMessageComponent[]; } interface MessageEmbedAuthor { @@ -3311,6 +3411,7 @@ declare module 'discord.js' { nonce?: string | number; content?: string; embed?: MessageEmbed | MessageEmbedOptions; + components?: MessageComponentResolvable[]; allowedMentions?: MessageMentionOptions; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; From 9d5425712ccadc525d9d261e785a4cdf9e5cfaf7 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Wed, 26 May 2021 07:48:39 +1000 Subject: [PATCH 02/55] refactor: make component interactions more generic --- .../websocket/handlers/INTERACTION_CREATE.js | 4 +-- ...Interaction.js => ComponentInteraction.js} | 4 +-- ...or.js => ComponentInteractionCollector.js} | 16 ++++++------ src/structures/Interaction.js | 6 ++--- src/structures/Message.js | 26 +++++++++---------- src/util/Structures.js | 4 +-- typings/index.d.ts | 4 +-- 7 files changed, 32 insertions(+), 32 deletions(-) rename src/structures/{ButtonInteraction.js => ComponentInteraction.js} (98%) rename src/structures/{ButtonInteractionCollector.js => ComponentInteractionCollector.js} (87%) diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js index 37de3e1b9a65..dce9b2f1eba5 100644 --- a/src/client/websocket/handlers/INTERACTION_CREATE.js +++ b/src/client/websocket/handlers/INTERACTION_CREATE.js @@ -15,9 +15,9 @@ module.exports = (client, { d: data }) => { } case InteractionTypes.MESSAGE_COMPONENT: { if (!Structures) Structures = require('../../../util/Structures'); - const ButtonInteraction = Structures.get('ButtonInteraction'); + const ComponentInteraction = Structures.get('ComponentInteraction'); - interaction = new ButtonInteraction(client, data); + interaction = new ComponentInteraction(client, data); break; } default: diff --git a/src/structures/ButtonInteraction.js b/src/structures/ComponentInteraction.js similarity index 98% rename from src/structures/ButtonInteraction.js rename to src/structures/ComponentInteraction.js index 76a7db0238ec..33bd53234710 100644 --- a/src/structures/ButtonInteraction.js +++ b/src/structures/ComponentInteraction.js @@ -11,7 +11,7 @@ const MessageFlags = require('../util/MessageFlags'); * Represents a message button interaction. * @extends {Interaction} */ -class ButtonInteraction extends Interaction { +class ComponentInteraction extends Interaction { // eslint-disable-next-line no-useless-constructor constructor(client, data) { super(client, data); @@ -230,4 +230,4 @@ class ButtonInteraction extends Interaction { } } -module.exports = ButtonInteraction; +module.exports = ComponentInteraction; diff --git a/src/structures/ButtonInteractionCollector.js b/src/structures/ComponentInteractionCollector.js similarity index 87% rename from src/structures/ButtonInteractionCollector.js rename to src/structures/ComponentInteractionCollector.js index be13d87d6cd1..cb3042af6d06 100644 --- a/src/structures/ButtonInteractionCollector.js +++ b/src/structures/ComponentInteractionCollector.js @@ -5,9 +5,9 @@ const Collection = require('../util/Collection'); const { Events } = require('../util/Constants'); /** - * @typedef {CollectorOptions} ButtonInteractionCollectorOptions + * @typedef {CollectorOptions} ComponentInteractionCollectorOptions * @property {number} max The maximum total amount of interactions to collect - * @property {number} maxButtons The maximum number of buttons to collect + * @property {number} maxComponents The maximum number of buttons to collect * @property {number} maxUsers The maximum number of users to interact */ @@ -17,11 +17,11 @@ const { Events } = require('../util/Constants'); * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. * @extends {Collector} */ -class ButtonInteractionCollector extends Collector { +class ComponentInteractionCollector extends Collector { /** * @param {Message} message The message upon which to collect button interactions * @param {CollectorFilter} filter The filter to apply to this collector - * @param {ButtonInteractionCollectorOptions} [options={}] The options to apply to this collector + * @param {ComponentInteractionCollectorOptions} [options={}] The options to apply to this collector */ constructor(message, filter, options = {}) { super(message.client, filter, options); @@ -78,8 +78,8 @@ class ButtonInteractionCollector extends Collector { collect(interaction) { /** * Emitted whenever a reaction is collected. - * @event ButtonInteractionCollector#collect - * @param {ButtonInteraction} interaction The reaction that was collected + * @event ComponentInteractionCollector#collect + * @param {ComponentInteraction} interaction The reaction that was collected */ if (!interaction.isButton()) return null; @@ -100,7 +100,7 @@ class ButtonInteractionCollector extends Collector { get endReason() { if (this.options.max && this.total >= this.options.max) return 'limit'; - if (this.options.maxButtons && this.collected.size >= this.options.maxButtons) return 'buttonLimit'; + if (this.options.maxComponents && this.collected.size >= this.options.maxComponents) return 'componentLimit'; if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; return null; } @@ -142,4 +142,4 @@ class ButtonInteractionCollector extends Collector { } } -module.exports = ButtonInteractionCollector; +module.exports = ComponentInteractionCollector; diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 771c36d9070e..34fcc596e705 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -114,11 +114,11 @@ class Interaction extends Base { } /** - * Indicates whether this interaction is a button interacion. + * Indicates whether this interaction is a component interacion. * @returns {boolean} */ - isButton() { - return InteractionTypes[this.type] === InteractionTypes.BUTTON; + isComponent() { + return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT; } } diff --git a/src/structures/Message.js b/src/structures/Message.js index 199c94e2d067..efff64d4e6f2 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -3,8 +3,8 @@ const APIMessage = require('./APIMessage'); const Base = require('./Base'); const BaseMessageComponent = require('./BaseMessageComponent'); -const ButtonInteractionCollector = require('./ButtonInteractionCollector'); const ClientApplication = require('./ClientApplication'); +const ComponentInteractionCollector = require('./ComponentInteractionCollector'); const MessageAttachment = require('./MessageAttachment'); const Embed = require('./MessageEmbed'); const Mentions = require('./MessageMentions'); @@ -420,41 +420,41 @@ class Message extends Base { /** * Creates a button interaction collector. * @param {CollectorFilter} filter The filter to apply - * @param {ButtonInteractionCollectorOptions} [options={}] Options to send to the collector - * @returns {ButtonInteractionCollector} + * @param {ComponentInteractionCollectorOptions} [options={}] Options to send to the collector + * @returns {ComponentInteractionCollector} * @example * // Create a button interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; - * const collector = message.createButtonInteractionCollector(filter, { time: 15000 }); + * const collector = message.createComponentInteractionCollector(filter, { time: 15000 }); * collector.on('collect', i => console.log(`Collected ${i.customID}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ - createButtonInteractionCollector(filter, options = {}) { - return new ButtonInteractionCollector(this, filter, options); + createComponentInteractionCollector(filter, options = {}) { + return new ComponentInteractionCollector(this, filter, options); } /** * An object containing the same properties as CollectorOptions, but a few more: - * @typedef {ButtonInteractionCollectorOptions} AwaitButtonInteractionsOptions + * @typedef {ComponentInteractionCollectorOptions} AwaitComponentInteractionsOptions * @property {string[]} [errors] Stop/end reasons that cause the promise to reject */ /** - * Similar to createButtonInteractionCollector but in promise form. + * Similar to createComponentInteractionCollector but in promise form. * Resolves with a collection of interactions that pass the specified filter. * @param {CollectorFilter} filter The filter function to use - * @param {AwaitButtonInteractionsOptions} [options={}] Optional options to pass to the internal collector - * @returns {Promise>} + * @param {AwaitComponentInteractionsOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise>} * @example * // Create a button interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; - * message.awaitButtonInteraction(filter, { time: 15000 }) + * message.awaitComponentInteraction(filter, { time: 15000 }) * .then(collected => console.log(`Collected ${collected.size} interactions`)) * .catch(console.error); */ - awaitButtonInteractions(filter, options = {}) { + awaitComponentInteractions(filter, options = {}) { return new Promise((resolve, reject) => { - const collector = this.createButtonInteractionCollector(filter, options); + const collector = this.createComponentInteractionCollector(filter, options); collector.once('end', (interactions, reason) => { if (options.errors && options.errors.includes(reason)) reject(interactions); else resolve(interactions); diff --git a/src/util/Structures.js b/src/util/Structures.js index bee9d6373cc2..95cf8a620358 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -20,7 +20,7 @@ * * **`Role`** * * **`User`** * * **`CommandInteraction`** - * * **`ButtonInteraction`** + * * **`ComponentInteraction`** * @typedef {string} ExtendableStructure */ @@ -112,7 +112,7 @@ const structures = { Role: require('../structures/Role'), User: require('../structures/User'), CommandInteraction: require('../structures/CommandInteraction'), - ButtonInteraction: require('../structures/ButtonInteraction'), + ComponentInteraction: require('../structures/ComponentInteraction'), }; module.exports = Structures; diff --git a/typings/index.d.ts b/typings/index.d.ts index 293ac5efabcf..b52c6842358f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -319,7 +319,7 @@ declare module 'discord.js' { public static resolve(bit?: BitFieldResolvable): number | bigint; } - export class ButtonInteraction extends Interaction { + export class ComponentInteraction extends Interaction { public customID: string; public deferred: boolean; public message: Message | RawMessage; @@ -1158,7 +1158,7 @@ declare module 'discord.js' { public user: User; public version: number; public isCommand(): this is CommandInteraction; - public isButton(): this is ButtonInteraction; + public isComponent(): this is ComponentInteraction; } export class Invite extends Base { From 2a9e3a5f2fa9d95dd77edaaf123d50e6146583b5 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 27 May 2021 08:19:18 +1000 Subject: [PATCH 03/55] Apply suggestions from code review Co-authored-by: Vicente <33096355+Vicente015@users.noreply.github.com> Co-authored-by: Shubham Parihar Co-authored-by: SpaceEEC --- src/structures/BaseMessageComponent.js | 2 +- src/structures/ComponentInteractionCollector.js | 2 +- src/structures/Interaction.js | 2 +- src/structures/Message.js | 2 +- src/structures/MessageButton.js | 2 +- typings/index.d.ts | 10 +++++----- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index d847fdf9e9e4..f6e40ea9d7e0 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -23,7 +23,7 @@ class BaseMessageComponent { } /** - * Sets the type of this component; + * Sets the type of this component * @param {MessageComponentType} type The type of this component * @returns {BaseMessageComponent} */ diff --git a/src/structures/ComponentInteractionCollector.js b/src/structures/ComponentInteractionCollector.js index cb3042af6d06..9d3dd2679c23 100644 --- a/src/structures/ComponentInteractionCollector.js +++ b/src/structures/ComponentInteractionCollector.js @@ -81,7 +81,7 @@ class ComponentInteractionCollector extends Collector { * @event ComponentInteractionCollector#collect * @param {ComponentInteraction} interaction The reaction that was collected */ - if (!interaction.isButton()) return null; + if (!interaction.isComponent()) return null; if (interaction.message.id !== this.message.id) return null; diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 34fcc596e705..46dd29aea5f2 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -114,7 +114,7 @@ class Interaction extends Base { } /** - * Indicates whether this interaction is a component interacion. + * Indicates whether this interaction is a component interaction. * @returns {boolean} */ isComponent() { diff --git a/src/structures/Message.js b/src/structures/Message.js index efff64d4e6f2..9ca9c52c9bbe 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -126,7 +126,7 @@ class Message extends Base { this.embeds = (data.embeds || []).map(e => new Embed(e, true)); /** - * A list of component in the message e.g. ActionRows, Buttons + * A list of components in the message e.g. ActionRows, Buttons * @type {MessageComponent[]} */ this.components = (data.components || []).map(BaseMessageComponent.create); diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 7bcc39cf9415..a8d5d1a4c843 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -44,7 +44,7 @@ class MessageButton extends BaseMessageComponent { /** * Emoji for this button - * @type {Emoji|string} + * @type {?Emoji|string} */ this.emoji = data.emoji ?? null; diff --git a/typings/index.d.ts b/typings/index.d.ts index b52c6842358f..4b37f2da786b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -293,7 +293,7 @@ declare module 'discord.js' { } interface BaseMessageComponentOptions { - type?: MessageComponentType; + type?: MessageComponentType | MessageComponentTypes; } class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { @@ -1310,7 +1310,7 @@ declare module 'discord.js' { public setDisabled(disabled: boolean): this; public setEmoji(emoji: EmojiIdentifierResolvable): this; public setLabel(label: string): this; - public setStyle(style: MessageButtonStyle | number): this; + public setStyle(style: MessageButtonStyle | MessageButtonStyles): this; public setURL(url: string): this; } @@ -3277,7 +3277,7 @@ declare module 'discord.js' { type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; interface MessageActionRowOptions extends BaseMessageComponentOptions { - type: 'ACTION_ROW'; + type: 'ACTION_ROW' | MessageComponentTypes.ACTION_ROW; components?: MessageComponentResolvable[]; } @@ -3291,7 +3291,7 @@ declare module 'discord.js' { disabled?: boolean; emoji?: RawEmoji; label?: string; - style?: MessageButtonStyle; + style?: MessageButtonStyle | MessageButtonStyles; url?: string; } @@ -3318,7 +3318,7 @@ declare module 'discord.js' { files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; - components?: BaseMessageComponent[]; + components?: MessageComponentResolvable[]; } interface MessageEmbedAuthor { From f454435152a9251637687a87cfe46145460c66bf Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 08:20:24 +1000 Subject: [PATCH 04/55] fix: add constants to typings --- typings/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4b37f2da786b..56c3cb250f25 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -737,6 +737,8 @@ declare module 'discord.js' { ApplicationCommandPermissionTypes: typeof ApplicationCommandPermissionTypes; InteractionTypes: typeof InteractionTypes; InteractionResponseTypes: typeof InteractionResponseTypes; + MessageComponentTypes: typeof MessageComponentTypes; + MessageButtonStyles: typeof MessageButtonStyles; NSFWLevels: typeof NSFWLevels; }; From fe9f3c62f2e5fc1e7ab8f4570611365ee7ee1ecf Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 09:42:07 +1000 Subject: [PATCH 05/55] fix: export new classes --- src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index 59308276e0ae..4ab7eb51677c 100644 --- a/src/index.js +++ b/src/index.js @@ -78,6 +78,8 @@ module.exports = { }, Collector: require('./structures/interfaces/Collector'), CommandInteraction: require('./structures/CommandInteraction'), + ComponentInteraction: require('./structures/ComponentInteraction'), + ComponentInteractionCollector: require('./structures/ComponentInteractionCollector'), DMChannel: require('./structures/DMChannel'), Emoji: require('./structures/Emoji'), Guild: require('./structures/Guild'), From 25cf23486eb41f80b7e7ea58b5d1aae06a0c4b86 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 09:42:27 +1000 Subject: [PATCH 06/55] feat: improved jsdoc typedefs --- src/structures/BaseMessageComponent.js | 42 ++++++++++++++++++++++++-- src/structures/MessageActionRow.js | 14 +-------- src/structures/MessageButton.js | 2 +- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index f6e40ea9d7e0..56a8908fb241 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -7,10 +7,31 @@ const { MessageComponentTypes, MessageButtonStyles } = require('../util/Constant */ class BaseMessageComponent { /** + * Options for a BaseMessageComponent * @typedef {Object} BaseMessageComponentOptions * @property {MessageComponentType} type The type of this component */ + /** + * Data that can be resolved into options for a MessageComponent. This can be: + * * MessageActionRowOptions + * * MessageButtonOptions + * @typedef {MessageActionRowOptions|MessageButtonOptions} MessageComponentOptions + */ + + /** + * Components that can be sent in a message + * @typedef {MessageActionRow|MessageButton} MessageComponent + */ + + /** + * Data that resolves to give a MessageComponent object. This can be: + * * A MessageComponentOptions object + * * A MessageActionRow + * * A MessageButton + * @typedef {MessageComponentOptions|MessageComponent} MessageComponentResolvable + */ + /** * @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component */ @@ -19,16 +40,16 @@ class BaseMessageComponent { * The type of this component * @type {?MessageComponentType} */ - this.type = 'type' in data ? data.type : null; + this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null; } /** * Sets the type of this component - * @param {MessageComponentType} type The type of this component + * @param {MessageComponentTypeResolvable} type The type of this component * @returns {BaseMessageComponent} */ setType(type) { - this.type = type; + this.type = BaseMessageComponent.resolveType(type); return this; } @@ -53,6 +74,21 @@ class BaseMessageComponent { return component; } + /** + * Resolves the type of a MessageComponent + * @param {MessageComponentTypeResolvable} type The type to resolve + * @returns {any} + */ + static resolveType(type) { + return typeof type === 'string' ? type : MessageComponentTypes[type]; + } + + /** + * Transforms a MessageComponent object into something that can be used with the API. + * @param {MessageComponentResolvable} component The component to transform + * @returns {Object} + * @private + */ static transform(component) { const { type, components, label, customID, style, emoji, url, disabled } = component; diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index 5c3de0b86ca0..9ef424fcf9f1 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -7,19 +7,7 @@ const BaseMessageComponent = require('./BaseMessageComponent'); */ class MessageActionRow extends BaseMessageComponent { /** - * @typedef {BaseMessageComponentOptions|MessageActionRowOptions|MessageButtonOptions} MessageComponentOptions - */ - - /** - * @typedef {BaseMessageComponent|MessageActionRow|MessageButton} MessageComponent - */ - - /** - * @typedef {MessageComponentOptions|MessageComponent} MessageComponentResolvable - */ - - /** - * @typedef {Object} MessageActionRowOptions + * @typedef {BaseMessageComponentOptions} MessageActionRowOptions * @property {MessageComponent[]|MessageComponentOptions[]} [components] The components to place in this ActionRow */ diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index a8d5d1a4c843..38b1e3d3d9ae 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -5,7 +5,7 @@ const { MessageButtonStyles } = require('../util/Constants.js'); const Util = require('../util/Util'); class MessageButton extends BaseMessageComponent { /** - * @typedef {Object} MessageButtonOptions + * @typedef {BaseMessageComponentOptions} MessageButtonOptions * @property {string} [label] The text to be displayed on this button * @property {string} [customID] A unique string to be sent in the interaction when clicked * @property {MessageButtonStyle} [style] The style of this button From adb2cbf7fcc286bb7b04ca3cf9a3f9369535cc28 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 09:42:42 +1000 Subject: [PATCH 07/55] fix: rename component type --- src/util/Constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index a97dba1391b8..3877c3ab82eb 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -830,7 +830,7 @@ exports.InteractionResponseTypes = createEnum([ * BUTTON * @typedef {string} MessageComponentType */ -exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON']); +exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'MESSAGE_COMPONENT']); /** * The style of a message button From 3aafa5c3ed4921b90f2bbb3fd98f0906b7ffc5bf Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 10:44:50 +1000 Subject: [PATCH 08/55] feat: typings for missing methods and general improvements --- src/structures/BaseMessageComponent.js | 11 +++++++++-- src/structures/MessageButton.js | 10 ++++++++-- typings/index.d.ts | 15 ++++++++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 56a8908fb241..fc45da619f4b 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -9,7 +9,7 @@ class BaseMessageComponent { /** * Options for a BaseMessageComponent * @typedef {Object} BaseMessageComponentOptions - * @property {MessageComponentType} type The type of this component + * @property {MessageComponentTypeResolvable} type The type of this component */ /** @@ -53,6 +53,12 @@ class BaseMessageComponent { return this; } + /** + * Constructs a MessageComponent based on the type of the incoming data + * @param {MessageComponentOptions} data Data for a MessageComponent + * @returns {MessageComponent} + * @private + */ static create(data) { let component; let type = data.type; @@ -77,7 +83,8 @@ class BaseMessageComponent { /** * Resolves the type of a MessageComponent * @param {MessageComponentTypeResolvable} type The type to resolve - * @returns {any} + * @returns {MessageComponentType} + * @private */ static resolveType(type) { return typeof type === 'string' ? type : MessageComponentTypes[type]; diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 38b1e3d3d9ae..93881795b989 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -8,7 +8,7 @@ class MessageButton extends BaseMessageComponent { * @typedef {BaseMessageComponentOptions} MessageButtonOptions * @property {string} [label] The text to be displayed on this button * @property {string} [customID] A unique string to be sent in the interaction when clicked - * @property {MessageButtonStyle} [style] The style of this button + * @property {MessageButtonStyleResolvable} [style] The style of this button * @property {Emoji} [emoji] The emoji to be displayed to the left of the text * @property {string} [url] Optional URL for link-style buttons * @property {boolean} [disabled=false] Disables the button to prevent interactions @@ -103,7 +103,7 @@ class MessageButton extends BaseMessageComponent { /** * Sets the style of this button - * @param {MessageButtonStyle} style The style of this button + * @param {MessageButtonStyleResolvable} style The style of this button * @returns {MessageButton} */ setStyle(style) { @@ -121,6 +121,12 @@ class MessageButton extends BaseMessageComponent { return this; } + /** + * Resolves the style of a MessageButton + * @param {MessageButtonStyleResolvable} style The style to resolve + * @returns {MessageButtonStyle} + * @private + */ static resolveStyle(style) { return typeof style === 'string' ? style : MessageButtonStyles[style]; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 56c3cb250f25..f5cfcc2a6d25 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -289,7 +289,10 @@ declare module 'discord.js' { export class BaseMessageComponent { constructor(data?: BaseMessageComponent | BaseMessageComponentOptions); public type: MessageComponentType | null; - public setType(type: MessageComponentType | number): this; + public setType(type: MessageComponentTypeResolvable): this; + private static create(data: MessageComponentOptions): MessageComponent; + private static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; + private static transform(component: MessageComponentResolvable): object; } interface BaseMessageComponentOptions { @@ -1312,8 +1315,9 @@ declare module 'discord.js' { public setDisabled(disabled: boolean): this; public setEmoji(emoji: EmojiIdentifierResolvable): this; public setLabel(label: string): this; - public setStyle(style: MessageButtonStyle | MessageButtonStyles): this; + public setStyle(style: MessageButtonStyleResolvable): this; public setURL(url: string): this; + private static resolveStyle(style: MessageButtonStyleResolvable): MessageButtonStyle; } export class MessageCollector extends Collector { @@ -3293,12 +3297,15 @@ declare module 'discord.js' { disabled?: boolean; emoji?: RawEmoji; label?: string; - style?: MessageButtonStyle | MessageButtonStyles; + style: MessageButtonStyleResolvable; + type: 'BUTTON' | MessageComponentTypes.BUTTON; url?: string; } type MessageButtonStyle = keyof typeof MessageButtonStyles; + type MessageButtonStyleResolvable = MessageButtonStyle | MessageButtonStyles | string | number; + interface MessageCollectorOptions extends CollectorOptions { max?: number; maxProcessed?: number; @@ -3312,6 +3319,8 @@ declare module 'discord.js' { type MessageComponentType = keyof typeof MessageComponentTypes; + type MessageComponentTypeResolvable = MessageComponentType | MessageComponentTypes | string | number; + interface MessageEditOptions { attachments?: MessageAttachment[]; content?: string | null; From c0c3a58eda445c5c99369573e3288e82766a7890 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 11:27:04 +1000 Subject: [PATCH 09/55] fix: changed the wrong constant --- src/util/Constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 3877c3ab82eb..a97dba1391b8 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -830,7 +830,7 @@ exports.InteractionResponseTypes = createEnum([ * BUTTON * @typedef {string} MessageComponentType */ -exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'MESSAGE_COMPONENT']); +exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON']); /** * The style of a message button From 52f128a668c070f6361b570491ae376238cad1f4 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 08:02:14 +1000 Subject: [PATCH 10/55] refactor: move interaction response methods to interface --- src/structures/CommandInteraction.js | 155 ++------------- src/structures/ComponentInteraction.js | 148 ++------------ .../interfaces/InteractionResponses.js | 184 ++++++++++++++++++ 3 files changed, 208 insertions(+), 279 deletions(-) create mode 100644 src/structures/interfaces/InteractionResponses.js diff --git a/src/structures/CommandInteraction.js b/src/structures/CommandInteraction.js index 095ef871b2a8..0f3aeca32c1b 100644 --- a/src/structures/CommandInteraction.js +++ b/src/structures/CommandInteraction.js @@ -1,12 +1,10 @@ 'use strict'; -const APIMessage = require('./APIMessage'); const Interaction = require('./Interaction'); +const InteractionResponses = require('./interfaces/InteractionResponses'); const WebhookClient = require('../client/WebhookClient'); -const { Error } = require('../errors'); const Collection = require('../util/Collection'); -const { ApplicationCommandOptionTypes, InteractionResponseTypes } = require('../util/Constants'); -const MessageFlags = require('../util/MessageFlags'); +const { ApplicationCommandOptionTypes } = require('../util/Constants'); /** * Represents a command interaction. @@ -69,126 +67,6 @@ class CommandInteraction extends Interaction { return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null; } - /** - * Options for deferring the reply to a {@link CommandInteraction}. - * @typedef {Object} InteractionDeferOptions - * @property {boolean} [ephemeral] Whether the reply should be ephemeral - */ - - /** - * Defers the reply to this interaction. - * @param {InteractionDeferOptions} [options] Options for deferring the reply to this interaction - * @returns {Promise} - * @example - * // Defer the reply to this interaction - * interaction.defer() - * .then(console.log) - * .catch(console.error) - * @example - * // Defer to send an ephemeral reply later - * interaction.defer({ ephemeral: true }) - * .then(console.log) - * .catch(console.error); - */ - async defer({ ephemeral } = {}) { - if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); - await this.client.api.interactions(this.id, this.token).callback.post({ - data: { - type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, - data: { - flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined, - }, - }, - }); - this.deferred = true; - } - - /** - * Options for a reply to an interaction. - * @typedef {BaseMessageOptions} InteractionReplyOptions - * @property {boolean} [ephemeral] Whether the reply should be ephemeral - * @property {MessageEmbed[]|Object[]} [embeds] An array of embeds for the message - */ - - /** - * Creates a reply to this interaction. - * @param {string|APIMessage|MessageAdditions} content The content for the reply - * @param {InteractionReplyOptions} [options] Additional options for the reply - * @returns {Promise} - * @example - * // Reply to the interaction with an embed - * const embed = new MessageEmbed().setDescription('Pong!'); - * - * interaction.reply(embed) - * .then(console.log) - * .catch(console.error); - * @example - * // Create an ephemeral reply - * interaction.reply('Pong!', { ephemeral: true }) - * .then(console.log) - * .catch(console.error); - */ - async reply(content, options) { - if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); - const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); - const { data, files } = await apiMessage.resolveData().resolveFiles(); - - await this.client.api.interactions(this.id, this.token).callback.post({ - data: { - type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE, - data, - }, - files, - }); - this.replied = true; - } - - /** - * Fetches the initial reply to this interaction. - * @see Webhook#fetchMessage - * @returns {Promise} - * @example - * // Fetch the reply to this interaction - * interaction.fetchReply() - * .then(reply => console.log(`Replied with ${reply.content}`)) - * .catch(console.error); - */ - async fetchReply() { - const raw = await this.webhook.fetchMessage('@original'); - return this.channel?.messages.add(raw) ?? raw; - } - - /** - * Edits the initial reply to this interaction. - * @see Webhook#editMessage - * @param {string|APIMessage|MessageAdditions} content The new content for the message - * @param {WebhookEditMessageOptions} [options] The options to provide - * @returns {Promise} - * @example - * // Edit the reply to this interaction - * interaction.editReply('New content') - * .then(console.log) - * .catch(console.error); - */ - async editReply(content, options) { - const raw = await this.webhook.editMessage('@original', content, options); - return this.channel?.messages.add(raw) ?? raw; - } - - /** - * Deletes the initial reply to this interaction. - * @see Webhook#deleteMessage - * @returns {Promise} - * @example - * // Delete the reply to this interaction - * interaction.deleteReply() - * .then(console.log) - * .catch(console.error); - */ - async deleteReply() { - await this.webhook.deleteMessage('@original'); - } - /** * Represents an option of a received command interaction. * @typedef {Object} CommandInteractionOption @@ -203,24 +81,6 @@ class CommandInteraction extends Interaction { * @property {Role|Object} [role] The resolved role */ - /** - * Send a follow-up message to this interaction. - * @param {string|APIMessage|MessageAdditions} content The content for the reply - * @param {InteractionReplyOptions} [options] Additional options for the reply - * @returns {Promise} - */ - async followUp(content, options) { - const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); - const { data, files } = await apiMessage.resolveData().resolveFiles(); - - const raw = await this.client.api.webhooks(this.applicationID, this.token).post({ - data, - files, - }); - - return this.channel?.messages.add(raw) ?? raw; - } - /** * Transforms an option received from the API. * @param {Object} option The received option @@ -267,6 +127,17 @@ class CommandInteraction extends Interaction { } return optionsCollection; } + + // These are here only for documentation purposes - they are implemented by InteractionResponses + /* eslint-disable no-empty-function */ + defer() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} } +InteractionResponses.applyToClass(CommandInteraction); + module.exports = CommandInteraction; diff --git a/src/structures/ComponentInteraction.js b/src/structures/ComponentInteraction.js index 33bd53234710..8b96c80e08ae 100644 --- a/src/structures/ComponentInteraction.js +++ b/src/structures/ComponentInteraction.js @@ -2,10 +2,10 @@ const APIMessage = require('./APIMessage'); const Interaction = require('./Interaction'); +const InteractionResponses = require('./interfaces/InteractionResponses'); const WebhookClient = require('../client/WebhookClient'); const { Error } = require('../errors'); const { InteractionResponseTypes } = require('../util/Constants'); -const MessageFlags = require('../util/MessageFlags'); /** * Represents a message button interaction. @@ -47,73 +47,6 @@ class ComponentInteraction extends Interaction { this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options); } - /** - * Defers the reply to this interaction. - * @param {boolean} [ephemeral] Whether the reply should be ephemeral - * @returns {Promise} - * @example - * // Defer the reply to this interaction - * interaction.defer() - * .then(console.log) - * .catch(console.error) - * @example - * // Defer to send an ephemeral reply later - * interaction.defer(true) - * .then(console.log) - * .catch(console.error); - */ - async defer(ephemeral) { - if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); - await this.client.api.interactions(this.id, this.token).callback.post({ - data: { - type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, - data: { - flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined, - }, - }, - }); - this.deferred = true; - } - - /** - * Options for a reply to an interaction. - * @typedef {WebhookMessageOptions} InteractionReplyOptions - * @property {boolean} [ephemeral] Whether the reply should be ephemeral - */ - - /** - * Creates a reply to this interaction. - * @param {string|APIMessage|MessageAdditions} content The content for the reply - * @param {InteractionReplyOptions} [options] Additional options for the reply - * @returns {Promise} - * @example - * // Reply to the interaction with an embed - * const embed = new MessageEmbed().setDescription('Pong!'); - * - * interaction.reply(embed) - * .then(console.log) - * .catch(console.error); - * @example - * // Create an ephemeral reply - * interaction.reply('Pong!', { ephemeral: true }) - * .then(console.log) - * .catch(console.error); - */ - async reply(content, options) { - if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); - const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); - const { data, files } = await apiMessage.resolveData().resolveFiles(); - - await this.client.api.interactions(this.id, this.token).callback.post({ - data: { - type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE, - data, - }, - files, - }); - this.replied = true; - } - /** * Defers an update to the message to which the button was attached * @returns {Promise} @@ -159,75 +92,16 @@ class ComponentInteraction extends Interaction { this.replied = true; } - /** - * Fetches the initial reply to this interaction. - * * For `defer` and `reply` this is the new message - * * For `deferUpdate` and `update` this is the message to which the buttons are attached - * @see Webhook#fetchMessage - * @returns {Promise} - * @example - * // Fetch the reply to this interaction - * interaction.fetchReply() - * .then(reply => console.log(`Replied with ${reply.content}`)) - * .catch(console.error); - */ - async fetchReply() { - const raw = await this.webhook.fetchMessage('@original'); - return this.channel?.messages.add(raw) ?? raw; - } - - /** - * Edits the initial reply to this interaction. - * * For `defer` and `reply` this is the new message sent - * * For `deferUpdate` and `update` this is the message to which the buttons are attached - * @see Webhook#editMessage - * @param {string|APIMessage|MessageEmbed|MessageEmbed[]} content The new content for the message - * @param {WebhookEditMessageOptions} [options] The options to provide - * @returns {Promise} - * @example - * // Edit the reply to this interaction - * interaction.editReply('New content') - * .then(console.log) - * .catch(console.error); - */ - async editReply(content, options) { - const raw = await this.webhook.editMessage('@original', content, options); - return this.channel?.messages.add(raw) ?? raw; - } - - /** - * Deletes the initial reply to this interaction. - * * For `defer` and `reply` this is the new message - * * For `deferUpdate` and `update` this is the message to which the buttons are attached - * @see Webhook#deleteMessage - * @returns {Promise} - * @example - * // Delete the reply to this interaction - * interaction.deleteReply() - * .then(console.log) - * .catch(console.error); - */ - async deleteReply() { - await this.webhook.deleteMessage('@original'); - } - - /** - * Send a follow-up message to this interaction. - * @param {string|APIMessage|MessageAdditions} content The content for the reply - * @param {InteractionReplyOptions} [options] Additional options for the reply - * @returns {Promise} - */ - async followUp(content, options) { - const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); - const { data, files } = await apiMessage.resolveData().resolveFiles(); - - const raw = await this.client.api.webhooks(this.applicationID, this.token).post({ - data, - files, - }); - - return this.channel?.messages.add(raw) ?? raw; - } + // These are here only for documentation purposes - they are implemented by InteractionResponses + /* eslint-disable no-empty-function */ + defer() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} } +InteractionResponses.applyToClass(ComponentInteraction); + module.exports = ComponentInteraction; diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js new file mode 100644 index 000000000000..53fae9c0a63b --- /dev/null +++ b/src/structures/interfaces/InteractionResponses.js @@ -0,0 +1,184 @@ +'use strict'; + +const WebhookClient = require('../../client/WebhookClient'); +const { InteractionResponseTypes } = require('../../util/Constants'); +const MessageFlags = require('../../util/MessageFlags'); +const APIMessage = require('../APIMessage'); + +/** + * Interface for classes that support shared interaction response types. + * @interface + */ +class InteractionResponses { + constructor() { + /** + * Whether the reply to this interaction has been deferred + * @type {boolean} + */ + this.deferred = false; + + /** + * Whether this interaction has already been replied to + * @type {boolean} + */ + this.replied = false; + + /** + * An associated webhook client, can be used to create deferred replies + * @type {WebhookClient} + */ + this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options); + } + + /** + * Options for deferring the reply to a {@link CommandInteraction}. + * @typedef {InteractionDeferOptions} + * @property {boolean} [ephemeral] Whether the reply should be ephemeral + */ + + /** + * Options for a reply to an interaction. + * @typedef {BaseMessageOptions} InteractionReplyOptions + * @property {boolean} [ephemeral] Whether the reply should be ephemeral + * @property {MessageEmbed[]|Object[]} [embeds] An array of embeds for the message + */ + + /** + * Defers the reply to this interaction. + * @param {InteractionDeferOptions} [options] Options for deferring the reply to this interaction + * @returns {Promise} + * @example + * // Defer the reply to this interaction + * interaction.defer() + * .then(console.log) + * .catch(console.error) + * @example + * // Defer to send an ephemeral reply later + * interaction.defer({ ephemeral: true }) + * .then(console.log) + * .catch(console.error); + */ + async defer({ ephemeral } = {}) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, + data: { + flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined, + }, + }, + }); + this.deferred = true; + } + + /** + * Creates a reply to this interaction. + * @param {string|APIMessage|MessageAdditions} content The content for the reply + * @param {InteractionReplyOptions} [options] Additional options for the reply + * @returns {Promise} + * @example + * // Reply to the interaction with an embed + * const embed = new MessageEmbed().setDescription('Pong!'); + * + * interaction.reply(embed) + * .then(console.log) + * .catch(console.error); + * @example + * // Create an ephemeral reply + * interaction.reply('Pong!', { ephemeral: true }) + * .then(console.log) + * .catch(console.error); + */ + async reply(content, options) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); + const { data, files } = await apiMessage.resolveData().resolveFiles(); + + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE, + data, + }, + files, + }); + this.replied = true; + } + + /** + * Fetches the initial reply to this interaction. + * @see Webhook#fetchMessage + * @returns {Promise} + * @example + * // Fetch the reply to this interaction + * interaction.fetchReply() + * .then(reply => console.log(`Replied with ${reply.content}`)) + * .catch(console.error); + */ + async fetchReply() { + const raw = await this.webhook.fetchMessage('@original'); + return this.channel?.messages.add(raw) ?? raw; + } + + /** + * Edits the initial reply to this interaction. + * @see Webhook#editMessage + * @param {string|APIMessage|MessageAdditions} content The new content for the message + * @param {WebhookEditMessageOptions} [options] The options to provide + * @returns {Promise} + * @example + * // Edit the reply to this interaction + * interaction.editReply('New content') + * .then(console.log) + * .catch(console.error); + */ + async editReply(content, options) { + const raw = await this.webhook.editMessage('@original', content, options); + return this.channel?.messages.add(raw) ?? raw; + } + + /** + * Deletes the initial reply to this interaction. + * @see Webhook#deleteMessage + * @returns {Promise} + * @example + * // Delete the reply to this interaction + * interaction.deleteReply() + * .then(console.log) + * .catch(console.error); + */ + async deleteReply() { + await this.webhook.deleteMessage('@original'); + } + + /** + * Send a follow-up message to this interaction. + * @param {string|APIMessage|MessageAdditions} content The content for the reply + * @param {InteractionReplyOptions} [options] Additional options for the reply + * @returns {Promise} + */ + async followUp(content, options) { + const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); + const { data, files } = await apiMessage.resolveData().resolveFiles(); + + const raw = await this.client.api.webhooks(this.applicationID, this.token).post({ + data, + files, + }); + + return this.channel?.messages.add(raw) ?? raw; + } + + static applyToClass(structure, ignore = []) { + const props = ['defer', 'reply', 'fetchReply', 'editReply', 'deleteReply', 'followUp']; + for (const prop of props) { + if (ignore.includes(prop)) continue; + Object.defineProperty( + structure.prototype, + prop, + Object.getOwnPropertyDescriptor(InteractionResponses.prototype, prop), + ); + } + } +} + +module.exports = InteractionResponses; From a45bb0e96f53d04562c585400dea92579a5e1a85 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 08:06:53 +1000 Subject: [PATCH 11/55] refactor: move additional response methods --- src/structures/CommandInteraction.js | 2 +- src/structures/ComponentInteraction.js | 50 +------------------ .../interfaces/InteractionResponses.js | 47 ++++++++++++++++- 3 files changed, 49 insertions(+), 50 deletions(-) diff --git a/src/structures/CommandInteraction.js b/src/structures/CommandInteraction.js index 0f3aeca32c1b..66f6e5ef856f 100644 --- a/src/structures/CommandInteraction.js +++ b/src/structures/CommandInteraction.js @@ -138,6 +138,6 @@ class CommandInteraction extends Interaction { followUp() {} } -InteractionResponses.applyToClass(CommandInteraction); +InteractionResponses.applyToClass(CommandInteraction, ['deferUpdate', 'update']); module.exports = CommandInteraction; diff --git a/src/structures/ComponentInteraction.js b/src/structures/ComponentInteraction.js index 8b96c80e08ae..28e8ed7438d5 100644 --- a/src/structures/ComponentInteraction.js +++ b/src/structures/ComponentInteraction.js @@ -1,11 +1,8 @@ 'use strict'; -const APIMessage = require('./APIMessage'); const Interaction = require('./Interaction'); const InteractionResponses = require('./interfaces/InteractionResponses'); const WebhookClient = require('../client/WebhookClient'); -const { Error } = require('../errors'); -const { InteractionResponseTypes } = require('../util/Constants'); /** * Represents a message button interaction. @@ -47,51 +44,6 @@ class ComponentInteraction extends Interaction { this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options); } - /** - * Defers an update to the message to which the button was attached - * @returns {Promise} - * @example - * // Defer to update the button to a loading state - * interaction.defer() - * .then(console.log) - * .catch(console.error); - */ - async deferUpdate() { - if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); - await this.client.api.interactions(this.id, this.token).callback.post({ - data: { - type: InteractionResponseTypes.DEFERRED_MESSAGE_UPDATE, - }, - }); - this.deferred = true; - } - - /** - * Updates the message to which the button was attached - * @param {string|APIMessage|MessageAdditions} content The content for the reply - * @param {WebhookEditMessageOptions} [options] Additional options for the reply - * @returns {Promise} - * @example - * // Remove the buttons from the message * - * interaction.reply("A button was clicked", { components: [] }) - * .then(console.log) - * .catch(console.error); - */ - async update(content, options) { - if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); - const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); - const { data, files } = await apiMessage.resolveData().resolveFiles(); - - await this.client.api.interactions(this.id, this.token).callback.post({ - data: { - type: InteractionResponseTypes.UPDATE_MESSAGE, - data, - }, - files, - }); - this.replied = true; - } - // These are here only for documentation purposes - they are implemented by InteractionResponses /* eslint-disable no-empty-function */ defer() {} @@ -100,6 +52,8 @@ class ComponentInteraction extends Interaction { editReply() {} deleteReply() {} followUp() {} + deferUpdate() {} + update() {} } InteractionResponses.applyToClass(ComponentInteraction); diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index 53fae9c0a63b..fa4169cbfb1d 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -168,8 +168,53 @@ class InteractionResponses { return this.channel?.messages.add(raw) ?? raw; } + /** + * Defers an update to the message to which the button was attached + * @returns {Promise} + * @example + * // Defer to update the button to a loading state + * interaction.defer() + * .then(console.log) + * .catch(console.error); + */ + async deferUpdate() { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.DEFERRED_MESSAGE_UPDATE, + }, + }); + this.deferred = true; + } + + /** + * Updates the message to which the button was attached + * @param {string|APIMessage|MessageAdditions} content The content for the reply + * @param {WebhookEditMessageOptions} [options] Additional options for the reply + * @returns {Promise} + * @example + * // Remove the buttons from the message * + * interaction.reply("A button was clicked", { components: [] }) + * .then(console.log) + * .catch(console.error); + */ + async update(content, options) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); + const { data, files } = await apiMessage.resolveData().resolveFiles(); + + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.UPDATE_MESSAGE, + data, + }, + files, + }); + this.replied = true; + } + static applyToClass(structure, ignore = []) { - const props = ['defer', 'reply', 'fetchReply', 'editReply', 'deleteReply', 'followUp']; + const props = ['defer', 'reply', 'fetchReply', 'editReply', 'deleteReply', 'followUp', 'deferUpdate', 'update']; for (const prop of props) { if (ignore.includes(prop)) continue; Object.defineProperty( From 7045839b1dcd2c9951cd33e93b0d9c129a11592e Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 08:32:45 +1000 Subject: [PATCH 12/55] fix: make Component resolvables less generic --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index f5cfcc2a6d25..cff96e468a23 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -3304,7 +3304,7 @@ declare module 'discord.js' { type MessageButtonStyle = keyof typeof MessageButtonStyles; - type MessageButtonStyleResolvable = MessageButtonStyle | MessageButtonStyles | string | number; + type MessageButtonStyleResolvable = MessageButtonStyle | MessageButtonStyles; interface MessageCollectorOptions extends CollectorOptions { max?: number; @@ -3319,7 +3319,7 @@ declare module 'discord.js' { type MessageComponentType = keyof typeof MessageComponentTypes; - type MessageComponentTypeResolvable = MessageComponentType | MessageComponentTypes | string | number; + type MessageComponentTypeResolvable = MessageComponentType | MessageComponentTypes; interface MessageEditOptions { attachments?: MessageAttachment[]; From 17431c415297ae9c6db4e5597099d3eff1bbadff Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 08:50:26 +1000 Subject: [PATCH 13/55] fix: bug in API transform of component --- src/structures/BaseMessageComponent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index fc45da619f4b..603baa4b7833 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -105,8 +105,8 @@ class BaseMessageComponent { disabled, emoji, label, - style: MessageButtonStyles[style], - type: MessageComponentTypes[type], + style: typeof style === 'string' ? MessageButtonStyles[style] : style, + type: typeof type === 'string' ? MessageComponentTypes[type] : type, url, }; } From 46513e33aa91c296e26342d88df6bcb739b453c3 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 09:12:04 +1000 Subject: [PATCH 14/55] fix: allow components in webhook edits --- typings/index.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index cff96e468a23..aab51e98948b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -3827,7 +3827,10 @@ declare module 'discord.js' { reason?: string; } - type WebhookEditMessageOptions = Pick; + type WebhookEditMessageOptions = Pick< + WebhookMessageOptions, + 'content' | 'embeds' | 'files' | 'allowedMentions' | 'components' + >; interface WebhookMessageOptions extends Omit { username?: string; From 77d9398cec10af1106ee538c9009e3b972a41aa2 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 09:17:29 +1000 Subject: [PATCH 15/55] feat: typings for new response type methods --- src/structures/interfaces/InteractionResponses.js | 2 +- typings/index.d.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index fa4169cbfb1d..79effb99d0dc 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -191,7 +191,7 @@ class InteractionResponses { * Updates the message to which the button was attached * @param {string|APIMessage|MessageAdditions} content The content for the reply * @param {WebhookEditMessageOptions} [options] Additional options for the reply - * @returns {Promise} + * @returns {Promise} * @example * // Remove the buttons from the message * * interaction.reply("A button was clicked", { components: [] }) diff --git a/typings/index.d.ts b/typings/index.d.ts index aab51e98948b..42db9cde32ef 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -341,6 +341,11 @@ declare module 'discord.js' { public followUp(content: string, options?: InteractionReplyOptions): Promise; public reply(content: string | APIMessage | InteractionReplyOptions | MessageAdditions): Promise; public reply(content: string, options?: InteractionReplyOptions): Promise; + public deferUpdate(): Promise; + public update( + content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], + ): Promise; + public update(content: string, options?: WebhookEditMessageOptions): Promise; } export class CategoryChannel extends GuildChannel { From 310ff7a9f9cae182e2b919759f66f3245d707db3 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 09:18:56 +1000 Subject: [PATCH 16/55] fix: ordering --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 42db9cde32ef..d82a18f4ffbb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -329,6 +329,7 @@ declare module 'discord.js' { public replied: boolean; public webhook: WebhookClient; public defer(ephemeral?: boolean): Promise; + public deferUpdate(): Promise; public deleteReply(): Promise; public editReply( content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], @@ -341,7 +342,6 @@ declare module 'discord.js' { public followUp(content: string, options?: InteractionReplyOptions): Promise; public reply(content: string | APIMessage | InteractionReplyOptions | MessageAdditions): Promise; public reply(content: string, options?: InteractionReplyOptions): Promise; - public deferUpdate(): Promise; public update( content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], ): Promise; From 24224427bba9ad501ce399d42be84233a4db2327 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 09:54:24 +1000 Subject: [PATCH 17/55] feat: collector for components for channels --- src/index.js | 3 +- .../ChannelComponentInteractionCollector.js | 123 ++++++++++++++++++ src/structures/DMChannel.js | 2 + src/structures/Message.js | 12 +- ...> MessageComponentInteractionCollector.js} | 6 +- src/structures/TextChannel.js | 2 + src/structures/interfaces/TextBasedChannel.js | 42 ++++++ 7 files changed, 180 insertions(+), 10 deletions(-) create mode 100644 src/structures/ChannelComponentInteractionCollector.js rename src/structures/{ComponentInteractionCollector.js => MessageComponentInteractionCollector.js} (96%) diff --git a/src/index.js b/src/index.js index 4ab7eb51677c..fde5ac82c5f8 100644 --- a/src/index.js +++ b/src/index.js @@ -71,6 +71,7 @@ module.exports = { BaseMessageComponent: require('./structures/BaseMessageComponent'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), + ChannelComponentInteractionCollector: require('./structures/ChannelComponentInteractionCollector'), ClientApplication: require('./structures/ClientApplication'), get ClientUser() { // This is a getter so that it properly extends any custom User class @@ -79,7 +80,6 @@ module.exports = { Collector: require('./structures/interfaces/Collector'), CommandInteraction: require('./structures/CommandInteraction'), ComponentInteraction: require('./structures/ComponentInteraction'), - ComponentInteractionCollector: require('./structures/ComponentInteractionCollector'), DMChannel: require('./structures/DMChannel'), Emoji: require('./structures/Emoji'), Guild: require('./structures/Guild'), @@ -99,6 +99,7 @@ module.exports = { MessageAttachment: require('./structures/MessageAttachment'), MessageButton: require('./structures/MessageButton'), MessageCollector: require('./structures/MessageCollector'), + MessageComponentInteractionCollector: require('./structures/MessageComponentInteractionCollector'), MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), MessageReaction: require('./structures/MessageReaction'), diff --git a/src/structures/ChannelComponentInteractionCollector.js b/src/structures/ChannelComponentInteractionCollector.js new file mode 100644 index 000000000000..2954d56b81ac --- /dev/null +++ b/src/structures/ChannelComponentInteractionCollector.js @@ -0,0 +1,123 @@ +'use strict'; + +const Collector = require('./interfaces/Collector'); +const Collection = require('../util/Collection'); +const { Events } = require('../util/Constants'); + +/** + * Collects interaction on message buttons. + * Will automatically stop if the message (`'messageDelete'`), + * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. + * @extends {Collector} + */ +class ChannelComponentInteractionCollector extends Collector { + /** + * @param {TextChannel|DMChannel|NewsChannel} channel The channel from which to collect button interactions + * @param {CollectorFilter} filter The filter to apply to this collector + * @param {ComponentInteractionCollectorOptions} [options={}] The options to apply to this collector + */ + constructor(channel, filter, options = {}) { + super(channel.client, filter, options); + + /** + * The message upon which to collect button interactions + * @type {TextChannel|DMChannel|NewsChannel} + */ + this.channel = channel; + + /** + * The users which have interacted to buttons on this message + * @type {Collection} + */ + this.users = new Collection(); + + /** + * The total number of interactions collected + * @type {number} + */ + this.total = 0; + + this.empty = this.empty.bind(this); + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + + this.client.incrementMaxListeners(); + this.client.on(Events.INTERACTION_CREATE, this.handleCollect); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); + + this.once('end', () => { + this.client.removeListener(Events.INTERACTION_CREATE, this.handleCollect); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); + this.client.decrementMaxListeners(); + }); + + this.on('collect', interaction => { + this.total++; + this.users.set(interaction.user.id, interaction.user); + }); + } + + /** + * Handles an incoming interaction for possible collection. + * @param {Interaction} interaction The interaction to possibly collect + * @returns {?Snowflake|string} + * @private + */ + collect(interaction) { + /** + * Emitted whenever a reaction is collected. + * @event TextChannelComponentInteractionCollector#collect + * @param {ComponentInteraction} interaction The reaction that was collected + */ + if (!interaction.isComponent()) return null; + + if (interaction.channel.id !== this.channel.id) return null; + + return interaction.id; + } + + /** + * Empties this reaction collector. + */ + empty() { + this.total = 0; + this.collected.clear(); + this.users.clear(); + this.checkEnd(); + } + + get endReason() { + if (this.options.max && this.total >= this.options.max) return 'limit'; + if (this.options.maxComponents && this.collected.size >= this.options.maxComponents) return 'componentLimit'; + if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; + return null; + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.message.channel.id) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (this.message.guild && guild.id === this.message.guild.id) { + this.stop('guildDelete'); + } + } +} + +module.exports = ChannelComponentInteractionCollector; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 48075974ab85..f5c9328d9560 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -91,6 +91,8 @@ class DMChannel extends Channel { get typingCount() {} createMessageCollector() {} awaitMessages() {} + createComponentInteractionCollector() {} + awaitComponentInteractions() {} // Doesn't work on DM channels; bulkDelete() {} } diff --git a/src/structures/Message.js b/src/structures/Message.js index 9ca9c52c9bbe..d5a66e711921 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -4,8 +4,8 @@ const APIMessage = require('./APIMessage'); const Base = require('./Base'); const BaseMessageComponent = require('./BaseMessageComponent'); const ClientApplication = require('./ClientApplication'); -const ComponentInteractionCollector = require('./ComponentInteractionCollector'); const MessageAttachment = require('./MessageAttachment'); +const MessageComponentInteractionCollector = require('./MessageComponentInteractionCollector'); const Embed = require('./MessageEmbed'); const Mentions = require('./MessageMentions'); const ReactionCollector = require('./ReactionCollector'); @@ -421,7 +421,7 @@ class Message extends Base { * Creates a button interaction collector. * @param {CollectorFilter} filter The filter to apply * @param {ComponentInteractionCollectorOptions} [options={}] Options to send to the collector - * @returns {ComponentInteractionCollector} + * @returns {MessageComponentInteractionCollector} * @example * // Create a button interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; @@ -430,7 +430,7 @@ class Message extends Base { * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ createComponentInteractionCollector(filter, options = {}) { - return new ComponentInteractionCollector(this, filter, options); + return new MessageComponentInteractionCollector(this, filter, options); } /** @@ -440,15 +440,15 @@ class Message extends Base { */ /** - * Similar to createComponentInteractionCollector but in promise form. + * Similar to createMessageComponentInteractionCollector but in promise form. * Resolves with a collection of interactions that pass the specified filter. * @param {CollectorFilter} filter The filter function to use - * @param {AwaitComponentInteractionsOptions} [options={}] Optional options to pass to the internal collector + * @param {AwaitComponentInteractionOptions} [options={}] Optional options to pass to the internal collector * @returns {Promise>} * @example * // Create a button interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; - * message.awaitComponentInteraction(filter, { time: 15000 }) + * message.awaitComponentInteractions(filter, { time: 15000 }) * .then(collected => console.log(`Collected ${collected.size} interactions`)) * .catch(console.error); */ diff --git a/src/structures/ComponentInteractionCollector.js b/src/structures/MessageComponentInteractionCollector.js similarity index 96% rename from src/structures/ComponentInteractionCollector.js rename to src/structures/MessageComponentInteractionCollector.js index 9d3dd2679c23..e4e839ad9294 100644 --- a/src/structures/ComponentInteractionCollector.js +++ b/src/structures/MessageComponentInteractionCollector.js @@ -17,7 +17,7 @@ const { Events } = require('../util/Constants'); * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. * @extends {Collector} */ -class ComponentInteractionCollector extends Collector { +class MessageComponentInteractionCollector extends Collector { /** * @param {Message} message The message upon which to collect button interactions * @param {CollectorFilter} filter The filter to apply to this collector @@ -78,7 +78,7 @@ class ComponentInteractionCollector extends Collector { collect(interaction) { /** * Emitted whenever a reaction is collected. - * @event ComponentInteractionCollector#collect + * @event MessageComponentInteractionCollector#collect * @param {ComponentInteraction} interaction The reaction that was collected */ if (!interaction.isComponent()) return null; @@ -142,4 +142,4 @@ class ComponentInteractionCollector extends Collector { } } -module.exports = ComponentInteractionCollector; +module.exports = MessageComponentInteractionCollector; diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 5c8e984f4144..b39e731f5534 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -157,6 +157,8 @@ class TextChannel extends GuildChannel { get typingCount() {} createMessageCollector() {} awaitMessages() {} + createComponentInteractionCollector() {} + awaitComponentInteractions() {} bulkDelete() {} } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 56f35305a4b8..5600e6ff0a1e 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -6,6 +6,7 @@ const APIMessage = require('../APIMessage'); const SnowflakeUtil = require('../../util/SnowflakeUtil'); const Collection = require('../../util/Collection'); const { RangeError, TypeError } = require('../../errors'); +const ChannelComponentInteractionCollector = require('../ChannelComponentInteractionCollector'); /** * Interface for classes that have text-channel-like features. @@ -315,6 +316,45 @@ class TextBasedChannel { }); } + /** + * Creates a button interaction collector. + * @param {CollectorFilter} filter The filter to apply + * @param {ComponentInteractionCollectorOptions} [options={}] Options to send to the collector + * @returns {ChannelComponentInteractionCollector} + * @example + * // Create a button interaction collector + * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; + * const collector = channel.createComponentInteractionCollector(filter, { time: 15000 }); + * collector.on('collect', i => console.log(`Collected ${i.customID}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createComponentInteractionCollector(filter, options = {}) { + return new ChannelComponentInteractionCollector(this, filter, options); + } + + /** + * Similar to createComponentInteractionCollector but in promise form. + * Resolves with a collection of interactions that pass the specified filter. + * @param {CollectorFilter} filter The filter function to use + * @param {AwaitComponentInteractionOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise>} + * @example + * // Create a button interaction collector + * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; + * channel.awaitComponentInteraction(filter, { time: 15000 }) + * .then(collected => console.log(`Collected ${collected.size} interactions`)) + * .catch(console.error); + */ + awaitComponentInteractions(filter, options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createComponentInteractionCollector(filter, options); + collector.once('end', (interactions, reason) => { + if (options.errors && options.errors.includes(reason)) reject(interactions); + else resolve(interactions); + }); + }); + } + /** * Bulk deletes given messages that are newer than two weeks. * @param {Collection|MessageResolvable[]|number} messages @@ -379,6 +419,8 @@ class TextBasedChannel { 'typingCount', 'createMessageCollector', 'awaitMessages', + 'createComponentInteractionCollector', + 'awaitComponentInteractions', ); } for (const prop of props) { From 8d3417af8b8c971e1b374b59b77e8611378cf469 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 10:09:05 +1000 Subject: [PATCH 18/55] feat: typings for component collectors --- .../ChannelComponentInteractionCollector.js | 22 ++++++- .../MessageComponentInteractionCollector.js | 20 ++++++- typings/index.d.ts | 59 +++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) diff --git a/src/structures/ChannelComponentInteractionCollector.js b/src/structures/ChannelComponentInteractionCollector.js index 2954d56b81ac..0ddf106a32fd 100644 --- a/src/structures/ChannelComponentInteractionCollector.js +++ b/src/structures/ChannelComponentInteractionCollector.js @@ -67,9 +67,9 @@ class ChannelComponentInteractionCollector extends Collector { */ collect(interaction) { /** - * Emitted whenever a reaction is collected. - * @event TextChannelComponentInteractionCollector#collect - * @param {ComponentInteraction} interaction The reaction that was collected + * Emitted whenever a interaction is collected. + * @event ChannelComponentInteractionCollector#collect + * @param {Interaction} interaction The interaction that was collected */ if (!interaction.isComponent()) return null; @@ -78,6 +78,22 @@ class ChannelComponentInteractionCollector extends Collector { return interaction.id; } + /** + * Handles an interaction for possible disposal. + * @param {Interaction} interaction The interaction that could be disposed of + * @returns {?Snowflake} + */ + dispose(interaction) { + /** + * Emitted whenever an interaction is disposed of. + * @event ChannelComponentInteractionCollector#dispose + * @param {Interaction} interaction The interaction that was disposed of + */ + if (!interaction.isComponent()) return null; + + return interaction.channel.id === this.channel.id ? interaction.id : null; + } + /** * Empties this reaction collector. */ diff --git a/src/structures/MessageComponentInteractionCollector.js b/src/structures/MessageComponentInteractionCollector.js index e4e839ad9294..31c26c94bd1d 100644 --- a/src/structures/MessageComponentInteractionCollector.js +++ b/src/structures/MessageComponentInteractionCollector.js @@ -77,9 +77,9 @@ class MessageComponentInteractionCollector extends Collector { */ collect(interaction) { /** - * Emitted whenever a reaction is collected. + * Emitted whenever a interaction is collected. * @event MessageComponentInteractionCollector#collect - * @param {ComponentInteraction} interaction The reaction that was collected + * @param {Interaction} interaction The interaction that was collected */ if (!interaction.isComponent()) return null; @@ -88,6 +88,22 @@ class MessageComponentInteractionCollector extends Collector { return interaction.id; } + /** + * Handles an interaction for possible disposal. + * @param {Interaction} interaction The interaction that could be disposed of + * @returns {?Snowflake} + */ + dispose(interaction) { + /** + * Emitted whenever an interaction is disposed of. + * @event MessageComponentInteractionCollector#dispose + * @param {Interaction} interaction The interaction that was disposed of + */ + if (!interaction.isComponent()) return null; + + return interaction.message.id === this.message.id ? interaction.id : null; + } + /** * Empties this reaction collector. */ diff --git a/typings/index.d.ts b/typings/index.d.ts index d82a18f4ffbb..9f2b3fe0e60e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -368,6 +368,24 @@ declare module 'discord.js' { public toString(): string; } + export class ChannelComponentInteractionCollector extends Collector { + constructor( + channel: TextChannel | DMChannel | NewsChannel, + filter: CollectorFilter<[ComponentInteraction]>, + options?: ComponentInteractionCollectorOptions, + ); + private _handleChannelDeletion(channel: GuildChannel): void; + private _handleGuildDeletion(guild: Guild): void; + + public channel: Channel; + public readonly endReason: string | null; + public options: ComponentInteractionCollectorOptions; + public received: number; + + public collect(interaction: ComponentInteraction): Snowflake; + public dispose(interaction: ComponentInteraction): Snowflake; + } + export class Client extends BaseClient { constructor(options: ClientOptions); private actions: unknown; @@ -1245,6 +1263,10 @@ declare module 'discord.js' { filter: CollectorFilter<[MessageReaction, User]>, options?: ReactionCollectorOptions, ): ReactionCollector; + public createComponentInteractionCollector( + filter: CollectorFilter<[ComponentInteraction]>, + options?: AwaitComponentInteractionsOptions, + ): MessageComponentInteractionCollector; public delete(): Promise; public edit( content: string | null | MessageEditOptions | MessageEmbed | APIMessage | MessageAttachment | MessageAttachment[], @@ -1343,6 +1365,25 @@ declare module 'discord.js' { public dispose(message: Message): Snowflake; } + export class MessageComponentInteractionCollector extends Collector { + constructor( + message: Message, + filter: CollectorFilter<[ComponentInteraction]>, + options?: ComponentInteractionCollectorOptions, + ); + private _handleMessageDeletion(message: Message): void; + private _handleChannelDeletion(channel: GuildChannel): void; + private _handleGuildDeletion(guild: Guild): void; + + public message: Message; + public readonly endReason: string | null; + public options: ComponentInteractionCollectorOptions; + public received: number; + + public collect(interaction: ComponentInteractionCollectorOptions): Snowflake; + public dispose(interaction: ComponentInteractionCollectorOptions): Snowflake; + } + export class MessageEmbed { constructor(data?: MessageEmbed | MessageEmbedOptions); public author: MessageEmbedAuthor | null; @@ -2424,6 +2465,10 @@ declare module 'discord.js' { readonly lastPinAt: Date | null; typing: boolean; typingCount: number; + awaitComponentInteractions( + filter: CollectorFilter<[ComponentInteraction]>, + options?: AwaitComponentInteractionsOptions, + ): Promise>; awaitMessages( filter: CollectorFilter<[Message]>, options?: AwaitMessagesOptions, @@ -2432,6 +2477,10 @@ declare module 'discord.js' { messages: Collection | readonly MessageResolvable[] | number, filterOld?: boolean, ): Promise>; + createComponentInteractionCollector( + filter: CollectorFilter<[ComponentInteraction]>, + options?: ComponentInteractionCollectorOptions, + ): ChannelComponentInteractionCollector; createMessageCollector(filter: CollectorFilter<[Message]>, options?: MessageCollectorOptions): MessageCollector; startTyping(count?: number): Promise; stopTyping(force?: boolean): void; @@ -2638,6 +2687,10 @@ declare module 'discord.js' { new?: any; } + interface AwaitComponentInteractionsOptions extends ComponentInteractionCollectorOptions { + errors?: string[]; + } + interface AwaitMessagesOptions extends MessageCollectorOptions { errors?: string[]; } @@ -2857,6 +2910,12 @@ declare module 'discord.js' { role?: Role | RawRole; } + interface ComponentInteractionCollectorOptions extends CollectorOptions { + max?: number; + maxComponents?: number; + maxUsers?: number; + } + interface CrosspostedChannel { channelID: Snowflake; guildID: Snowflake; From b1be9c688567c7c282f6956ba048c076d447ec23 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 14:00:43 +1000 Subject: [PATCH 19/55] fix: missing ComponentInteraction#componentType prop, plus docs --- src/structures/BaseMessageComponent.js | 10 +++++++++- src/structures/ComponentInteraction.js | 17 +++++++++++++++++ src/structures/MessageButton.js | 8 ++++++++ typings/index.d.ts | 3 ++- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 603baa4b7833..2837ec13f38c 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -25,13 +25,21 @@ class BaseMessageComponent { */ /** - * Data that resolves to give a MessageComponent object. This can be: + * Data that can be resolved to give a MessageComponent object. This can be: * * A MessageComponentOptions object * * A MessageActionRow * * A MessageButton * @typedef {MessageComponentOptions|MessageComponent} MessageComponentResolvable */ + /** + * Data that can be resolved to a MessageComponentType. This can be: + * * {@link MessageComponentType} + * * string + * * number + * @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable + */ + /** * @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component */ diff --git a/src/structures/ComponentInteraction.js b/src/structures/ComponentInteraction.js index 28e8ed7438d5..53bb0f1a8631 100644 --- a/src/structures/ComponentInteraction.js +++ b/src/structures/ComponentInteraction.js @@ -3,6 +3,7 @@ const Interaction = require('./Interaction'); const InteractionResponses = require('./interfaces/InteractionResponses'); const WebhookClient = require('../client/WebhookClient'); +const { MessageComponentTypes } = require('../util/Constants'); /** * Represents a message button interaction. @@ -25,6 +26,12 @@ class ComponentInteraction extends Interaction { */ this.customID = data.data.custom_id; + /** + * The type of component that was interacted with + * @type {string} + */ + this.componentType = ComponentInteraction.resolveType(data.data.component_type); + /** * Whether the reply to this interaction has been deferred * @type {boolean} @@ -44,6 +51,16 @@ class ComponentInteraction extends Interaction { this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options); } + /** + * Resolves the type of a MessageComponent + * @param {MessageComponentTypeResolvable} type The type to resolve + * @returns {MessageComponentType} + * @private + */ + static resolveType(type) { + return typeof type === 'string' ? type : MessageComponentTypes[type]; + } + // These are here only for documentation purposes - they are implemented by InteractionResponses /* eslint-disable no-empty-function */ defer() {} diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 93881795b989..8d3e7a912fa5 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -121,6 +121,14 @@ class MessageButton extends BaseMessageComponent { return this; } + /** + * Data that can be resolved to a MessageButtonStyle. This can be + * * {@link MessageButtonStyle} + * * string + * * number + * @typedef {string|number|MessageButtonStyle} MessageButtonStyleResolvable + */ + /** * Resolves the style of a MessageButton * @param {MessageButtonStyleResolvable} style The style to resolve diff --git a/typings/index.d.ts b/typings/index.d.ts index 9f2b3fe0e60e..ad573d63daf5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -291,7 +291,7 @@ declare module 'discord.js' { public type: MessageComponentType | null; public setType(type: MessageComponentTypeResolvable): this; private static create(data: MessageComponentOptions): MessageComponent; - private static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; + public static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; private static transform(component: MessageComponentResolvable): object; } @@ -346,6 +346,7 @@ declare module 'discord.js' { content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], ): Promise; public update(content: string, options?: WebhookEditMessageOptions): Promise; + public static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; } export class CategoryChannel extends GuildChannel { From e2d2cde1177f9c199f85901f0ecffda32c98265c Mon Sep 17 00:00:00 2001 From: monbrey Date: Fri, 28 May 2021 12:27:59 +1000 Subject: [PATCH 20/55] fix: Apply suggestions from code review Corrects copy-pasted jsdoc Co-authored-by: BannerBomb --- src/structures/MessageActionRow.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index 9ef424fcf9f1..9eef677b7526 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -23,7 +23,7 @@ class MessageActionRow extends BaseMessageComponent { /** * Adds a component to the row (max 5). * @param {MessageComponent|MessageComponentOptions} component The component to add - * @returns {MessageEmbed} + * @returns {MessageActionRow} */ addComponent(component) { return this.addComponents({ ...component }); @@ -32,7 +32,7 @@ class MessageActionRow extends BaseMessageComponent { /** * Adds components to the row (max 5). * @param {...(MessageComponent[]|MessageComponentOptions[])} components The components to add - * @returns {MessageEmbed} + * @returns {MessageActionRow} */ addComponents(...components) { this.components.push(...components.map(BaseMessageComponent.create)); From 10d9f1dd23c94f81064d2c899a66dce3ffb2d310 Mon Sep 17 00:00:00 2001 From: monbrey Date: Fri, 28 May 2021 14:02:33 +1000 Subject: [PATCH 21/55] fix: apply suggestions from code review Co-authored-by: BannerBomb --- src/structures/MessageButton.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 8d3e7a912fa5..81c719187f76 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -73,8 +73,8 @@ class MessageButton extends BaseMessageComponent { /** * Sets the interactive status of the button - * @param {boolean} disabled Whether this emoji should be disabled - * @returns {MessageEmbed} + * @param {boolean} disabled Whether this button should be disabled + * @returns {MessageButton} */ setDisabled(disabled) { this.disabled = disabled; From 641feef9ea8fe19e1ffdf052e53b8bb82b36bd35 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 14:58:01 +1000 Subject: [PATCH 22/55] refactor: big rename, consolidate the two collectors --- .../websocket/handlers/INTERACTION_CREATE.js | 4 +- src/index.js | 3 +- .../ChannelComponentInteractionCollector.js | 139 ------------------ src/structures/DMChannel.js | 4 +- src/structures/Interaction.js | 2 +- src/structures/Message.js | 24 +-- ...tion.js => MessageComponentInteraction.js} | 14 +- .../MessageComponentInteractionCollector.js | 36 +++-- src/structures/TextChannel.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 28 ++-- src/util/Structures.js | 4 +- typings/index.d.ts | 120 +++++++-------- 12 files changed, 115 insertions(+), 267 deletions(-) delete mode 100644 src/structures/ChannelComponentInteractionCollector.js rename src/structures/{ComponentInteraction.js => MessageComponentInteraction.js} (80%) diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js index dce9b2f1eba5..57a507d51742 100644 --- a/src/client/websocket/handlers/INTERACTION_CREATE.js +++ b/src/client/websocket/handlers/INTERACTION_CREATE.js @@ -15,9 +15,9 @@ module.exports = (client, { d: data }) => { } case InteractionTypes.MESSAGE_COMPONENT: { if (!Structures) Structures = require('../../../util/Structures'); - const ComponentInteraction = Structures.get('ComponentInteraction'); + const MessageComponentInteraction = Structures.get('MessageComponentInteraction'); - interaction = new ComponentInteraction(client, data); + interaction = new MessageComponentInteraction(client, data); break; } default: diff --git a/src/index.js b/src/index.js index fde5ac82c5f8..e8ebb000abdc 100644 --- a/src/index.js +++ b/src/index.js @@ -71,7 +71,6 @@ module.exports = { BaseMessageComponent: require('./structures/BaseMessageComponent'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), - ChannelComponentInteractionCollector: require('./structures/ChannelComponentInteractionCollector'), ClientApplication: require('./structures/ClientApplication'), get ClientUser() { // This is a getter so that it properly extends any custom User class @@ -79,7 +78,6 @@ module.exports = { }, Collector: require('./structures/interfaces/Collector'), CommandInteraction: require('./structures/CommandInteraction'), - ComponentInteraction: require('./structures/ComponentInteraction'), DMChannel: require('./structures/DMChannel'), Emoji: require('./structures/Emoji'), Guild: require('./structures/Guild'), @@ -99,6 +97,7 @@ module.exports = { MessageAttachment: require('./structures/MessageAttachment'), MessageButton: require('./structures/MessageButton'), MessageCollector: require('./structures/MessageCollector'), + MessageComponentInteraction: require('./structures/MessageComponentInteraction'), MessageComponentInteractionCollector: require('./structures/MessageComponentInteractionCollector'), MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), diff --git a/src/structures/ChannelComponentInteractionCollector.js b/src/structures/ChannelComponentInteractionCollector.js deleted file mode 100644 index 0ddf106a32fd..000000000000 --- a/src/structures/ChannelComponentInteractionCollector.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict'; - -const Collector = require('./interfaces/Collector'); -const Collection = require('../util/Collection'); -const { Events } = require('../util/Constants'); - -/** - * Collects interaction on message buttons. - * Will automatically stop if the message (`'messageDelete'`), - * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. - * @extends {Collector} - */ -class ChannelComponentInteractionCollector extends Collector { - /** - * @param {TextChannel|DMChannel|NewsChannel} channel The channel from which to collect button interactions - * @param {CollectorFilter} filter The filter to apply to this collector - * @param {ComponentInteractionCollectorOptions} [options={}] The options to apply to this collector - */ - constructor(channel, filter, options = {}) { - super(channel.client, filter, options); - - /** - * The message upon which to collect button interactions - * @type {TextChannel|DMChannel|NewsChannel} - */ - this.channel = channel; - - /** - * The users which have interacted to buttons on this message - * @type {Collection} - */ - this.users = new Collection(); - - /** - * The total number of interactions collected - * @type {number} - */ - this.total = 0; - - this.empty = this.empty.bind(this); - this._handleChannelDeletion = this._handleChannelDeletion.bind(this); - this._handleGuildDeletion = this._handleGuildDeletion.bind(this); - - this.client.incrementMaxListeners(); - this.client.on(Events.INTERACTION_CREATE, this.handleCollect); - this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); - this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); - - this.once('end', () => { - this.client.removeListener(Events.INTERACTION_CREATE, this.handleCollect); - this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); - this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); - this.client.decrementMaxListeners(); - }); - - this.on('collect', interaction => { - this.total++; - this.users.set(interaction.user.id, interaction.user); - }); - } - - /** - * Handles an incoming interaction for possible collection. - * @param {Interaction} interaction The interaction to possibly collect - * @returns {?Snowflake|string} - * @private - */ - collect(interaction) { - /** - * Emitted whenever a interaction is collected. - * @event ChannelComponentInteractionCollector#collect - * @param {Interaction} interaction The interaction that was collected - */ - if (!interaction.isComponent()) return null; - - if (interaction.channel.id !== this.channel.id) return null; - - return interaction.id; - } - - /** - * Handles an interaction for possible disposal. - * @param {Interaction} interaction The interaction that could be disposed of - * @returns {?Snowflake} - */ - dispose(interaction) { - /** - * Emitted whenever an interaction is disposed of. - * @event ChannelComponentInteractionCollector#dispose - * @param {Interaction} interaction The interaction that was disposed of - */ - if (!interaction.isComponent()) return null; - - return interaction.channel.id === this.channel.id ? interaction.id : null; - } - - /** - * Empties this reaction collector. - */ - empty() { - this.total = 0; - this.collected.clear(); - this.users.clear(); - this.checkEnd(); - } - - get endReason() { - if (this.options.max && this.total >= this.options.max) return 'limit'; - if (this.options.maxComponents && this.collected.size >= this.options.maxComponents) return 'componentLimit'; - if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; - return null; - } - - /** - * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. - * @private - * @param {GuildChannel} channel The channel that was deleted - * @returns {void} - */ - _handleChannelDeletion(channel) { - if (channel.id === this.message.channel.id) { - this.stop('channelDelete'); - } - } - - /** - * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. - * @private - * @param {Guild} guild The guild that was deleted - * @returns {void} - */ - _handleGuildDeletion(guild) { - if (this.message.guild && guild.id === this.message.guild.id) { - this.stop('guildDelete'); - } - } -} - -module.exports = ChannelComponentInteractionCollector; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index f5c9328d9560..41ea724f2eea 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -91,8 +91,8 @@ class DMChannel extends Channel { get typingCount() {} createMessageCollector() {} awaitMessages() {} - createComponentInteractionCollector() {} - awaitComponentInteractions() {} + createMessageComponentInteractionCollector() {} + awaitMessageComponentInteractions() {} // Doesn't work on DM channels; bulkDelete() {} } diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 46dd29aea5f2..2d34f6079f3d 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -117,7 +117,7 @@ class Interaction extends Base { * Indicates whether this interaction is a component interaction. * @returns {boolean} */ - isComponent() { + isMessageComponent() { return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT; } } diff --git a/src/structures/Message.js b/src/structures/Message.js index d5a66e711921..7794b4cba2da 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -418,24 +418,24 @@ class Message extends Base { } /** - * Creates a button interaction collector. + * Creates a message component interaction collector. * @param {CollectorFilter} filter The filter to apply - * @param {ComponentInteractionCollectorOptions} [options={}] Options to send to the collector + * @param {MessageComponentInteractionCollectorOptions} [options={}] Options to send to the collector * @returns {MessageComponentInteractionCollector} * @example - * // Create a button interaction collector + * // Create a message component interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; - * const collector = message.createComponentInteractionCollector(filter, { time: 15000 }); + * const collector = message.createMessageComponentInteractionCollector(filter, { time: 15000 }); * collector.on('collect', i => console.log(`Collected ${i.customID}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ - createComponentInteractionCollector(filter, options = {}) { + createMessageComponentInteractionCollector(filter, options = {}) { return new MessageComponentInteractionCollector(this, filter, options); } /** * An object containing the same properties as CollectorOptions, but a few more: - * @typedef {ComponentInteractionCollectorOptions} AwaitComponentInteractionsOptions + * @typedef {MessageComponentInteractionCollectorOptions} AwaitMessageComponentInteractionsOptions * @property {string[]} [errors] Stop/end reasons that cause the promise to reject */ @@ -443,18 +443,18 @@ class Message extends Base { * Similar to createMessageComponentInteractionCollector but in promise form. * Resolves with a collection of interactions that pass the specified filter. * @param {CollectorFilter} filter The filter function to use - * @param {AwaitComponentInteractionOptions} [options={}] Optional options to pass to the internal collector - * @returns {Promise>} + * @param {AwaitMessageComponentInteractionsOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise>} * @example - * // Create a button interaction collector + * // Create a message component interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; - * message.awaitComponentInteractions(filter, { time: 15000 }) + * message.awaitMessageComponentInteractions(filter, { time: 15000 }) * .then(collected => console.log(`Collected ${collected.size} interactions`)) * .catch(console.error); */ - awaitComponentInteractions(filter, options = {}) { + awaitMessageComponentInteractions(filter, options = {}) { return new Promise((resolve, reject) => { - const collector = this.createComponentInteractionCollector(filter, options); + const collector = this.createMessageComponentInteractionCollector(filter, options); collector.once('end', (interactions, reason) => { if (options.errors && options.errors.includes(reason)) reject(interactions); else resolve(interactions); diff --git a/src/structures/ComponentInteraction.js b/src/structures/MessageComponentInteraction.js similarity index 80% rename from src/structures/ComponentInteraction.js rename to src/structures/MessageComponentInteraction.js index 53bb0f1a8631..cd11660feedb 100644 --- a/src/structures/ComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -6,22 +6,22 @@ const WebhookClient = require('../client/WebhookClient'); const { MessageComponentTypes } = require('../util/Constants'); /** - * Represents a message button interaction. + * Represents a message component interaction. * @extends {Interaction} */ -class ComponentInteraction extends Interaction { +class MessageComponentInteraction extends Interaction { // eslint-disable-next-line no-useless-constructor constructor(client, data) { super(client, data); /** - * The message to which the button was attached + * The message to which the component was attached * @type {?Message|Object} */ this.message = data.message ? this.channel?.messages.add(data.message) ?? data.message : null; /** - * The custom ID of the button which was clicked + * The custom ID of the bucomponenttton which was clicked * @type {string} */ this.customID = data.data.custom_id; @@ -30,7 +30,7 @@ class ComponentInteraction extends Interaction { * The type of component that was interacted with * @type {string} */ - this.componentType = ComponentInteraction.resolveType(data.data.component_type); + this.componentType = MessageComponentInteraction.resolveType(data.data.component_type); /** * Whether the reply to this interaction has been deferred @@ -73,6 +73,6 @@ class ComponentInteraction extends Interaction { update() {} } -InteractionResponses.applyToClass(ComponentInteraction); +InteractionResponses.applyToClass(MessageComponentInteraction); -module.exports = ComponentInteraction; +module.exports = MessageComponentInteraction; diff --git a/src/structures/MessageComponentInteractionCollector.js b/src/structures/MessageComponentInteractionCollector.js index 31c26c94bd1d..11feb0fdb981 100644 --- a/src/structures/MessageComponentInteractionCollector.js +++ b/src/structures/MessageComponentInteractionCollector.js @@ -5,32 +5,32 @@ const Collection = require('../util/Collection'); const { Events } = require('../util/Constants'); /** - * @typedef {CollectorOptions} ComponentInteractionCollectorOptions + * @typedef {CollectorOptions} MessageComponentInteractionCollectorOptions * @property {number} max The maximum total amount of interactions to collect - * @property {number} maxComponents The maximum number of buttons to collect + * @property {number} maxComponents The maximum number of components to collect * @property {number} maxUsers The maximum number of users to interact */ /** - * Collects interaction on message buttons. + * Collects interaction on message components. * Will automatically stop if the message (`'messageDelete'`), * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. * @extends {Collector} */ class MessageComponentInteractionCollector extends Collector { /** - * @param {Message} message The message upon which to collect button interactions + * @param {Message|Channel} source The source from which to collect message component interactions * @param {CollectorFilter} filter The filter to apply to this collector - * @param {ComponentInteractionCollectorOptions} [options={}] The options to apply to this collector + * @param {MessageComponentInteractionCollectorOptions} [options={}] The options to apply to this collector */ - constructor(message, filter, options = {}) { - super(message.client, filter, options); + constructor(source, filter, options = {}) { + super(source.client, filter, options); /** - * The message upon which to collect button interactions - * @type {Message} + * The source from which to collect message component interactions + * @type {Message|Channel} */ - this.message = message; + this.source = source; /** * The users which have interacted to buttons on this message @@ -51,7 +51,9 @@ class MessageComponentInteractionCollector extends Collector { this.client.incrementMaxListeners(); this.client.on(Events.INTERACTION_CREATE, this.handleCollect); - this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + + if (this.source instanceof require('./Message')) this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); @@ -81,11 +83,15 @@ class MessageComponentInteractionCollector extends Collector { * @event MessageComponentInteractionCollector#collect * @param {Interaction} interaction The interaction that was collected */ - if (!interaction.isComponent()) return null; + if (!interaction.isMessageComponent()) return null; - if (interaction.message.id !== this.message.id) return null; + if (this.source instanceof require('./Message')) { + return interaction.message.id === this.source.id ? interaction.id : null; + } else if (this.source instanceof require('./Channel')) { + return interaction.channel.id === this.source.id ? interaction.id : null; + } - return interaction.id; + return null; } /** @@ -99,7 +105,7 @@ class MessageComponentInteractionCollector extends Collector { * @event MessageComponentInteractionCollector#dispose * @param {Interaction} interaction The interaction that was disposed of */ - if (!interaction.isComponent()) return null; + if (!interaction.isMessageComponent()) return null; return interaction.message.id === this.message.id ? interaction.id : null; } diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index b39e731f5534..9eb86b13c1b7 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -157,8 +157,8 @@ class TextChannel extends GuildChannel { get typingCount() {} createMessageCollector() {} awaitMessages() {} - createComponentInteractionCollector() {} - awaitComponentInteractions() {} + createMessageComponentInteractionCollector() {} + awaitMessageComponentInteractions() {} bulkDelete() {} } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 5600e6ff0a1e..09322b8149d1 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -6,7 +6,7 @@ const APIMessage = require('../APIMessage'); const SnowflakeUtil = require('../../util/SnowflakeUtil'); const Collection = require('../../util/Collection'); const { RangeError, TypeError } = require('../../errors'); -const ChannelComponentInteractionCollector = require('../ChannelComponentInteractionCollector'); +const MessageComponentInteractionCollector = require('../MessageComponentInteractionCollector'); /** * Interface for classes that have text-channel-like features. @@ -319,35 +319,35 @@ class TextBasedChannel { /** * Creates a button interaction collector. * @param {CollectorFilter} filter The filter to apply - * @param {ComponentInteractionCollectorOptions} [options={}] Options to send to the collector - * @returns {ChannelComponentInteractionCollector} + * @param {MessageComponentInteractionCollectorOptions} [options={}] Options to send to the collector + * @returns {MessageComponentInteractionCollector} * @example * // Create a button interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; - * const collector = channel.createComponentInteractionCollector(filter, { time: 15000 }); + * const collector = channel.createMessageComponentInteractionCollector(filter, { time: 15000 }); * collector.on('collect', i => console.log(`Collected ${i.customID}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ - createComponentInteractionCollector(filter, options = {}) { - return new ChannelComponentInteractionCollector(this, filter, options); + createMessageComponentInteractionCollector(filter, options = {}) { + return new MessageComponentInteractionCollector(this, filter, options); } /** - * Similar to createComponentInteractionCollector but in promise form. + * Similar to createMessageComponentInteractionCollector but in promise form. * Resolves with a collection of interactions that pass the specified filter. * @param {CollectorFilter} filter The filter function to use - * @param {AwaitComponentInteractionOptions} [options={}] Optional options to pass to the internal collector - * @returns {Promise>} + * @param {AwaitMessageComponentInteractionsOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise>} * @example * // Create a button interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; - * channel.awaitComponentInteraction(filter, { time: 15000 }) + * channel.awaitMessageComponentInteractions(filter, { time: 15000 }) * .then(collected => console.log(`Collected ${collected.size} interactions`)) * .catch(console.error); */ - awaitComponentInteractions(filter, options = {}) { + awaitMessageComponentInteractions(filter, options = {}) { return new Promise((resolve, reject) => { - const collector = this.createComponentInteractionCollector(filter, options); + const collector = this.createMessageComponentInteractionCollector(filter, options); collector.once('end', (interactions, reason) => { if (options.errors && options.errors.includes(reason)) reject(interactions); else resolve(interactions); @@ -419,8 +419,8 @@ class TextBasedChannel { 'typingCount', 'createMessageCollector', 'awaitMessages', - 'createComponentInteractionCollector', - 'awaitComponentInteractions', + 'createMessageComponentInteractionCollector', + 'awaitMessageComponentInteractions', ); } for (const prop of props) { diff --git a/src/util/Structures.js b/src/util/Structures.js index 95cf8a620358..bc6d2e06106e 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -20,7 +20,7 @@ * * **`Role`** * * **`User`** * * **`CommandInteraction`** - * * **`ComponentInteraction`** + * * **`MessageComponentInteraction`** * @typedef {string} ExtendableStructure */ @@ -112,7 +112,7 @@ const structures = { Role: require('../structures/Role'), User: require('../structures/User'), CommandInteraction: require('../structures/CommandInteraction'), - ComponentInteraction: require('../structures/ComponentInteraction'), + MessageComponentInteraction: require('../structures/MessageComponentInteraction'), }; module.exports = Structures; diff --git a/typings/index.d.ts b/typings/index.d.ts index ad573d63daf5..ff89cc118867 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -322,33 +322,6 @@ declare module 'discord.js' { public static resolve(bit?: BitFieldResolvable): number | bigint; } - export class ComponentInteraction extends Interaction { - public customID: string; - public deferred: boolean; - public message: Message | RawMessage; - public replied: boolean; - public webhook: WebhookClient; - public defer(ephemeral?: boolean): Promise; - public deferUpdate(): Promise; - public deleteReply(): Promise; - public editReply( - content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], - ): Promise; - public editReply(content: string, options?: WebhookEditMessageOptions): Promise; - public fetchReply(): Promise; - public followUp( - content: string | APIMessage | InteractionReplyOptions | MessageAdditions, - ): Promise; - public followUp(content: string, options?: InteractionReplyOptions): Promise; - public reply(content: string | APIMessage | InteractionReplyOptions | MessageAdditions): Promise; - public reply(content: string, options?: InteractionReplyOptions): Promise; - public update( - content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], - ): Promise; - public update(content: string, options?: WebhookEditMessageOptions): Promise; - public static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; - } - export class CategoryChannel extends GuildChannel { public readonly children: Collection; public type: 'category'; @@ -369,24 +342,6 @@ declare module 'discord.js' { public toString(): string; } - export class ChannelComponentInteractionCollector extends Collector { - constructor( - channel: TextChannel | DMChannel | NewsChannel, - filter: CollectorFilter<[ComponentInteraction]>, - options?: ComponentInteractionCollectorOptions, - ); - private _handleChannelDeletion(channel: GuildChannel): void; - private _handleGuildDeletion(guild: Guild): void; - - public channel: Channel; - public readonly endReason: string | null; - public options: ComponentInteractionCollectorOptions; - public received: number; - - public collect(interaction: ComponentInteraction): Snowflake; - public dispose(interaction: ComponentInteraction): Snowflake; - } - export class Client extends BaseClient { constructor(options: ClientOptions); private actions: unknown; @@ -1187,7 +1142,7 @@ declare module 'discord.js' { public user: User; public version: number; public isCommand(): this is CommandInteraction; - public isComponent(): this is ComponentInteraction; + public isMessageComponent(): this is MessageComponentInteraction; } export class Invite extends Base { @@ -1264,9 +1219,9 @@ declare module 'discord.js' { filter: CollectorFilter<[MessageReaction, User]>, options?: ReactionCollectorOptions, ): ReactionCollector; - public createComponentInteractionCollector( - filter: CollectorFilter<[ComponentInteraction]>, - options?: AwaitComponentInteractionsOptions, + public createMessageComponentInteractionCollector( + filter: CollectorFilter<[MessageComponentInteraction]>, + options?: AwaitMessageComponentInteractionsOptions, ): MessageComponentInteractionCollector; public delete(): Promise; public edit( @@ -1366,11 +1321,38 @@ declare module 'discord.js' { public dispose(message: Message): Snowflake; } + export class MessageComponentInteraction extends Interaction { + public customID: string; + public deferred: boolean; + public message: Message | RawMessage; + public replied: boolean; + public webhook: WebhookClient; + public defer(ephemeral?: boolean): Promise; + public deferUpdate(): Promise; + public deleteReply(): Promise; + public editReply( + content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], + ): Promise; + public editReply(content: string, options?: WebhookEditMessageOptions): Promise; + public fetchReply(): Promise; + public followUp( + content: string | APIMessage | InteractionReplyOptions | MessageAdditions, + ): Promise; + public followUp(content: string, options?: InteractionReplyOptions): Promise; + public reply(content: string | APIMessage | InteractionReplyOptions | MessageAdditions): Promise; + public reply(content: string, options?: InteractionReplyOptions): Promise; + public update( + content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], + ): Promise; + public update(content: string, options?: WebhookEditMessageOptions): Promise; + public static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; + } + export class MessageComponentInteractionCollector extends Collector { constructor( message: Message, - filter: CollectorFilter<[ComponentInteraction]>, - options?: ComponentInteractionCollectorOptions, + filter: CollectorFilter<[MessageComponentInteraction]>, + options?: MessageComponentInteractionCollectorOptions, ); private _handleMessageDeletion(message: Message): void; private _handleChannelDeletion(channel: GuildChannel): void; @@ -1378,11 +1360,11 @@ declare module 'discord.js' { public message: Message; public readonly endReason: string | null; - public options: ComponentInteractionCollectorOptions; + public options: MessageComponentInteractionCollectorOptions; public received: number; - public collect(interaction: ComponentInteractionCollectorOptions): Snowflake; - public dispose(interaction: ComponentInteractionCollectorOptions): Snowflake; + public collect(interaction: MessageComponentInteractionCollectorOptions): Snowflake; + public dispose(interaction: MessageComponentInteractionCollectorOptions): Snowflake; } export class MessageEmbed { @@ -2466,10 +2448,10 @@ declare module 'discord.js' { readonly lastPinAt: Date | null; typing: boolean; typingCount: number; - awaitComponentInteractions( - filter: CollectorFilter<[ComponentInteraction]>, - options?: AwaitComponentInteractionsOptions, - ): Promise>; + awaitMessageComponentInteractions( + filter: CollectorFilter<[MessageComponentInteraction]>, + options?: AwaitMessageComponentInteractionsOptions, + ): Promise>; awaitMessages( filter: CollectorFilter<[Message]>, options?: AwaitMessagesOptions, @@ -2478,10 +2460,10 @@ declare module 'discord.js' { messages: Collection | readonly MessageResolvable[] | number, filterOld?: boolean, ): Promise>; - createComponentInteractionCollector( - filter: CollectorFilter<[ComponentInteraction]>, - options?: ComponentInteractionCollectorOptions, - ): ChannelComponentInteractionCollector; + createMessageComponentInteractionCollector( + filter: CollectorFilter<[MessageComponentInteraction]>, + options?: MessageComponentInteractionCollectorOptions, + ): MessageComponentInteractionCollector; createMessageCollector(filter: CollectorFilter<[Message]>, options?: MessageCollectorOptions): MessageCollector; startTyping(count?: number): Promise; stopTyping(force?: boolean): void; @@ -2688,7 +2670,7 @@ declare module 'discord.js' { new?: any; } - interface AwaitComponentInteractionsOptions extends ComponentInteractionCollectorOptions { + interface AwaitMessageComponentInteractionsOptions extends MessageComponentInteractionCollectorOptions { errors?: string[]; } @@ -2911,12 +2893,6 @@ declare module 'discord.js' { role?: Role | RawRole; } - interface ComponentInteractionCollectorOptions extends CollectorOptions { - max?: number; - maxComponents?: number; - maxUsers?: number; - } - interface CrosspostedChannel { channelID: Snowflake; guildID: Snowflake; @@ -3378,6 +3354,12 @@ declare module 'discord.js' { type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton; + interface MessageComponentInteractionCollectorOptions extends CollectorOptions { + max?: number; + maxComponents?: number; + maxUsers?: number; + } + type MessageComponentOptions = BaseMessageComponentOptions | MessageActionRowOptions | MessageButtonOptions; type MessageComponentResolvable = MessageComponent | MessageComponentOptions; From 77e441918284f74c06fcd46c654cc72be660eb8b Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 15:23:16 +1000 Subject: [PATCH 23/55] refactor: toJSON methods for api transformation --- src/structures/APIMessage.js | 2 +- src/structures/BaseMessageComponent.js | 23 +---------------------- src/structures/MessageActionRow.js | 12 ++++++++++++ src/structures/MessageButton.js | 18 +++++++++++++++++- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index bcb5916a2ddd..57783f010584 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -155,7 +155,7 @@ class APIMessage { let components; if (this.options.components) { components = []; - components.push(...this.options.components.map(BaseMessageComponent.transform)); + components.push(...this.options.components.map(c => BaseMessageComponent.create(c).toJSON())); } let username; diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 2837ec13f38c..bc8a44ca42a3 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -1,6 +1,6 @@ 'use strict'; -const { MessageComponentTypes, MessageButtonStyles } = require('../util/Constants'); +const { MessageComponentTypes } = require('../util/Constants'); /** * Represents an interactive component of a Message. It should not be necessary to construct this directly. @@ -97,27 +97,6 @@ class BaseMessageComponent { static resolveType(type) { return typeof type === 'string' ? type : MessageComponentTypes[type]; } - - /** - * Transforms a MessageComponent object into something that can be used with the API. - * @param {MessageComponentResolvable} component The component to transform - * @returns {Object} - * @private - */ - static transform(component) { - const { type, components, label, customID, style, emoji, url, disabled } = component; - - return { - components: components?.map(BaseMessageComponent.transform), - custom_id: customID, - disabled, - emoji, - label, - style: typeof style === 'string' ? MessageButtonStyles[style] : style, - type: typeof type === 'string' ? MessageComponentTypes[type] : type, - url, - }; - } } module.exports = BaseMessageComponent; diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index 9eef677b7526..dd885fd259d4 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -1,6 +1,7 @@ 'use strict'; const BaseMessageComponent = require('./BaseMessageComponent'); +const { MessageComponentTypes } = require('../util/Constants'); /** * Represents an ActionRow containing message components. @@ -38,6 +39,17 @@ class MessageActionRow extends BaseMessageComponent { this.components.push(...components.map(BaseMessageComponent.create)); return this; } + + /** + * Transforms the action row to a plain object. + * @returns {Object} The raw data of this action row + */ + toJSON() { + return { + components: this.components.map(c => BaseMessageComponent.create(c).toJSON()), + type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type, + }; + } } module.exports = MessageActionRow; diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 81c719187f76..491c590c9be0 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -1,7 +1,7 @@ 'use strict'; const BaseMessageComponent = require('./BaseMessageComponent'); -const { MessageButtonStyles } = require('../util/Constants.js'); +const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants.js'); const Util = require('../util/Util'); class MessageButton extends BaseMessageComponent { /** @@ -121,6 +121,22 @@ class MessageButton extends BaseMessageComponent { return this; } + /** + * Transforms the button to a plain object. + * @returns {Object} The raw data of this button + */ + toJSON() { + return { + custom_id: this.customID, + disabled: this.disabled, + emoji: this.emoji, + label: this.label, + style: typeof this.style === 'string' ? MessageButtonStyles[this.style] : this.style, + type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type, + url: this.url, + }; + } + /** * Data that can be resolved to a MessageButtonStyle. This can be * * {@link MessageButtonStyle} From 2d0c21538ccf16d36033282b70af8983feeb25ed Mon Sep 17 00:00:00 2001 From: monbrey Date: Fri, 28 May 2021 15:24:48 +1000 Subject: [PATCH 24/55] fix: typo Co-authored-by: Shubham Parihar --- src/structures/MessageComponentInteraction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index cd11660feedb..381019d2a4f8 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -21,7 +21,7 @@ class MessageComponentInteraction extends Interaction { this.message = data.message ? this.channel?.messages.add(data.message) ?? data.message : null; /** - * The custom ID of the bucomponenttton which was clicked + * The custom ID of the component which was clicked * @type {string} */ this.customID = data.data.custom_id; From a2f0e5932cbfd07a2d72d3262b422e1523b6ed0b Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 16:56:39 +1000 Subject: [PATCH 25/55] fix: missing toJSON typings --- typings/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index ff89cc118867..27a3cc4b7074 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1265,6 +1265,7 @@ declare module 'discord.js' { public components: MessageComponent[]; public addComponent(component: MessageComponentOptions): this; public addComponents(...components: MessageComponentOptions[] | MessageComponentOptions[][]): this; + public toJSON(): object; } export class MessageAttachment { @@ -1301,6 +1302,7 @@ declare module 'discord.js' { public setStyle(style: MessageButtonStyleResolvable): this; public setURL(url: string): this; private static resolveStyle(style: MessageButtonStyleResolvable): MessageButtonStyle; + public toJSON(): object; } export class MessageCollector extends Collector { From eccc1489cb81d972bcb72c273b0fc47baba33295 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 18:41:31 +1000 Subject: [PATCH 26/55] feat: handle invalid component types --- src/structures/BaseMessageComponent.js | 15 ++++++++++++--- src/structures/Message.js | 4 ++-- src/structures/MessageActionRow.js | 6 +++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index bc8a44ca42a3..65e055561c73 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -1,6 +1,7 @@ 'use strict'; -const { MessageComponentTypes } = require('../util/Constants'); +const { TypeError } = require('../errors'); +const { MessageComponentTypes, Events } = require('../util/Constants'); /** * Represents an interactive component of a Message. It should not be necessary to construct this directly. @@ -64,10 +65,12 @@ class BaseMessageComponent { /** * Constructs a MessageComponent based on the type of the incoming data * @param {MessageComponentOptions} data Data for a MessageComponent - * @returns {MessageComponent} + * @param {Client|WebhookClient} [client] Client constructing this component + * @param {boolean} [skipValidation=false] Whether or not to validate the component type + * @returns {?MessageComponent} * @private */ - static create(data) { + static create(data, client, skipValidation = false) { let component; let type = data.type; @@ -84,6 +87,12 @@ class BaseMessageComponent { component = new MessageButton(data); break; } + default: + if (client) { + client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`); + } else if (!skipValidation) { + throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType'); + } } return component; } diff --git a/src/structures/Message.js b/src/structures/Message.js index 7794b4cba2da..dd1b45ffe9c4 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -129,7 +129,7 @@ class Message extends Base { * A list of components in the message e.g. ActionRows, Buttons * @type {MessageComponent[]} */ - this.components = (data.components || []).map(BaseMessageComponent.create); + this.components = (data.components || []).map(c => BaseMessageComponent.create(c, this.client)); /** * A collection of attachments in the message - e.g. Pictures - mapped by their ID @@ -290,7 +290,7 @@ class Message extends Base { if ('tts' in data) this.tts = data.tts; if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(e, true)); else this.embeds = this.embeds.slice(); - if ('components' in data) this.components = data.components.map(BaseMessageComponent.create); + if ('components' in data) this.components = data.components.map(c => BaseMessageComponent.create(c, this.clien)); else this.components = this.components.slice(); if ('attachments' in data) { diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index dd885fd259d4..d8d8ae529806 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -18,7 +18,7 @@ class MessageActionRow extends BaseMessageComponent { constructor(data = {}) { super({ type: 'ACTION_ROW' }); - this.components = (data.components ?? []).map(BaseMessageComponent.create); + this.components = (data.components ?? []).map(c => BaseMessageComponent.create(c, null, true)); } /** @@ -36,7 +36,7 @@ class MessageActionRow extends BaseMessageComponent { * @returns {MessageActionRow} */ addComponents(...components) { - this.components.push(...components.map(BaseMessageComponent.create)); + this.components.push(...components.map(c => BaseMessageComponent.create(c, null, true))); return this; } @@ -46,7 +46,7 @@ class MessageActionRow extends BaseMessageComponent { */ toJSON() { return { - components: this.components.map(c => BaseMessageComponent.create(c).toJSON()), + components: this.components.map(c => c.toJSON()), type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type, }; } From b5db7bd067e46453e1f8ee289a4f47a9a8dcf8f6 Mon Sep 17 00:00:00 2001 From: monbrey Date: Fri, 28 May 2021 19:39:45 +1000 Subject: [PATCH 27/55] fix: typo Co-authored-by: Arechi <22101241+Arechii@users.noreply.github.com> --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index dd1b45ffe9c4..cd9accc1eb08 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -290,7 +290,7 @@ class Message extends Base { if ('tts' in data) this.tts = data.tts; if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(e, true)); else this.embeds = this.embeds.slice(); - if ('components' in data) this.components = data.components.map(c => BaseMessageComponent.create(c, this.clien)); + if ('components' in data) this.components = data.components.map(c => BaseMessageComponent.create(c, this.client)); else this.components = this.components.slice(); if ('attachments' in data) { From 7b609acfa1b95397c84c6708bdfa83c87d3f552c Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 28 May 2021 22:14:56 +1000 Subject: [PATCH 28/55] fix: rmeove types from Options definitions --- typings/index.d.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 27a3cc4b7074..84b7b061b5c7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -3326,7 +3326,6 @@ declare module 'discord.js' { type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; interface MessageActionRowOptions extends BaseMessageComponentOptions { - type: 'ACTION_ROW' | MessageComponentTypes.ACTION_ROW; components?: MessageComponentResolvable[]; } @@ -3341,7 +3340,6 @@ declare module 'discord.js' { emoji?: RawEmoji; label?: string; style: MessageButtonStyleResolvable; - type: 'BUTTON' | MessageComponentTypes.BUTTON; url?: string; } From d45e6128b6a339315da6713d570d1de13b59c2d3 Mon Sep 17 00:00:00 2001 From: monbrey Date: Sat, 29 May 2021 07:56:08 +1000 Subject: [PATCH 29/55] fix: apply suggestions from code review Co-authored-by: SpaceEEC Co-authored-by: Vlad Frangu --- src/structures/MessageActionRow.js | 1 + src/structures/MessageButton.js | 1 + src/structures/MessageComponentInteraction.js | 1 + .../MessageComponentInteractionCollector.js | 4 ++-- src/structures/interfaces/InteractionResponses.js | 8 ++++---- typings/index.d.ts | 12 ++++++------ 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index d8d8ae529806..561350c39a60 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -5,6 +5,7 @@ const { MessageComponentTypes } = require('../util/Constants'); /** * Represents an ActionRow containing message components. + * @extends {BaseMessageComponent} */ class MessageActionRow extends BaseMessageComponent { /** diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 491c590c9be0..aa501c427280 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -3,6 +3,7 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants.js'); const Util = require('../util/Util'); + class MessageButton extends BaseMessageComponent { /** * @typedef {BaseMessageComponentOptions} MessageButtonOptions diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index 381019d2a4f8..9974bec08022 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -8,6 +8,7 @@ const { MessageComponentTypes } = require('../util/Constants'); /** * Represents a message component interaction. * @extends {Interaction} + * @implements {InteractionResponses} */ class MessageComponentInteraction extends Interaction { // eslint-disable-next-line no-useless-constructor diff --git a/src/structures/MessageComponentInteractionCollector.js b/src/structures/MessageComponentInteractionCollector.js index 11feb0fdb981..61ff33a0270c 100644 --- a/src/structures/MessageComponentInteractionCollector.js +++ b/src/structures/MessageComponentInteractionCollector.js @@ -33,7 +33,7 @@ class MessageComponentInteractionCollector extends Collector { this.source = source; /** - * The users which have interacted to buttons on this message + * The users which have interacted to buttons on this collector * @type {Collection} */ this.users = new Collection(); @@ -111,7 +111,7 @@ class MessageComponentInteractionCollector extends Collector { } /** - * Empties this reaction collector. + * Empties this message component collector. */ empty() { this.total = 0; diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index 79effb99d0dc..b1062353996a 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -173,7 +173,7 @@ class InteractionResponses { * @returns {Promise} * @example * // Defer to update the button to a loading state - * interaction.defer() + * interaction.deferUpdate() * .then(console.log) * .catch(console.error); */ @@ -188,13 +188,13 @@ class InteractionResponses { } /** - * Updates the message to which the button was attached + * Updates the original message whose button was pressed * @param {string|APIMessage|MessageAdditions} content The content for the reply * @param {WebhookEditMessageOptions} [options] Additional options for the reply * @returns {Promise} * @example - * // Remove the buttons from the message * - * interaction.reply("A button was clicked", { components: [] }) + * // Remove the buttons from the message + * interaction.update("A button was clicked", { components: [] }) * .then(console.log) * .catch(console.error); */ diff --git a/typings/index.d.ts b/typings/index.d.ts index 84b7b061b5c7..ced2c7c229b6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1301,8 +1301,8 @@ declare module 'discord.js' { public setLabel(label: string): this; public setStyle(style: MessageButtonStyleResolvable): this; public setURL(url: string): this; - private static resolveStyle(style: MessageButtonStyleResolvable): MessageButtonStyle; public toJSON(): object; + private static resolveStyle(style: MessageButtonStyleResolvable): MessageButtonStyle; } export class MessageCollector extends Collector { @@ -1352,7 +1352,7 @@ declare module 'discord.js' { export class MessageComponentInteractionCollector extends Collector { constructor( - message: Message, + source: Channel | Message, filter: CollectorFilter<[MessageComponentInteraction]>, options?: MessageComponentInteractionCollectorOptions, ); @@ -1360,13 +1360,13 @@ declare module 'discord.js' { private _handleChannelDeletion(channel: GuildChannel): void; private _handleGuildDeletion(guild: Guild): void; - public message: Message; + public source: Channel | Message; public readonly endReason: string | null; public options: MessageComponentInteractionCollectorOptions; - public received: number; + public total: number; - public collect(interaction: MessageComponentInteractionCollectorOptions): Snowflake; - public dispose(interaction: MessageComponentInteractionCollectorOptions): Snowflake; + public collect(interaction: Interaction): Snowflake; + public dispose(interaction: Interaction): Snowflake; } export class MessageEmbed { From cc7ec7db372d2ca4667683774d644ae66ed2b605 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Sat, 29 May 2021 08:16:23 +1000 Subject: [PATCH 30/55] fix: apply suggestions from code review --- src/structures/BaseMessageComponent.js | 1 + src/structures/CommandInteraction.js | 1 + src/structures/MessageActionRow.js | 11 +----- src/structures/MessageButton.js | 8 +++-- src/structures/MessageComponentInteraction.js | 1 - .../MessageComponentInteractionCollector.js | 35 ++++++++++++------- typings/index.d.ts | 12 +++---- 7 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 65e055561c73..1df51a82254a 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -5,6 +5,7 @@ const { MessageComponentTypes, Events } = require('../util/Constants'); /** * Represents an interactive component of a Message. It should not be necessary to construct this directly. + * See {@link MessageComponent} */ class BaseMessageComponent { /** diff --git a/src/structures/CommandInteraction.js b/src/structures/CommandInteraction.js index 66f6e5ef856f..7888d5c7e550 100644 --- a/src/structures/CommandInteraction.js +++ b/src/structures/CommandInteraction.js @@ -9,6 +9,7 @@ const { ApplicationCommandOptionTypes } = require('../util/Constants'); /** * Represents a command interaction. * @extends {Interaction} + * @implements {InteractionResponses} */ class CommandInteraction extends Interaction { constructor(client, data) { diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index 561350c39a60..1caa2299d4fb 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -22,15 +22,6 @@ class MessageActionRow extends BaseMessageComponent { this.components = (data.components ?? []).map(c => BaseMessageComponent.create(c, null, true)); } - /** - * Adds a component to the row (max 5). - * @param {MessageComponent|MessageComponentOptions} component The component to add - * @returns {MessageActionRow} - */ - addComponent(component) { - return this.addComponents({ ...component }); - } - /** * Adds components to the row (max 5). * @param {...(MessageComponent[]|MessageComponentOptions[])} components The components to add @@ -48,7 +39,7 @@ class MessageActionRow extends BaseMessageComponent { toJSON() { return { components: this.components.map(c => c.toJSON()), - type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type, + type: MessageComponentTypes[this.type], }; } } diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index aa501c427280..b4fd41a8f214 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -4,6 +4,10 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants.js'); const Util = require('../util/Util'); +/** + * Represents a Button message component. + * @extends {BaseMessageComponent} + */ class MessageButton extends BaseMessageComponent { /** * @typedef {BaseMessageComponentOptions} MessageButtonOptions @@ -132,8 +136,8 @@ class MessageButton extends BaseMessageComponent { disabled: this.disabled, emoji: this.emoji, label: this.label, - style: typeof this.style === 'string' ? MessageButtonStyles[this.style] : this.style, - type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type, + style: MessageButtonStyles[this.style], + type: MessageComponentTypes[this.type], url: this.url, }; } diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index 9974bec08022..711f7c282522 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -11,7 +11,6 @@ const { MessageComponentTypes } = require('../util/Constants'); * @implements {InteractionResponses} */ class MessageComponentInteraction extends Interaction { - // eslint-disable-next-line no-useless-constructor constructor(client, data) { super(client, data); diff --git a/src/structures/MessageComponentInteractionCollector.js b/src/structures/MessageComponentInteractionCollector.js index 61ff33a0270c..b686f6dbb627 100644 --- a/src/structures/MessageComponentInteractionCollector.js +++ b/src/structures/MessageComponentInteractionCollector.js @@ -19,7 +19,8 @@ const { Events } = require('../util/Constants'); */ class MessageComponentInteractionCollector extends Collector { /** - * @param {Message|Channel} source The source from which to collect message component interactions + * @param {Message|TextChannel|DMChannel|NewsChannel} source + * The source from which to collect message component interactions * @param {CollectorFilter} filter The filter to apply to this collector * @param {MessageComponentInteractionCollectorOptions} [options={}] The options to apply to this collector */ @@ -27,10 +28,16 @@ class MessageComponentInteractionCollector extends Collector { super(source.client, filter, options); /** - * The source from which to collect message component interactions - * @type {Message|Channel} + * The message from which to collect message component interactions, if provided + * @type {Message} */ - this.source = source; + this.message = source instanceof require('./Message') ? source : null; + + /** + * The source channel from which to collect message component interactions + * @type {TextChannel|DMChannel|NewsChannel} + */ + this.channel = this.message ? this.message.channel : source; /** * The users which have interacted to buttons on this collector @@ -52,14 +59,16 @@ class MessageComponentInteractionCollector extends Collector { this.client.incrementMaxListeners(); this.client.on(Events.INTERACTION_CREATE, this.handleCollect); - if (this.source instanceof require('./Message')) this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + if (this.message) this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); this.once('end', () => { this.client.removeListener(Events.INTERACTION_CREATE, this.handleCollect); - this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion); + + if (this.message) this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); this.client.decrementMaxListeners(); @@ -85,13 +94,11 @@ class MessageComponentInteractionCollector extends Collector { */ if (!interaction.isMessageComponent()) return null; - if (this.source instanceof require('./Message')) { - return interaction.message.id === this.source.id ? interaction.id : null; - } else if (this.source instanceof require('./Channel')) { - return interaction.channel.id === this.source.id ? interaction.id : null; + if (this.message) { + return interaction.message.id === this.message.id ? interaction.id : null; } - return null; + return interaction.channel.id === this.channel.id ? interaction.id : null; } /** @@ -107,7 +114,11 @@ class MessageComponentInteractionCollector extends Collector { */ if (!interaction.isMessageComponent()) return null; - return interaction.message.id === this.message.id ? interaction.id : null; + if (this.message) { + return interaction.message.id === this.message.id ? interaction.id : null; + } + + return interaction.channel.id === this.channel.id ? interaction.id : null; } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index ced2c7c229b6..5ae0a73e32fc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -291,14 +291,10 @@ declare module 'discord.js' { public type: MessageComponentType | null; public setType(type: MessageComponentTypeResolvable): this; private static create(data: MessageComponentOptions): MessageComponent; - public static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; + private static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; private static transform(component: MessageComponentResolvable): object; } - interface BaseMessageComponentOptions { - type?: MessageComponentType | MessageComponentTypes; - } - class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { public broadcast: VoiceBroadcast; } @@ -1263,7 +1259,6 @@ declare module 'discord.js' { constructor(data?: MessageActionRow | MessageActionRowOptions); public type: 'ACTION_ROW'; public components: MessageComponent[]; - public addComponent(component: MessageComponentOptions): this; public addComponents(...components: MessageComponentOptions[] | MessageComponentOptions[][]): this; public toJSON(): object; } @@ -1364,6 +1359,7 @@ declare module 'discord.js' { public readonly endReason: string | null; public options: MessageComponentInteractionCollectorOptions; public total: number; + public users: Collection; public collect(interaction: Interaction): Snowflake; public dispose(interaction: Interaction): Snowflake; @@ -2693,6 +2689,10 @@ declare module 'discord.js' { type Base64String = string; + interface BaseMessageComponentOptions { + type?: MessageComponentType | MessageComponentTypes; + } + type BitFieldResolvable = | RecursiveReadonlyArray>> | T From 81b3311f86ef56d6ed3f6ee43faabbf02c8b57c1 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Sat, 29 May 2021 09:06:14 +1000 Subject: [PATCH 31/55] fix: use webhook.send directly --- src/structures/interfaces/InteractionResponses.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index b1062353996a..487ab661ec40 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -157,15 +157,7 @@ class InteractionResponses { * @returns {Promise} */ async followUp(content, options) { - const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); - const { data, files } = await apiMessage.resolveData().resolveFiles(); - - const raw = await this.client.api.webhooks(this.applicationID, this.token).post({ - data, - files, - }); - - return this.channel?.messages.add(raw) ?? raw; + await this.webhook.send(content, options); } /** From fdf8ea75dfd1f395372aecf5ac31b56ff1e0c98f Mon Sep 17 00:00:00 2001 From: Monbrey Date: Sun, 30 May 2021 00:40:02 +1000 Subject: [PATCH 32/55] fix: addComponents array bug --- src/structures/MessageActionRow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index 1caa2299d4fb..ca81c2e2c207 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -28,7 +28,7 @@ class MessageActionRow extends BaseMessageComponent { * @returns {MessageActionRow} */ addComponents(...components) { - this.components.push(...components.map(c => BaseMessageComponent.create(c, null, true))); + this.components.push(...components.flat(2).map(c => BaseMessageComponent.create(c, null, true))); return this; } From aa8450234713cd644d6d5378ede9997089e45902 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Sun, 30 May 2021 00:59:24 +1000 Subject: [PATCH 33/55] feat: use emoji parsing --- src/structures/MessageButton.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index b4fd41a8f214..c816ad936cbb 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -92,7 +92,8 @@ class MessageButton extends BaseMessageComponent { * @returns {MessageButton} */ setEmoji(emoji) { - this.emoji = typeof emoji === 'string' ? { name: emoji } : emoji; + emoji = Util.parseEmoji(emoji); + this.emoji = emoji; return this; } From 4659c2db055cca1685dd483032e3ea0600cfb077 Mon Sep 17 00:00:00 2001 From: monbrey Date: Sun, 30 May 2021 07:35:01 +1000 Subject: [PATCH 34/55] fix: apply suggestion from code review Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> --- src/structures/MessageButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index c816ad936cbb..fc1ea4dcd309 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -92,7 +92,7 @@ class MessageButton extends BaseMessageComponent { * @returns {MessageButton} */ setEmoji(emoji) { - emoji = Util.parseEmoji(emoji); + this.emoji = Util.parseEmoji(emoji); this.emoji = emoji; return this; } From 1c549a17bca432ac4abad62426347df8d76018ba Mon Sep 17 00:00:00 2001 From: Monbrey Date: Sun, 30 May 2021 07:54:11 +1000 Subject: [PATCH 35/55] fix: simplify components mapping --- src/structures/APIMessage.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 57783f010584..dee07e0572c3 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -152,11 +152,7 @@ class APIMessage { } const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON()); - let components; - if (this.options.components) { - components = []; - components.push(...this.options.components.map(c => BaseMessageComponent.create(c).toJSON())); - } + let components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON()); let username; let avatarURL; From be380f2ab019fe93093227e55038f00bb347a720 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Sun, 30 May 2021 08:19:25 +1000 Subject: [PATCH 36/55] fix: stringify emoji input --- src/structures/MessageButton.js | 3 +-- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index fc1ea4dcd309..7acb7c643ee8 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -92,8 +92,7 @@ class MessageButton extends BaseMessageComponent { * @returns {MessageButton} */ setEmoji(emoji) { - this.emoji = Util.parseEmoji(emoji); - this.emoji = emoji; + this.emoji = Util.parseEmoji(`${emoji}`); return this; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 5ae0a73e32fc..d780d93c7d49 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1285,7 +1285,7 @@ declare module 'discord.js' { constructor(data?: MessageButton | MessageButtonOptions); public customID: string | null; public disabled: boolean; - public emoji: unknown | null; + public emoji: string | RawEmoji | null; public label: string | null; public style: MessageButtonStyle | null; public type: 'BUTTON'; From d6270b123d6847b4177329b9e73b5d6570b39ce5 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Sun, 30 May 2021 18:24:55 +1000 Subject: [PATCH 37/55] feat: stricter typings for components --- typings/index.d.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index d780d93c7d49..eb943bab120d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -292,7 +292,6 @@ declare module 'discord.js' { public setType(type: MessageComponentTypeResolvable): this; private static create(data: MessageComponentOptions): MessageComponent; private static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; - private static transform(component: MessageComponentResolvable): object; } class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { @@ -1178,7 +1177,7 @@ declare module 'discord.js' { public author: User; public channel: TextChannel | DMChannel | NewsChannel; public readonly cleanContent: string; - public components: MessageComponent[]; + public components: MessageActionRow[]; public content: string; public readonly createdAt: Date; public createdTimestamp: number; @@ -1258,8 +1257,8 @@ declare module 'discord.js' { export class MessageActionRow extends BaseMessageComponent { constructor(data?: MessageActionRow | MessageActionRowOptions); public type: 'ACTION_ROW'; - public components: MessageComponent[]; - public addComponents(...components: MessageComponentOptions[] | MessageComponentOptions[][]): this; + public components: MessageActionRowComponent[]; + public addComponents(...components: MessageActionRowComponent[] | MessageActionRowComponent[][]): this; public toJSON(): object; } @@ -3325,8 +3324,12 @@ declare module 'discord.js' { type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; + type MessageActionRowComponent = MessageButton; + + type MessageActionRowComponentOptions = MessageButtonOptions; + interface MessageActionRowOptions extends BaseMessageComponentOptions { - components?: MessageComponentResolvable[]; + components?: MessageActionRowComponent[] | MessageActionRowComponentOptions[]; } interface MessageActivity { @@ -3362,8 +3365,6 @@ declare module 'discord.js' { type MessageComponentOptions = BaseMessageComponentOptions | MessageActionRowOptions | MessageButtonOptions; - type MessageComponentResolvable = MessageComponent | MessageComponentOptions; - type MessageComponentType = keyof typeof MessageComponentTypes; type MessageComponentTypeResolvable = MessageComponentType | MessageComponentTypes; @@ -3376,7 +3377,7 @@ declare module 'discord.js' { files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; - components?: MessageComponentResolvable[]; + components?: MessageActionRow[] | MessageActionRowOptions[]; } interface MessageEmbedAuthor { @@ -3469,7 +3470,7 @@ declare module 'discord.js' { nonce?: string | number; content?: string; embed?: MessageEmbed | MessageEmbedOptions; - components?: MessageComponentResolvable[]; + components?: MessageActionRow[] | MessageActionRowOptions[]; allowedMentions?: MessageMentionOptions; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; From cd196a1c2819b62488a0d207d77578e119c51978 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Mon, 31 May 2021 08:42:29 +1000 Subject: [PATCH 38/55] fix: revert change to followUp, doesnt support ephem --- src/structures/interfaces/InteractionResponses.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index 487ab661ec40..b1062353996a 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -157,7 +157,15 @@ class InteractionResponses { * @returns {Promise} */ async followUp(content, options) { - await this.webhook.send(content, options); + const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); + const { data, files } = await apiMessage.resolveData().resolveFiles(); + + const raw = await this.client.api.webhooks(this.applicationID, this.token).post({ + data, + files, + }); + + return this.channel?.messages.add(raw) ?? raw; } /** From c1d9a4ba54362467b6528eabaa3e90814ada8642 Mon Sep 17 00:00:00 2001 From: monbrey Date: Tue, 1 Jun 2021 08:49:25 +1000 Subject: [PATCH 39/55] fix: apply suggestions from code review Co-authored-by: SpaceEEC --- src/structures/MessageComponentInteractionCollector.js | 2 +- typings/index.d.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/structures/MessageComponentInteractionCollector.js b/src/structures/MessageComponentInteractionCollector.js index b686f6dbb627..5d2fb50c1d18 100644 --- a/src/structures/MessageComponentInteractionCollector.js +++ b/src/structures/MessageComponentInteractionCollector.js @@ -29,7 +29,7 @@ class MessageComponentInteractionCollector extends Collector { /** * The message from which to collect message component interactions, if provided - * @type {Message} + * @type {?Message} */ this.message = source instanceof require('./Message') ? source : null; diff --git a/typings/index.d.ts b/typings/index.d.ts index eb943bab120d..838ec6eff7c7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1346,7 +1346,7 @@ declare module 'discord.js' { export class MessageComponentInteractionCollector extends Collector { constructor( - source: Channel | Message, + source: Message | TextChannel | NewsChannel | DMChannel, filter: CollectorFilter<[MessageComponentInteraction]>, options?: MessageComponentInteractionCollectorOptions, ); @@ -1354,8 +1354,9 @@ declare module 'discord.js' { private _handleChannelDeletion(channel: GuildChannel): void; private _handleGuildDeletion(guild: Guild): void; - public source: Channel | Message; + public channel: TextChannel | NewsChannel | DMChannel; public readonly endReason: string | null; + public message: Message | null; public options: MessageComponentInteractionCollectorOptions; public total: number; public users: Collection; From 0cb81a3613913bc845cb5adcfdf13f3e96744817 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Tue, 1 Jun 2021 09:01:09 +1000 Subject: [PATCH 40/55] fix(MessageButton): allow IDs in setEmoji --- src/structures/MessageButton.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 7acb7c643ee8..751a83ce8ecd 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -92,7 +92,8 @@ class MessageButton extends BaseMessageComponent { * @returns {MessageButton} */ setEmoji(emoji) { - this.emoji = Util.parseEmoji(`${emoji}`); + if (/^\d{17,19}$/.test(emoji)) this.emoji = { id: emoji }; + else this.emoji = Util.parseEmoji(`${emoji}`); return this; } From 6e691d46ef3323cb2ffba51632708b7ff5f299e1 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Tue, 1 Jun 2021 09:56:18 +1000 Subject: [PATCH 41/55] fix: align docs with typings, remove setType, add spliceComponents --- src/structures/BaseMessageComponent.js | 18 ------------ src/structures/Message.js | 4 +-- src/structures/MessageActionRow.js | 40 ++++++++++++++++++++++++-- typings/index.d.ts | 14 +++++++-- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 1df51a82254a..14b1910a45c9 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -26,14 +26,6 @@ class BaseMessageComponent { * @typedef {MessageActionRow|MessageButton} MessageComponent */ - /** - * Data that can be resolved to give a MessageComponent object. This can be: - * * A MessageComponentOptions object - * * A MessageActionRow - * * A MessageButton - * @typedef {MessageComponentOptions|MessageComponent} MessageComponentResolvable - */ - /** * Data that can be resolved to a MessageComponentType. This can be: * * {@link MessageComponentType} @@ -53,16 +45,6 @@ class BaseMessageComponent { this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null; } - /** - * Sets the type of this component - * @param {MessageComponentTypeResolvable} type The type of this component - * @returns {BaseMessageComponent} - */ - setType(type) { - this.type = BaseMessageComponent.resolveType(type); - return this; - } - /** * Constructs a MessageComponent based on the type of the incoming data * @param {MessageComponentOptions} data Data for a MessageComponent diff --git a/src/structures/Message.js b/src/structures/Message.js index cd9accc1eb08..4722158ce2bc 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -126,8 +126,8 @@ class Message extends Base { this.embeds = (data.embeds || []).map(e => new Embed(e, true)); /** - * A list of components in the message e.g. ActionRows, Buttons - * @type {MessageComponent[]} + * A list of MessageActionRows in the message + * @type {MessageActionRow[]} */ this.components = (data.components || []).map(c => BaseMessageComponent.create(c, this.client)); diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index ca81c2e2c207..4e6f6f794104 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -8,9 +8,29 @@ const { MessageComponentTypes } = require('../util/Constants'); * @extends {BaseMessageComponent} */ class MessageActionRow extends BaseMessageComponent { + /** + * Components that can be placed in a MessageActionRow + * * MessageButton + * @typedef {MessageButton} MessageActionRowComponent + */ + + /** + * Options for components that can be placed in a MessageActionRow + * * MessageButtonOptions + * @typedef {MessageButtonOptions} MessageActionRowComponentOptions + */ + + /** + * Data that can be resolved into a components that can be placed in a MessageActionRow + * * MessageActionRowComponent + * * MessageActionRowComponentOptions + * @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable + */ + /** * @typedef {BaseMessageComponentOptions} MessageActionRowOptions - * @property {MessageComponent[]|MessageComponentOptions[]} [components] The components to place in this ActionRow + * @property {MessageActionRowComponentResolvable[]} [components] + * The components to place in this ActionRow */ /** @@ -24,7 +44,7 @@ class MessageActionRow extends BaseMessageComponent { /** * Adds components to the row (max 5). - * @param {...(MessageComponent[]|MessageComponentOptions[])} components The components to add + * @param {...MessageActionRowComponentResolvable[]} components The components to add * @returns {MessageActionRow} */ addComponents(...components) { @@ -32,6 +52,22 @@ class MessageActionRow extends BaseMessageComponent { return this; } + /** + * Removes, replaces, and inserts components in the action row (max 25). + * @param {number} index The index to start at + * @param {number} deleteCount The number of components to remove + * @param {...MessageActionRowComponentResolvable[]} [components] The replacing components + * @returns {MessageSelectMenu} + */ + spliceComponents(index, deleteCount, ...components) { + this.components.splice( + index, + deleteCount, + ...components.flat(2).map(c => BaseMessageComponent.create(c, null, true)), + ); + return this; + } + /** * Transforms the action row to a plain object. * @returns {Object} The raw data of this action row diff --git a/typings/index.d.ts b/typings/index.d.ts index 838ec6eff7c7..f61cfe1e9061 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -289,7 +289,6 @@ declare module 'discord.js' { export class BaseMessageComponent { constructor(data?: BaseMessageComponent | BaseMessageComponentOptions); public type: MessageComponentType | null; - public setType(type: MessageComponentTypeResolvable): this; private static create(data: MessageComponentOptions): MessageComponent; private static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; } @@ -1258,7 +1257,14 @@ declare module 'discord.js' { constructor(data?: MessageActionRow | MessageActionRowOptions); public type: 'ACTION_ROW'; public components: MessageActionRowComponent[]; - public addComponents(...components: MessageActionRowComponent[] | MessageActionRowComponent[][]): this; + public addComponents( + ...components: MessageActionRowComponentResolvable[] | MessageActionRowComponentResolvable[][] + ): this; + public spliceComponents( + index: number, + deleteCount: number, + ...components: MessageActionRowComponentResolvable[] | MessageActionRowComponentResolvable[][] + ): this; public toJSON(): object; } @@ -3329,8 +3335,10 @@ declare module 'discord.js' { type MessageActionRowComponentOptions = MessageButtonOptions; + type MessageActionRowComponentResolvable = MessageActionRowComponent | MessageActionRowComponentOptions; + interface MessageActionRowOptions extends BaseMessageComponentOptions { - components?: MessageActionRowComponent[] | MessageActionRowComponentOptions[]; + components?: MessageActionRowComponentResolvable[]; } interface MessageActivity { From 9876decc1b83a844ae9fb2b77ab1d9d8575b3e5a Mon Sep 17 00:00:00 2001 From: monbrey Date: Tue, 1 Jun 2021 22:33:36 +1000 Subject: [PATCH 42/55] fix: apply suggestions from code review Co-authored-by: SpaceEEC --- typings/index.d.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index f61cfe1e9061..e78c17467b6e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1361,6 +1361,7 @@ declare module 'discord.js' { private _handleGuildDeletion(guild: Guild): void; public channel: TextChannel | NewsChannel | DMChannel; + public empty(): void; public readonly endReason: string | null; public message: Message | null; public options: MessageComponentInteractionCollectorOptions; @@ -1369,6 +1370,20 @@ declare module 'discord.js' { public collect(interaction: Interaction): Snowflake; public dispose(interaction: Interaction): Snowflake; + + public on(event: 'collect' | 'dispose', listener: (interaction: Interaction) => Awaited): this; + public on( + event: 'end', + listener: (collected: Collection, reason: string) => Awaited, + ): this; + public on(event: string, listener: (...args: any[]) => Awaited): this; + + public once(event: 'collect' | 'dispose', listener: (interaction: Interaction) => Awaited): this; + public once( + event: 'end', + listener: (collected: Collection, reason: string) => Awaited, + ): this; + public once(event: string, listener: (...args: any[]) => Awaited): this; } export class MessageEmbed { From b14168e1a4256eadd69250f23e8624272553e82a Mon Sep 17 00:00:00 2001 From: Monbrey Date: Tue, 1 Jun 2021 22:35:49 +1000 Subject: [PATCH 43/55] fix: suggestions from code review --- src/structures/MessageActionRow.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index 4e6f6f794104..8a628d5c26b2 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -39,6 +39,10 @@ class MessageActionRow extends BaseMessageComponent { constructor(data = {}) { super({ type: 'ACTION_ROW' }); + /** + * The components in this MessageActionRow + * @type {MessageActionRowComponent[]} + */ this.components = (data.components ?? []).map(c => BaseMessageComponent.create(c, null, true)); } @@ -53,7 +57,7 @@ class MessageActionRow extends BaseMessageComponent { } /** - * Removes, replaces, and inserts components in the action row (max 25). + * Removes, replaces, and inserts components in the action row. * @param {number} index The index to start at * @param {number} deleteCount The number of components to remove * @param {...MessageActionRowComponentResolvable[]} [components] The replacing components From a31cea9ee9848ca82af6b14a9804817e72b8efde Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 1 Jun 2021 17:52:03 +0200 Subject: [PATCH 44/55] fix: remove trailing whitespace --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index e78c17467b6e..e2242d452e73 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1370,7 +1370,6 @@ declare module 'discord.js' { public collect(interaction: Interaction): Snowflake; public dispose(interaction: Interaction): Snowflake; - public on(event: 'collect' | 'dispose', listener: (interaction: Interaction) => Awaited): this; public on( event: 'end', From 77d361632e60db87fb0828e3f6979ebf0355ab86 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 1 Jun 2021 17:56:04 +0200 Subject: [PATCH 45/55] fix: remove max mention from addComponents too --- src/structures/MessageActionRow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index 8a628d5c26b2..cf5a4a59fb97 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -47,7 +47,7 @@ class MessageActionRow extends BaseMessageComponent { } /** - * Adds components to the row (max 5). + * Adds components to the row. * @param {...MessageActionRowComponentResolvable[]} components The components to add * @returns {MessageActionRow} */ From 784bffc6c3123c910ac09537edc51fae1192ce3c Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 2 Jun 2021 08:31:48 +1000 Subject: [PATCH 46/55] types: apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antonio Román --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index e2242d452e73..bb7f372dca2e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1265,7 +1265,7 @@ declare module 'discord.js' { deleteCount: number, ...components: MessageActionRowComponentResolvable[] | MessageActionRowComponentResolvable[][] ): this; - public toJSON(): object; + public toJSON(): unknown; } export class MessageAttachment { @@ -1301,7 +1301,7 @@ declare module 'discord.js' { public setLabel(label: string): this; public setStyle(style: MessageButtonStyleResolvable): this; public setURL(url: string): this; - public toJSON(): object; + public toJSON(): unknown; private static resolveStyle(style: MessageButtonStyleResolvable): MessageButtonStyle; } From d4ceedaf6c63cf23d63d3d682e2b9610538fc007 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Wed, 2 Jun 2021 09:03:18 +1000 Subject: [PATCH 47/55] fix: use const --- src/structures/APIMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index dee07e0572c3..ff6bbeff9992 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -152,7 +152,7 @@ class APIMessage { } const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON()); - let components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON()); + const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON()); let username; let avatarURL; From de003f3f72617fb98fcd484a9027f10dc3d59dc9 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Wed, 2 Jun 2021 09:04:33 +1000 Subject: [PATCH 48/55] chore: remove unused props and constructor --- .../interfaces/InteractionResponses.js | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index b1062353996a..136d89354376 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -1,6 +1,5 @@ 'use strict'; -const WebhookClient = require('../../client/WebhookClient'); const { InteractionResponseTypes } = require('../../util/Constants'); const MessageFlags = require('../../util/MessageFlags'); const APIMessage = require('../APIMessage'); @@ -10,26 +9,6 @@ const APIMessage = require('../APIMessage'); * @interface */ class InteractionResponses { - constructor() { - /** - * Whether the reply to this interaction has been deferred - * @type {boolean} - */ - this.deferred = false; - - /** - * Whether this interaction has already been replied to - * @type {boolean} - */ - this.replied = false; - - /** - * An associated webhook client, can be used to create deferred replies - * @type {WebhookClient} - */ - this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options); - } - /** * Options for deferring the reply to a {@link CommandInteraction}. * @typedef {InteractionDeferOptions} From 234321df0815c505a5c099dba3a56ac95c35e3b0 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Wed, 2 Jun 2021 12:37:38 +1000 Subject: [PATCH 49/55] feat: enforce strings --- src/errors/Messages.js | 4 ++++ src/structures/MessageButton.js | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 31961ec41506..f864d71eec9c 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -44,6 +44,10 @@ const Messages = { EMBED_DESCRIPTION: 'MessageEmbed description must be a string.', EMBED_AUTHOR_NAME: 'MessageEmbed author name must be a string.', + BUTTON_LABEL: 'MessageButton label must be a string', + BUTTON_URL: 'MessageButton url must be a string', + BUTTON_CUSTOM_ID: 'MessageButton customID must be a string', + FILE_NOT_FOUND: file => `File could not be found: ${file}`, USER_NO_DMCHANNEL: 'No DM Channel exists!', diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 751a83ce8ecd..a61d2395bacc 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -1,6 +1,7 @@ 'use strict'; const BaseMessageComponent = require('./BaseMessageComponent'); +const { RangeError } = require('../errors'); const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants.js'); const Util = require('../util/Util'); @@ -72,7 +73,7 @@ class MessageButton extends BaseMessageComponent { * @returns {MessageButton} */ setCustomID(customID) { - this.customID = Util.resolveString(customID); + this.customID = Util.verifyString(customID, RangeError, 'BUTTON_CUSTOM_ID'); return this; } @@ -103,7 +104,7 @@ class MessageButton extends BaseMessageComponent { * @returns {MessageButton} */ setLabel(label) { - this.label = Util.resolveString(label); + this.label = Util.verifyString(label, RangeError, 'BUTTON_LABEL'); return this; } @@ -123,7 +124,7 @@ class MessageButton extends BaseMessageComponent { * @returns {MessageButton} */ setURL(url) { - this.url = Util.resolveString(url); + this.url = Util.verifyString(url, RangeError, 'BUTTON_URL'); return this; } From b15263177a2b30e554b25892e37d5d802f4cd578 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 3 Jun 2021 08:15:02 +1000 Subject: [PATCH 50/55] Update src/structures/Message.js Co-authored-by: Vlad Frangu --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 4722158ce2bc..46e066056d99 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -129,7 +129,7 @@ class Message extends Base { * A list of MessageActionRows in the message * @type {MessageActionRow[]} */ - this.components = (data.components || []).map(c => BaseMessageComponent.create(c, this.client)); + this.components = (data.components ?? []).map(c => BaseMessageComponent.create(c, this.client)); /** * A collection of attachments in the message - e.g. Pictures - mapped by their ID From a281d79401eef080b61baac4ba5e58398bb598fb Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 3 Jun 2021 08:16:05 +1000 Subject: [PATCH 51/55] docs: prop MessageButton#style is nullable --- src/structures/MessageButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index a61d2395bacc..9689e0cb0d3c 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -44,7 +44,7 @@ class MessageButton extends BaseMessageComponent { /** * The style of this button - * @type {MessageButtonStyle} + * @type {?MessageButtonStyle} */ this.style = data.style ? MessageButton.resolveStyle(data.style) : null; From 2d9fd1b6d0373eabf1a628bfdc8af2af8a0ed20a Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 3 Jun 2021 09:58:40 +1000 Subject: [PATCH 52/55] feat: flatten to infinity --- src/structures/MessageActionRow.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index cf5a4a59fb97..c0f25c5fdad4 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -52,7 +52,7 @@ class MessageActionRow extends BaseMessageComponent { * @returns {MessageActionRow} */ addComponents(...components) { - this.components.push(...components.flat(2).map(c => BaseMessageComponent.create(c, null, true))); + this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c, null, true))); return this; } @@ -67,7 +67,7 @@ class MessageActionRow extends BaseMessageComponent { this.components.splice( index, deleteCount, - ...components.flat(2).map(c => BaseMessageComponent.create(c, null, true)), + ...components.flat(Infinity).map(c => BaseMessageComponent.create(c, null, true)), ); return this; } From 961db5cbda3ffdcdf2a2a75e81a9bb5552dbc5a5 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 3 Jun 2021 19:26:50 +1000 Subject: [PATCH 53/55] fix: apply suggestions from code review Co-authored-by: SpaceEEC --- src/structures/APIMessage.js | 4 ++-- src/structures/MessageComponentInteractionCollector.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index ff6bbeff9992..b91699560a7b 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -197,8 +197,8 @@ class APIMessage { content, tts, nonce, - embed: this.options.embed === null ? null : embeds[0], - embeds, + embed: !isWebhookLike ? (this.options.embed === null ? null : embeds[0]) : undefined, + embeds: isWebhookLike ? embeds : undefined, components, username, avatar_url: avatarURL, diff --git a/src/structures/MessageComponentInteractionCollector.js b/src/structures/MessageComponentInteractionCollector.js index 5d2fb50c1d18..87cfae80ecbb 100644 --- a/src/structures/MessageComponentInteractionCollector.js +++ b/src/structures/MessageComponentInteractionCollector.js @@ -145,7 +145,7 @@ class MessageComponentInteractionCollector extends Collector { * @returns {void} */ _handleMessageDeletion(message) { - if (message.id === this.message.id) { + if (message.id === this.message?.id) { this.stop('messageDelete'); } } @@ -157,7 +157,7 @@ class MessageComponentInteractionCollector extends Collector { * @returns {void} */ _handleChannelDeletion(channel) { - if (channel.id === this.message.channel.id) { + if (channel.id === this.channel.id) { this.stop('channelDelete'); } } @@ -169,7 +169,7 @@ class MessageComponentInteractionCollector extends Collector { * @returns {void} */ _handleGuildDeletion(guild) { - if (this.message.guild && guild.id === this.message.guild.id) { + if (guild.id === this.channel.guild?.id) { this.stop('guildDelete'); } } From 1a6a6c215db794114daee6e22fe18d218c20bd42 Mon Sep 17 00:00:00 2001 From: monbrey Date: Fri, 4 Jun 2021 07:57:36 +1000 Subject: [PATCH 54/55] fix: apply suggestions from code review Co-authored-by: BannerBomb --- src/structures/MessageButton.js | 2 +- src/structures/interfaces/InteractionResponses.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 9689e0cb0d3c..737e43685a7d 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -2,7 +2,7 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const { RangeError } = require('../errors'); -const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants.js'); +const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants'); const Util = require('../util/Util'); /** diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index 136d89354376..99b1f5245ed1 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -170,7 +170,7 @@ class InteractionResponses { * Updates the original message whose button was pressed * @param {string|APIMessage|MessageAdditions} content The content for the reply * @param {WebhookEditMessageOptions} [options] Additional options for the reply - * @returns {Promise} + * @returns {Promise} * @example * // Remove the buttons from the message * interaction.update("A button was clicked", { components: [] }) From 4c0f2264313d12eedb9cd48761f49969a820a612 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Fri, 4 Jun 2021 22:03:51 +1000 Subject: [PATCH 55/55] types: missing method --- typings/index.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index bb7f372dca2e..93e1532071f3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1205,6 +1205,10 @@ declare module 'discord.js' { public webhookID: Snowflake | null; public flags: Readonly; public reference: MessageReference | null; + public awaitMessageComponentInteractions( + filter: CollectorFilter<[MessageComponentInteraction]>, + options?: AwaitMessageComponentInteractionsOptions, + ): Promise>; public awaitReactions( filter: CollectorFilter<[MessageReaction, User]>, options?: AwaitReactionsOptions,