diff --git a/src/client/actions/InteractionCreate.js b/src/client/actions/InteractionCreate.js
index 3559963efe2c..e30c3312e1f1 100644
--- a/src/client/actions/InteractionCreate.js
+++ b/src/client/actions/InteractionCreate.js
@@ -21,6 +21,9 @@ class InteractionCreateAction extends Action {
case MessageComponentTypes.BUTTON:
InteractionType = Structures.get('ButtonInteraction');
break;
+ case MessageComponentTypes.SELECT_MENU:
+ InteractionType = Structures.get('SelectMenuInteraction');
+ break;
default:
client.emit(
Events.DEBUG,
diff --git a/src/errors/Messages.js b/src/errors/Messages.js
index 36b3a123ff11..2e04d168094d 100644
--- a/src/errors/Messages.js
+++ b/src/errors/Messages.js
@@ -50,6 +50,12 @@ const Messages = {
BUTTON_URL: 'MessageButton url must be a string',
BUTTON_CUSTOM_ID: 'MessageButton customID must be a string',
+ SELECT_MENU_CUSTOM_ID: 'MessageSelectMenu customID must be a string',
+ SELECT_MENU_PLACEHOLDER: 'MessageSelectMenu placeholder must be a string',
+ SELECT_OPTION_LABEL: 'MessageSelectOption label must be a string',
+ SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string',
+ SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string',
+
INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`,
FILE_NOT_FOUND: file => `File could not be found: ${file}`,
diff --git a/src/index.js b/src/index.js
index 7fd16b9b6398..ca84b6ea4587 100644
--- a/src/index.js
+++ b/src/index.js
@@ -97,6 +97,7 @@ module.exports = {
MessageEmbed: require('./structures/MessageEmbed'),
MessageMentions: require('./structures/MessageMentions'),
MessageReaction: require('./structures/MessageReaction'),
+ MessageSelectMenu: require('./structures/MessageSelectMenu'),
NewsChannel: require('./structures/NewsChannel'),
OAuth2Guild: require('./structures/OAuth2Guild'),
PermissionOverwrites: require('./structures/PermissionOverwrites'),
@@ -106,6 +107,7 @@ module.exports = {
ReactionEmoji: require('./structures/ReactionEmoji'),
RichPresenceAssets: require('./structures/Presence').RichPresenceAssets,
Role: require('./structures/Role'),
+ SelectMenuInteraction: require('./structures/SelectMenuInteraction'),
Sticker: require('./structures/Sticker'),
StoreChannel: require('./structures/StoreChannel'),
StageChannel: require('./structures/StageChannel'),
diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js
index 0abb9568baed..3124e34b4bcf 100644
--- a/src/structures/BaseMessageComponent.js
+++ b/src/structures/BaseMessageComponent.js
@@ -18,14 +18,16 @@ class BaseMessageComponent {
* Data that can be resolved into options for a MessageComponent. This can be:
* * MessageActionRowOptions
* * MessageButtonOptions
- * @typedef {MessageActionRowOptions|MessageButtonOptions} MessageComponentOptions
+ * * MessageSelectMenuOptions
+ * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
*/
/**
* Components that can be sent in a message. This can be:
* * MessageActionRow
* * MessageButton
- * @typedef {MessageActionRow|MessageButton} MessageComponent
+ * * MessageSelectMenu
+ * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
*/
/**
@@ -72,6 +74,11 @@ class BaseMessageComponent {
component = new MessageButton(data);
break;
}
+ case MessageComponentTypes.SELECT_MENU: {
+ const MessageSelectMenu = require('./MessageSelectMenu');
+ component = new MessageSelectMenu(data);
+ break;
+ }
default:
if (client) {
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js
index 8fb11830ad78..189b3de03ec3 100644
--- a/src/structures/Interaction.js
+++ b/src/structures/Interaction.js
@@ -1,7 +1,7 @@
'use strict';
const Base = require('./Base');
-const { InteractionTypes } = require('../util/Constants');
+const { InteractionTypes, MessageComponentTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
@@ -114,7 +114,7 @@ class Interaction extends Base {
}
/**
- * Indicates whether this interaction is a component interaction.
+ * Indicates whether this interaction is a message component interaction.
* @returns {boolean}
*/
isMessageComponent() {
@@ -126,7 +126,21 @@ class Interaction extends Base {
* @returns {boolean}
*/
isButton() {
- return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT && this.componentType === 'BUTTON';
+ return (
+ InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT &&
+ MessageComponentTypes[this.componentType] === MessageComponentTypes.BUTTON
+ );
+ }
+
+ /**
+ * Indicates whether this interaction is a select menu interaction.
+ * @returns {boolean}
+ */
+ isSelectMenu() {
+ return (
+ InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT &&
+ MessageComponentTypes[this.componentType] === MessageComponentTypes.SELECT_MENU
+ );
}
}
diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js
index 450b387783c6..219e27e0825b 100644
--- a/src/structures/MessageActionRow.js
+++ b/src/structures/MessageActionRow.js
@@ -11,17 +11,19 @@ class MessageActionRow extends BaseMessageComponent {
/**
* Components that can be placed in an action row
* * MessageButton
- * @typedef {MessageButton} MessageActionRowComponent
+ * * MessageSelectMenu
+ * @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent
*/
/**
* Options for components that can be placed in an action row
* * MessageButtonOptions
- * @typedef {MessageButtonOptions} MessageActionRowComponentOptions
+ * * MessageSelectMenuOptions
+ * @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions
*/
/**
- * Data that can be resolved into a components that can be placed in an action row
+ * Data that can be resolved into components that can be placed in an action row
* * MessageActionRowComponent
* * MessageActionRowComponentOptions
* @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable
@@ -61,7 +63,7 @@ class MessageActionRow extends BaseMessageComponent {
* @param {number} index The index to start at
* @param {number} deleteCount The number of components to remove
* @param {...MessageActionRowComponentResolvable[]} [components] The replacing components
- * @returns {MessageSelectMenu}
+ * @returns {MessageActionRow}
*/
spliceComponents(index, deleteCount, ...components) {
this.components.splice(
diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js
new file mode 100644
index 000000000000..a79522cd3738
--- /dev/null
+++ b/src/structures/MessageSelectMenu.js
@@ -0,0 +1,202 @@
+'use strict';
+
+const BaseMessageComponent = require('./BaseMessageComponent');
+const { MessageComponentTypes } = require('../util/Constants');
+const Util = require('../util/Util');
+
+/**
+ * Represents a select menu message component
+ * @extends {BaseMessageComponent}
+ */
+class MessageSelectMenu extends BaseMessageComponent {
+ /**
+ * @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions
+ * @property {string} [customID] A unique string to be sent in the interaction when clicked
+ * @property {string} [placeholder] Custom placeholder text to display when nothing is selected
+ * @property {number} [minValues] The minimum number of selections required
+ * @property {number} [maxValues] The maximum number of selections allowed
+ * @property {MessageSelectOption[]} [options] Options for the select menu
+ * @property {boolean} [disabled=false] Disables the select menu to prevent interactions
+ */
+
+ /**
+ * @typedef {Object} MessageSelectOption
+ * @property {string} label The text to be displayed on this option
+ * @property {string} value The value to be sent for this option
+ * @property {?string} description Optional description to show for this option
+ * @property {?RawEmoji} emoji Emoji to display for this option
+ * @property {boolean} default Render this option as the default selection
+ */
+
+ /**
+ * @typedef {Object} MessageSelectOptionData
+ * @property {string} label The text to be displayed on this option
+ * @property {string} value The value to be sent for this option
+ * @property {string} [description] Optional description to show for this option
+ * @property {EmojiIdentifierResolvable} [emoji] Emoji to display for this option
+ * @property {boolean} [default] Render this option as the default selection
+ */
+
+ /**
+ * @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data
+ */
+ constructor(data = {}) {
+ super({ type: 'SELECT_MENU' });
+
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * A unique string to be sent in the interaction when clicked
+ * @type {?string}
+ */
+ this.customID = data.custom_id ?? data.customID ?? null;
+
+ /**
+ * Custom placeholder text to display when nothing is selected
+ * @type {?string}
+ */
+ this.placeholder = data.placeholder ?? null;
+
+ /**
+ * The minimum number of selections required
+ * @type {?number}
+ */
+ this.minValues = data.min_values ?? data.minValues ?? null;
+
+ /**
+ * The maximum number of selections allowed
+ * @type {?number}
+ */
+ this.maxValues = data.max_values ?? data.maxValues ?? null;
+
+ /**
+ * Options for the select menu
+ * @type {MessageSelectOption[]}
+ */
+ this.options = this.constructor.normalizeOptions(data.options ?? []);
+
+ /**
+ * Whether this select menu is currently disabled
+ * @type {?boolean}
+ */
+ this.disabled = data.disabled ?? false;
+ }
+
+ /**
+ * Sets the custom ID of this select menu
+ * @param {string} customID A unique string to be sent in the interaction when clicked
+ * @returns {MessageSelectMenu}
+ */
+ setCustomID(customID) {
+ this.customID = Util.verifyString(customID, RangeError, 'SELECT_MENU_CUSTOM_ID');
+ return this;
+ }
+
+ /**
+ * Sets the interactive status of the select menu
+ * @param {boolean} disabled Whether this select menu should be disabled
+ * @returns {MessageSelectMenu}
+ */
+ setDisabled(disabled) {
+ this.disabled = disabled;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of selections allowed for this select menu
+ * @param {number} maxValues Number of selections to be allowed
+ * @returns {MessageSelectMenu}
+ */
+ setMaxValues(maxValues) {
+ this.maxValues = maxValues;
+ return this;
+ }
+
+ /**
+ * Sets the minimum number of selections required for this select menu
+ * This will default the maxValues to the number of options, unless manually set
+ * @param {number} minValues Number of selections to be required
+ * @returns {MessageSelectMenu}
+ */
+ setMinValues(minValues) {
+ this.minValues = minValues;
+ return this;
+ }
+
+ /**
+ * Sets the placeholder of this select menu
+ * @param {string} placeholder Custom placeholder text to display when nothing is selected
+ * @returns {MessageSelectMenu}
+ */
+ setPlaceholder(placeholder) {
+ this.placeholder = Util.verifyString(placeholder, RangeError, 'SELECT_MENU_PLACEHOLDER');
+ return this;
+ }
+
+ /**
+ * Adds options to the select menu.
+ * @param {...(MessageSelectOption[]|MessageSelectOption[])} options The options to add
+ * @returns {MessageSelectMenu}
+ */
+ addOptions(...options) {
+ this.options.push(...this.constructor.normalizeOptions(options));
+ return this;
+ }
+
+ /**
+ * Removes, replaces, and inserts options in the select menu.
+ * @param {number} index The index to start at
+ * @param {number} deleteCount The number of options to remove
+ * @param {...MessageSelectOption|MessageSelectOption[]} [options] The replacing option objects
+ * @returns {MessageSelectMenu}
+ */
+ spliceOptions(index, deleteCount, ...options) {
+ this.options.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options));
+ return this;
+ }
+
+ /**
+ * Transforms this select menu to a plain object
+ * @returns {Object} The raw data of this select menu
+ */
+ toJSON() {
+ return {
+ custom_id: this.customID,
+ disabled: this.disabled,
+ placeholder: this.placeholder,
+ min_values: this.minValues,
+ max_values: this.maxValues ?? (this.minValues ? this.options.length : undefined),
+ options: this.options,
+ type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type,
+ };
+ }
+
+ /**
+ * Normalizes option input and resolves strings and emojis.
+ * @param {MessageSelectOptionData} option The select menu option to normalize
+ * @returns {MessageSelectOption}
+ */
+ static normalizeOption(option) {
+ let { label, value, description, emoji } = option;
+
+ label = Util.verifyString(label, RangeError, 'SELECT_OPTION_LABEL');
+ value = Util.verifyString(value, RangeError, 'SELECT_OPTION_VALUE');
+ emoji = emoji ? Util.resolvePartialEmoji(emoji) : null;
+ description = description ? Util.verifyString(description, RangeError, 'SELECT_OPTION_DESCRIPTION', true) : null;
+
+ return { label, value, description, emoji, default: option.default ?? false };
+ }
+
+ /**
+ * Normalizes option input and resolves strings and emojis.
+ * @param {...MessageSelectOptionData|MessageSelectOption[]} options The select menu options to normalize
+ * @returns {MessageSelectOption[]}
+ */
+ static normalizeOptions(...options) {
+ return options.flat(Infinity).map(option => this.normalizeOption(option));
+ }
+}
+
+module.exports = MessageSelectMenu;
diff --git a/src/structures/SelectMenuInteraction.js b/src/structures/SelectMenuInteraction.js
new file mode 100644
index 000000000000..be4f3a0ed649
--- /dev/null
+++ b/src/structures/SelectMenuInteraction.js
@@ -0,0 +1,21 @@
+'use strict';
+
+const MessageComponentInteraction = require('./MessageComponentInteraction');
+
+/**
+ * Represents a select menu interaction.
+ * @extends {MessageComponentInteraction}
+ */
+class SelectMenuInteraction extends MessageComponentInteraction {
+ constructor(client, data) {
+ super(client, data);
+
+ /**
+ * The values selected, if the component which was interacted with was a select menu
+ * @type {string[]}
+ */
+ this.values = this.componentType === 'SELECT_MENU' ? data.data.values : null;
+ }
+}
+
+module.exports = SelectMenuInteraction;
diff --git a/src/util/Constants.js b/src/util/Constants.js
index 27b9770502f7..e4e158bc0ea7 100644
--- a/src/util/Constants.js
+++ b/src/util/Constants.js
@@ -944,9 +944,10 @@ exports.InteractionResponseTypes = createEnum([
* The type of a message component
* * ACTION_ROW
* * BUTTON
+ * * SELECT_MENU
* @typedef {string} MessageComponentType
*/
-exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON']);
+exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU']);
/**
* The style of a message button
diff --git a/src/util/Structures.js b/src/util/Structures.js
index 039a9ca51ee4..325b9e4e6b0d 100644
--- a/src/util/Structures.js
+++ b/src/util/Structures.js
@@ -24,6 +24,7 @@
* * **`CommandInteraction`**
* * **`ButtonInteraction`**
* * **`StageInstance`**
+ * * **`SelectMenuInteraction`**
* @typedef {string} ExtendableStructure
*/
@@ -118,6 +119,7 @@ const structures = {
User: require('../structures/User'),
CommandInteraction: require('../structures/CommandInteraction'),
ButtonInteraction: require('../structures/ButtonInteraction'),
+ SelectMenuInteraction: require('../structures/SelectMenuInteraction'),
StageInstance: require('../structures/StageInstance'),
};
diff --git a/typings/index.d.ts b/typings/index.d.ts
index e9665f50d7eb..197903987381 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -82,6 +82,7 @@ declare enum MessageButtonStyles {
declare enum MessageComponentTypes {
ACTION_ROW = 1,
BUTTON = 2,
+ SELECT_MENU = 3,
}
declare enum MFALevels {
@@ -1176,6 +1177,7 @@ declare module 'discord.js' {
public isButton(): this is ButtonInteraction;
public isCommand(): this is CommandInteraction;
public isMessageComponent(): this is MessageComponentInteraction;
+ public isSelectMenu(): this is SelectMenuInteraction;
}
export class InteractionWebhook extends PartialWebhookMixin() {
@@ -1531,6 +1533,29 @@ declare module 'discord.js' {
public toJSON(): unknown;
}
+ class MessageSelectMenu extends BaseMessageComponent {
+ constructor(data?: MessageSelectMenu | MessageSelectMenuOptions);
+ public customID: string | null;
+ public disabled: boolean;
+ public maxValues: number | null;
+ public minValues: number | null;
+ public options: MessageSelectOption[];
+ public placeholder: string | null;
+ public type: 'SELECT_MENU';
+ public addOptions(options: MessageSelectOptionData[] | MessageSelectOptionData[][]): this;
+ public setCustomID(customID: string): this;
+ public setDisabled(disabled: boolean): this;
+ public setMaxValues(maxValues: number): this;
+ public setMinValues(minValues: number): this;
+ public setPlaceholder(placeholder: string): this;
+ public spliceOptions(
+ index: number,
+ deleteCount: number,
+ ...options: MessageSelectOptionData[] | MessageSelectOptionData[][]
+ ): this;
+ public toJSON(): unknown;
+ }
+
export class NewsChannel extends TextBasedChannel(GuildChannel) {
constructor(guild: Guild, data?: unknown);
public defaultAutoArchiveDuration?: ThreadAutoArchiveDuration;
@@ -1693,6 +1718,11 @@ declare module 'discord.js' {
public static comparePositions(role1: Role, role2: Role): number;
}
+ export class SelectMenuInteraction extends MessageComponentInteraction {
+ public componentType: 'SELECT_MENU';
+ public values: string[] | null;
+ }
+
export class Shard extends EventEmitter {
constructor(manager: ShardingManager, id: number);
private _evals: Map>;
@@ -3182,6 +3212,7 @@ declare module 'discord.js' {
User: typeof User;
CommandInteraction: typeof CommandInteraction;
ButtonInteraction: typeof ButtonInteraction;
+ SelectMenuInteraction: typeof SelectMenuInteraction;
}
interface FetchApplicationCommandOptions extends BaseFetchOptions {
@@ -3572,9 +3603,9 @@ declare module 'discord.js' {
type MembershipState = keyof typeof MembershipStates;
- type MessageActionRowComponent = MessageButton;
+ type MessageActionRowComponent = MessageButton | MessageSelectMenu;
- type MessageActionRowComponentOptions = MessageButtonOptions;
+ type MessageActionRowComponentOptions = MessageButtonOptions | MessageSelectMenuOptions;
type MessageActionRowComponentResolvable = MessageActionRowComponent | MessageActionRowComponentOptions;
@@ -3587,6 +3618,8 @@ declare module 'discord.js' {
type: number;
}
+ type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[];
+
interface MessageButtonOptions extends BaseMessageComponentOptions {
customID?: string;
disabled?: boolean;
@@ -3605,7 +3638,7 @@ declare module 'discord.js' {
maxProcessed?: number;
}
- type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton;
+ type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton | MessageSelectMenu;
interface MessageComponentInteractionCollectorOptions extends CollectorOptions {
max?: number;
@@ -3613,7 +3646,11 @@ declare module 'discord.js' {
maxUsers?: number;
}
- type MessageComponentOptions = BaseMessageComponentOptions | MessageActionRowOptions | MessageButtonOptions;
+ type MessageComponentOptions =
+ | BaseMessageComponentOptions
+ | MessageActionRowOptions
+ | MessageButtonOptions
+ | MessageSelectMenuOptions;
type MessageComponentType = keyof typeof MessageComponentTypes;
@@ -3750,6 +3787,31 @@ declare module 'discord.js' {
type MessageResolvable = Message | Snowflake;
+ interface MessageSelectMenuOptions extends BaseMessageComponentOptions {
+ customID?: string;
+ disabled?: boolean;
+ maxValues?: number;
+ minValues?: number;
+ options?: MessageSelectOptionData[];
+ placeholder?: string;
+ }
+
+ interface MessageSelectOption {
+ default: boolean;
+ description: string | null;
+ emoji: RawEmoji | null;
+ label: string;
+ value: string;
+ }
+
+ interface MessageSelectOptionData {
+ default?: boolean;
+ description?: string;
+ emoji?: EmojiIdentifierResolvable;
+ label: string;
+ value: string;
+ }
+
type MessageTarget =
| Interaction
| InteractionWebhook