From 5a84b243db602aac09d0af487017b33f889d4999 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Wed, 26 May 2021 07:38:47 +1000 Subject: [PATCH 1/8] chore: new branch without broken rebasing --- .../websocket/handlers/INTERACTION_CREATE.js | 35 ++- src/index.js | 3 + src/structures/APIMessage.js | 8 + 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, 861 insertions(+), 15 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 92fd0789bd40..752b312d079b 100644 --- a/src/index.js +++ b/src/index.js @@ -67,6 +67,7 @@ module.exports = { APIMessage: require('./structures/APIMessage'), BaseGuildEmoji: require('./structures/BaseGuildEmoji'), BaseGuildVoiceChannel: require('./structures/BaseGuildVoiceChannel'), + BaseMessageComponent: require('./structures/BaseMessageComponent'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), ClientApplication: require('./structures/ClientApplication'), @@ -91,7 +92,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 c169e32b0eb4..ecaa0ad7a231 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'); @@ -148,6 +149,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 (this.isWebhook) { @@ -193,6 +200,7 @@ class APIMessage { nonce, 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 a19b6ef7e5ba..86a7be0f8f2a 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 240292d77028..a9c469b85b4a 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']); + function keyMirror(arr) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; diff --git a/src/util/Structures.js b/src/util/Structures.js index 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 b611bb0abdf1..bde89a707c56 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 OverwriteTypes { role = 0, member = 1, @@ -52,14 +68,15 @@ 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, + ApplicationCommandOptionType as ApplicationCommandOptionTypes, + ApplicationCommandPermissionType as ApplicationCommandPermissionTypes, } from 'discord-api-types/v8'; import { EventEmitter } from 'events'; import { PathLike } from 'fs'; @@ -244,6 +261,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; } @@ -267,6 +294,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'; @@ -1077,6 +1125,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 { @@ -1116,6 +1165,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; @@ -1185,6 +1235,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?: object); @@ -1203,6 +1261,23 @@ declare module 'discord.js' { public toJSON(): object; } + 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, @@ -3138,16 +3213,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 { content?: StringResolvable; embed?: MessageEmbed | MessageEmbedOptions | null; @@ -3155,6 +3254,7 @@ declare module 'discord.js' { flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; attachments?: MessageAttachment[]; + components?: BaseMessageComponent[]; } interface MessageEmbedAuthor { @@ -3247,6 +3347,7 @@ declare module 'discord.js' { nonce?: string | number; content?: StringResolvable; embed?: MessageEmbed | MessageEmbedOptions; + components?: MessageComponentResolvable[]; allowedMentions?: MessageMentionOptions; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; From 87465253de0b1da2977039f284af098d390a9d88 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Wed, 26 May 2021 07:48:39 +1000 Subject: [PATCH 2/8] 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 86a7be0f8f2a..23b6367697b7 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 bde89a707c56..08f3c667c2bc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -294,7 +294,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; @@ -1125,7 +1125,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 243429e1a6da32a4ccffe5c2ad230b5076c416a2 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 27 May 2021 08:19:18 +1000 Subject: [PATCH 3/8] 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 23b6367697b7..55c9e9adfc89 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 08f3c667c2bc..166b7bcd7156 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -268,7 +268,7 @@ declare module 'discord.js' { } interface BaseMessageComponentOptions { - type?: MessageComponentType; + type?: MessageComponentType | MessageComponentTypes; } class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { @@ -1274,7 +1274,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; } @@ -3214,7 +3214,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[]; } @@ -3228,7 +3228,7 @@ declare module 'discord.js' { disabled?: boolean; emoji?: RawEmoji; label?: string; - style?: MessageButtonStyle; + style?: MessageButtonStyle | MessageButtonStyles; url?: string; } @@ -3254,7 +3254,7 @@ declare module 'discord.js' { flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; attachments?: MessageAttachment[]; - components?: BaseMessageComponent[]; + components?: MessageComponentResolvable[]; } interface MessageEmbedAuthor { From 7d692758b2d942f3690f217c629874b004d481ad Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 08:20:24 +1000 Subject: [PATCH 4/8] 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 166b7bcd7156..0bf79c816552 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -695,6 +695,8 @@ declare module 'discord.js' { ApplicationCommandPermissionTypes: typeof ApplicationCommandPermissionTypes; InteractionTypes: typeof InteractionTypes; InteractionResponseTypes: typeof InteractionResponseTypes; + MessageComponentTypes: typeof MessageComponentTypes; + MessageButtonStyles: typeof MessageButtonStyles; }; export class DataResolver { From 06de5f6bf9c7bd26e4519790b85590ba9a0b823d Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 09:42:07 +1000 Subject: [PATCH 5/8] fix: export new classes --- src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index 752b312d079b..826f1c603081 100644 --- a/src/index.js +++ b/src/index.js @@ -77,6 +77,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 280d454baef211b3d40500ed2cb9bafd7a39a991 Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 09:42:27 +1000 Subject: [PATCH 6/8] 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 fddbda529bff63729d5fab77e0f3c097dd3ec92b Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 09:42:42 +1000 Subject: [PATCH 7/8] 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 a9c469b85b4a..78cab06a93e6 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 da340c398cdb4607096b3951803538512e3863cc Mon Sep 17 00:00:00 2001 From: Monbrey Date: Thu, 27 May 2021 10:44:50 +1000 Subject: [PATCH 8/8] 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 0bf79c816552..b89d5fbb3480 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -264,7 +264,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 { @@ -1276,8 +1279,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 { @@ -3230,12 +3234,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; @@ -3249,6 +3256,8 @@ declare module 'discord.js' { type MessageComponentType = keyof typeof MessageComponentTypes; + type MessageComponentTypeResolvable = MessageComponentType | MessageComponentTypes | string | number; + interface MessageEditOptions { content?: StringResolvable; embed?: MessageEmbed | MessageEmbedOptions | null;