feat(MessageSelectMenu): droppybois (#5692)

Co-authored-by: Antonio Román <kyradiscord@gmail.com>
This commit is contained in:
monbrey
2021-06-25 07:25:16 +10:00
committed by GitHub
parent d984ac9d09
commit e5fcf0bee5
11 changed files with 336 additions and 14 deletions

View File

@@ -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,

View File

@@ -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}`,

View File

@@ -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'),

View File

@@ -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}`);

View File

@@ -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
);
}
}

View File

@@ -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(

View File

@@ -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
* <info>This will default the maxValues to the number of options, unless manually set</info>
* @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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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'),
};

70
typings/index.d.ts vendored
View File

@@ -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<string, Promise<any>>;
@@ -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