From c4f1c75efa1cff1f9c775a266dccbe581305e79d Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 9 Jun 2021 22:59:12 +1000 Subject: [PATCH] feat: general component improvements (#5787) --- .../websocket/handlers/INTERACTION_CREATE.js | 28 +++++++++++-------- src/index.js | 1 + src/structures/APIMessage.js | 7 ++++- src/structures/BaseMessageComponent.js | 6 ++-- src/structures/ButtonInteraction.js | 11 ++++++++ src/structures/Emoji.js | 8 ++++++ src/structures/Interaction.js | 8 ++++++ src/structures/Message.js | 4 +-- src/structures/MessageActionRow.js | 14 +++++----- src/structures/MessageButton.js | 18 ++++++------ src/structures/MessageComponentInteraction.js | 4 +-- .../MessageComponentInteractionCollector.js | 2 +- src/structures/Webhook.js | 4 +-- .../interfaces/InteractionResponses.js | 10 +++---- src/structures/interfaces/TextBasedChannel.js | 4 +-- src/util/Structures.js | 2 ++ src/util/Util.js | 14 ++++++++++ typings/index.d.ts | 14 +++++++--- 18 files changed, 111 insertions(+), 48 deletions(-) create mode 100644 src/structures/ButtonInteraction.js diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js index 7f8502bbc014..03ebc9a9213e 100644 --- a/src/client/websocket/handlers/INTERACTION_CREATE.js +++ b/src/client/websocket/handlers/INTERACTION_CREATE.js @@ -1,21 +1,27 @@ 'use strict'; -const { Events, InteractionTypes } = require('../../../util/Constants'); +const { Events, InteractionTypes, MessageComponentTypes } = require('../../../util/Constants'); const Structures = require('../../../util/Structures'); module.exports = (client, { d: data }) => { - let interaction; + let InteractionType; switch (data.type) { - case InteractionTypes.APPLICATION_COMMAND: { - const CommandInteraction = Structures.get('CommandInteraction'); - interaction = new CommandInteraction(client, data); + case InteractionTypes.APPLICATION_COMMAND: + InteractionType = Structures.get('CommandInteraction'); break; - } - case InteractionTypes.MESSAGE_COMPONENT: { - const MessageComponentInteraction = Structures.get('MessageComponentInteraction'); - interaction = new MessageComponentInteraction(client, data); + case InteractionTypes.MESSAGE_COMPONENT: + switch (data.data.component_type) { + case MessageComponentTypes.BUTTON: + InteractionType = Structures.get('ButtonInteraction'); + break; + default: + client.emit( + Events.DEBUG, + `[INTERACTION] Received component interaction with unknown type: ${data.data.component_type}`, + ); + return; + } break; - } default: client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`); return; @@ -26,5 +32,5 @@ module.exports = (client, { d: data }) => { * @event Client#interaction * @param {Interaction} interaction The interaction which was created */ - client.emit(Events.INTERACTION_CREATE, interaction); + client.emit(Events.INTERACTION_CREATE, new InteractionType(client, data)); }; diff --git a/src/index.js b/src/index.js index 5e14595f7961..644e1c954d35 100644 --- a/src/index.js +++ b/src/index.js @@ -70,6 +70,7 @@ module.exports = { BaseGuildEmoji: require('./structures/BaseGuildEmoji'), BaseGuildVoiceChannel: require('./structures/BaseGuildVoiceChannel'), BaseMessageComponent: require('./structures/BaseMessageComponent'), + ButtonInteraction: require('./structures/ButtonInteraction'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), ClientApplication: require('./structures/ClientApplication'), diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 9ef29907874d..7f5116c9399e 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -3,6 +3,7 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const MessageEmbed = require('./MessageEmbed'); const { RangeError } = require('../errors'); +const { MessageComponentTypes } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); const MessageFlags = require('../util/MessageFlags'); const Util = require('../util/Util'); @@ -152,7 +153,11 @@ class APIMessage { } const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON()); - const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON()); + const components = this.options.components?.map(c => + BaseMessageComponent.create( + Array.isArray(c) ? { type: MessageComponentTypes.ACTION_ROW, components: c } : c, + ).toJSON(), + ); let username; let avatarURL; diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 14b1910a45c9..0abb9568baed 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -22,13 +22,15 @@ class BaseMessageComponent { */ /** - * Components that can be sent in a message + * Components that can be sent in a message. This can be: + * * MessageActionRow + * * MessageButton * @typedef {MessageActionRow|MessageButton} MessageComponent */ /** * Data that can be resolved to a MessageComponentType. This can be: - * * {@link MessageComponentType} + * * MessageComponentType * * string * * number * @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable diff --git a/src/structures/ButtonInteraction.js b/src/structures/ButtonInteraction.js new file mode 100644 index 000000000000..073cf1d5d988 --- /dev/null +++ b/src/structures/ButtonInteraction.js @@ -0,0 +1,11 @@ +'use strict'; + +const MessageComponentInteraction = require('./MessageComponentInteraction'); + +/** + * Represents a button interaction. + * @exxtends {MessageComponentInteraction} + */ +class ButtonInteraction extends MessageComponentInteraction {} + +module.exports = ButtonInteraction; diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 801c85fb7152..e9f1306eec96 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -3,6 +3,14 @@ const Base = require('./Base'); const SnowflakeUtil = require('../util/SnowflakeUtil'); +/** + * Represents raw emoji data from the API + * @typedef {Object} RawEmoji + * @property {?Snowflake} id ID of this emoji + * @property {?string} name Name of this emoji + * @property {?boolean} animated Whether this emoji is animated + */ + /** * Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}. * @extends {Base} diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index 2d34f6079f3d..346b347852e5 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -120,6 +120,14 @@ class Interaction extends Base { isMessageComponent() { return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT; } + + /** + * Indicates whether this interaction is a button interaction. + * @returns {boolean} + */ + isButton() { + return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT && this.componentType === 'BUTTON'; + } } module.exports = Interaction; diff --git a/src/structures/Message.js b/src/structures/Message.js index 56c788a35015..8bdde8ce2ce7 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -538,8 +538,8 @@ class Message extends Base { * @property {MessageAttachment[]} [attachments] An array of attachments to keep, * all attachments will be kept if omitted * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message - * @property {MessageActionRow[]} [components] Action rows containing interactive components for the message - * (buttons, select menus) + * @property {MessageActionRow[]|MessageActionRowOptions[]|MessageActionRowComponentResolvable[][]} [components] + * Action rows containing interactive components for the message (buttons, select menus) */ /** diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index c0f25c5fdad4..555b2d476d56 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -4,24 +4,24 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const { MessageComponentTypes } = require('../util/Constants'); /** - * Represents an ActionRow containing message components. + * Represents an action row containing message components. * @extends {BaseMessageComponent} */ class MessageActionRow extends BaseMessageComponent { /** - * Components that can be placed in a MessageActionRow + * Components that can be placed in an action row * * MessageButton * @typedef {MessageButton} MessageActionRowComponent */ /** - * Options for components that can be placed in a MessageActionRow + * Options for components that can be placed in an action row * * MessageButtonOptions * @typedef {MessageButtonOptions} MessageActionRowComponentOptions */ /** - * Data that can be resolved into a components that can be placed in a MessageActionRow + * Data that can be resolved into a components that can be placed in an action row * * MessageActionRowComponent * * MessageActionRowComponentOptions * @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable @@ -30,7 +30,7 @@ class MessageActionRow extends BaseMessageComponent { /** * @typedef {BaseMessageComponentOptions} MessageActionRowOptions * @property {MessageActionRowComponentResolvable[]} [components] - * The components to place in this ActionRow + * The components to place in this action row */ /** @@ -40,14 +40,14 @@ class MessageActionRow extends BaseMessageComponent { super({ type: 'ACTION_ROW' }); /** - * The components in this MessageActionRow + * The components in this action row * @type {MessageActionRowComponent[]} */ this.components = (data.components ?? []).map(c => BaseMessageComponent.create(c, null, true)); } /** - * Adds components to the row. + * Adds components to the action row. * @param {...MessageActionRowComponentResolvable[]} components The components to add * @returns {MessageActionRow} */ diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 737e43685a7d..99ecc7de1886 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -6,7 +6,7 @@ const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constant const Util = require('../util/Util'); /** - * Represents a Button message component. + * Represents a button message component. * @extends {BaseMessageComponent} */ class MessageButton extends BaseMessageComponent { @@ -15,7 +15,7 @@ class MessageButton extends BaseMessageComponent { * @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 {MessageButtonStyleResolvable} [style] The style of this button - * @property {Emoji} [emoji] The emoji to be displayed to the left of the text + * @property {EmojiIdentifierResolvable} [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 */ @@ -50,9 +50,9 @@ class MessageButton extends BaseMessageComponent { /** * Emoji for this button - * @type {?Emoji|string} + * @type {?RawEmoji} */ - this.emoji = data.emoji ?? null; + this.emoji = data.emoji ? Util.resolvePartialEmoji(data.emoji) : null; /** * The URL this button links to, if it is a Link style button @@ -93,8 +93,7 @@ class MessageButton extends BaseMessageComponent { * @returns {MessageButton} */ setEmoji(emoji) { - if (/^\d{17,19}$/.test(emoji)) this.emoji = { id: emoji }; - else this.emoji = Util.parseEmoji(`${emoji}`); + this.emoji = Util.resolvePartialEmoji(emoji); return this; } @@ -119,7 +118,8 @@ class MessageButton extends BaseMessageComponent { } /** - * Sets the URL of this button. MessageButton#style should be LINK + * Sets the URL of this button. + * MessageButton#style must be LINK when setting a URL * @param {string} url The URL of this button * @returns {MessageButton} */ @@ -146,14 +146,14 @@ class MessageButton extends BaseMessageComponent { /** * Data that can be resolved to a MessageButtonStyle. This can be - * * {@link MessageButtonStyle} + * * MessageButtonStyle * * string * * number * @typedef {string|number|MessageButtonStyle} MessageButtonStyleResolvable */ /** - * Resolves the style of a MessageButton + * Resolves the style of a button * @param {MessageButtonStyleResolvable} style The style to resolve * @returns {MessageButtonStyle} * @private diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index 2ca201adf3bd..a0714e1252c0 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -21,13 +21,13 @@ class MessageComponentInteraction extends Interaction { this.message = data.message ? this.channel?.messages.add(data.message) ?? data.message : null; /** - * The custom ID of the component which was clicked + * The custom ID of the component which was interacted with * @type {string} */ this.customID = data.data.custom_id; /** - * The type of component that was interacted with + * The type of component which was interacted with * @type {string} */ this.componentType = MessageComponentInteraction.resolveType(data.data.component_type); diff --git a/src/structures/MessageComponentInteractionCollector.js b/src/structures/MessageComponentInteractionCollector.js index 87cfae80ecbb..36db24f56d79 100644 --- a/src/structures/MessageComponentInteractionCollector.js +++ b/src/structures/MessageComponentInteractionCollector.js @@ -40,7 +40,7 @@ class MessageComponentInteractionCollector extends Collector { this.channel = this.message ? this.message.channel : source; /** - * The users which have interacted to buttons on this collector + * The users which have interacted to components on this collector * @type {Collection} */ this.users = new Collection(); diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 8bbf3b52c44d..96ecbd95a032 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -101,8 +101,8 @@ class Webhook { * @property {string} [content] See {@link BaseMessageOptions#content} * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] See {@link BaseMessageOptions#files} * @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions} - * @property {MessageActionRow[]} [components] Action rows containing interactive components for the message - * (buttons, select menus) + * @property {MessageActionRow[]|MessageActionRowOptions[]|MessageActionRowComponentResolvable[][]} [components] + * Action rows containing interactive components for the message (buttons, select menus) */ /** diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index aaf5ec3180ad..981db6ee4b4a 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -11,13 +11,13 @@ const APIMessage = require('../APIMessage'); */ class InteractionResponses { /** - * Options for deferring the reply to a {@link CommandInteraction}. + * Options for deferring the reply to an {@link Interaction}. * @typedef {Object} InteractionDeferOptions * @property {boolean} [ephemeral] Whether the reply should be ephemeral */ /** - * Options for a reply to an interaction. + * Options for a reply to an {@link Interaction}. * @typedef {BaseMessageOptions} InteractionReplyOptions * @property {boolean} [ephemeral] Whether the reply should be ephemeral * @property {MessageEmbed[]|Object[]} [embeds] An array of embeds for the message @@ -140,10 +140,10 @@ class InteractionResponses { } /** - * Defers an update to the message to which the button was attached + * Defers an update to the message to which the component was attached * @returns {Promise} * @example - * // Defer to update the button to a loading state + * // Defer updating and reset the component's loading state * interaction.deferUpdate() * .then(console.log) * .catch(console.error); @@ -163,7 +163,7 @@ class InteractionResponses { * @param {string|APIMessage|WebhookEditMessageOptions} options The options for the reply * @returns {Promise} * @example - * // Remove the buttons from the message + * // Remove the components from the message * interaction.update("A button was clicked", { components: [] }) * .then(console.log) * .catch(console.error); diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index d6cbc77a96ec..8d5cb198f9f8 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -63,8 +63,8 @@ class TextBasedChannel { * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if * it exceeds the character limit. If an object is provided, these are the options for splitting the message - * @property {MessageActionRow[]} [components] Action rows containing interactive components for the message - * (buttons, select menus) + * @property {MessageActionRow[]|MessageActionRowOptions[]|MessageActionRowComponentResolvable[][]} [components] + * Action rows containing interactive components for the message (buttons, select menus) */ /** diff --git a/src/util/Structures.js b/src/util/Structures.js index bc6d2e06106e..7059c16a2cd7 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -21,6 +21,7 @@ * * **`User`** * * **`CommandInteraction`** * * **`MessageComponentInteraction`** + * * **`ButtonInteraction`** * @typedef {string} ExtendableStructure */ @@ -113,6 +114,7 @@ const structures = { User: require('../structures/User'), CommandInteraction: require('../structures/CommandInteraction'), MessageComponentInteraction: require('../structures/MessageComponentInteraction'), + ButtonInteraction: require('../structures/ButtonInteraction'), }; module.exports = Structures; diff --git a/src/util/Util.js b/src/util/Util.js index b00dc389c973..75cfad3aef75 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -267,6 +267,20 @@ class Util { return { animated: Boolean(m[1]), name: m[2], id: m[3] || null }; } + /** + * Resolves a partial emoji object from an {@link EmojiIdentifierResolvable}, without checking a Client. + * @param {EmojiIdentifierResolvable} emoji Emoji identifier to resolve + * @returns {?RawEmoji} + * @private + */ + static resolvePartialEmoji(emoji) { + if (!emoji) return null; + if (typeof emoji === 'string') return /^\d{17,19}$/.test(emoji) ? { id: emoji } : Util.parseEmoji(emoji); + const { id, name, animated } = emoji; + if (!id && !name) return null; + return { id, name, animated }; + } + /** * Shallow-copies an object with its class/prototype intact. * @param {Object} obj Object to clone diff --git a/typings/index.d.ts b/typings/index.d.ts index a9710cd10ac0..bb04725f8d9d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -294,6 +294,10 @@ declare module 'discord.js' { public static resolve(bit?: BitFieldResolvable): number | bigint; } + export class ButtonInteraction extends MessageComponentInteraction { + public componentType: 'BUTTON'; + } + export class CategoryChannel extends GuildChannel { public readonly children: Collection; public type: 'category'; @@ -1114,6 +1118,7 @@ declare module 'discord.js' { public type: InteractionType; public user: User; public version: number; + public isButton(): this is ButtonInteraction; public isCommand(): this is CommandInteraction; public isMessageComponent(): this is MessageComponentInteraction; } @@ -1267,7 +1272,7 @@ declare module 'discord.js' { constructor(data?: MessageButton | MessageButtonOptions); public customID: string | null; public disabled: boolean; - public emoji: string | RawEmoji | null; + public emoji: RawEmoji | null; public label: string | null; public style: MessageButtonStyle | null; public type: 'BUTTON'; @@ -1888,6 +1893,7 @@ declare module 'discord.js' { public static moveElementInArray(array: any[], element: any, newIndex: number, offset?: boolean): number; public static parseEmoji(text: string): { animated: boolean; name: string; id: Snowflake | null } | null; public static resolveColor(color: ColorResolvable): number; + public static resolvePartialEmoji(emoji: EmojiIdentifierResolvable): Partial | null; public static verifyString(data: string, error?: typeof Error, errorMessage?: string, allowEmpty?: boolean): string; public static setPosition( item: T, @@ -3309,7 +3315,7 @@ declare module 'discord.js' { interface MessageButtonOptions extends BaseMessageComponentOptions { customID?: string; disabled?: boolean; - emoji?: RawEmoji; + emoji?: EmojiIdentifierResolvable; label?: string; style: MessageButtonStyleResolvable; url?: string; @@ -3346,7 +3352,7 @@ declare module 'discord.js' { files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; - components?: MessageActionRow[] | MessageActionRowOptions[]; + components?: MessageActionRow[] | MessageActionRowOptions[] | MessageActionRowComponentResolvable[][]; } interface MessageEmbedAuthor { @@ -3439,7 +3445,7 @@ declare module 'discord.js' { nonce?: string | number; content?: string; embed?: MessageEmbed | MessageEmbedOptions; - components?: MessageActionRow[] | MessageActionRowOptions[]; + components?: MessageActionRow[] | MessageActionRowOptions[] | MessageActionRowComponentResolvable[][]; allowedMentions?: MessageMentionOptions; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean;