diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js
index 7f8502bbc..03ebc9a92 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 5e14595f7..644e1c954 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 9ef299078..7f5116c93 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 14b1910a4..0abb9568b 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 000000000..073cf1d5d
--- /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 801c85fb7..e9f1306ee 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 2d34f6079..346b34785 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 56c788a35..8bdde8ce2 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 c0f25c5fd..555b2d476 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 737e43685..99ecc7de1 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 2ca201adf..a0714e125 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 87cfae80e..36db24f56 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 8bbf3b52c..96ecbd95a 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 aaf5ec318..981db6ee4 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 d6cbc77a9..8d5cb198f 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 bc6d2e061..7059c16a2 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 b00dc389c..75cfad3ae 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 a9710cd10..bb04725f8 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;