From 3745394eb0ab92f9238601577537c6bcc50d04db Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 9 Feb 2022 19:07:49 +1100 Subject: [PATCH 01/34] feat(modals): modals, input texts and submit interactions, v13 style --- src/client/actions/InteractionCreate.js | 4 + src/index.js | 3 + src/structures/BaseMessageComponent.js | 19 +- src/structures/InputTextComponent.js | 201 ++++++++++++++++++ src/structures/Interaction.js | 8 + src/structures/MessageComponentInteraction.js | 1 + src/structures/Modal.js | 100 +++++++++ src/structures/ModalActionRow.js | 99 +++++++++ src/structures/ModalSubmitInteraction.js | 36 ++++ .../interfaces/InteractionResponses.js | 17 ++ src/util/Constants.js | 13 +- typings/enums.d.ts | 13 ++ typings/index.d.ts | 128 ++++++++++- typings/rawDataTypes.d.ts | 24 ++- 14 files changed, 653 insertions(+), 13 deletions(-) create mode 100644 src/structures/InputTextComponent.js create mode 100644 src/structures/Modal.js create mode 100644 src/structures/ModalActionRow.js create mode 100644 src/structures/ModalSubmitInteraction.js diff --git a/src/client/actions/InteractionCreate.js b/src/client/actions/InteractionCreate.js index 04774a6b442b..c869ea6ef86a 100644 --- a/src/client/actions/InteractionCreate.js +++ b/src/client/actions/InteractionCreate.js @@ -6,6 +6,7 @@ const AutocompleteInteraction = require('../../structures/AutocompleteInteractio const ButtonInteraction = require('../../structures/ButtonInteraction'); const CommandInteraction = require('../../structures/CommandInteraction'); const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction'); +const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction'); const SelectMenuInteraction = require('../../structures/SelectMenuInteraction'); const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction'); const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants'); @@ -59,6 +60,9 @@ class InteractionCreateAction extends Action { case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: InteractionType = AutocompleteInteraction; break; + case InteractionTypes.MODAL_SUBMIT: + InteractionType = ModalSubmitInteraction; + break; default: client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`); return; diff --git a/src/index.js b/src/index.js index eb99a644de2e..63004f948dff 100644 --- a/src/index.js +++ b/src/index.js @@ -102,6 +102,7 @@ exports.GuildPreview = require('./structures/GuildPreview'); exports.GuildPreviewEmoji = require('./structures/GuildPreviewEmoji'); exports.GuildScheduledEvent = require('./structures/GuildScheduledEvent').GuildScheduledEvent; exports.GuildTemplate = require('./structures/GuildTemplate'); +exports.InputTextComponent = require('./structures/InputTextComponent'); exports.Integration = require('./structures/Integration'); exports.IntegrationApplication = require('./structures/IntegrationApplication'); exports.Interaction = require('./structures/Interaction'); @@ -122,6 +123,8 @@ exports.MessageMentions = require('./structures/MessageMentions'); exports.MessagePayload = require('./structures/MessagePayload'); exports.MessageReaction = require('./structures/MessageReaction'); exports.MessageSelectMenu = require('./structures/MessageSelectMenu'); +exports.Modal = require('./structures/Modal'); +exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction'); exports.NewsChannel = require('./structures/NewsChannel'); exports.OAuth2Guild = require('./structures/OAuth2Guild'); exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel'); diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index c2470e0c1d62..6fa11ca1b5c5 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -4,7 +4,7 @@ const { TypeError } = require('../errors'); const { MessageComponentTypes, Events } = require('../util/Constants'); /** - * Represents an interactive component of a Message. It should not be necessary to construct this directly. + * Represents an interactive component of a Message or Modal. It should not be necessary to construct this directly. * See {@link MessageComponent} */ class BaseMessageComponent { @@ -15,18 +15,20 @@ class BaseMessageComponent { */ /** - * Data that can be resolved into options for a MessageComponent. This can be: + * Data that can be resolved into options for a component. This can be: * * MessageActionRowOptions * * MessageButtonOptions * * MessageSelectMenuOptions + * * InputTextComponentOptions * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions */ /** - * Components that can be sent in a message. These can be: + * Components that can be sent in a payload. These can be: * * MessageActionRow * * MessageButton * * MessageSelectMenu + * * InputTextComponent * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} */ @@ -51,10 +53,10 @@ class BaseMessageComponent { } /** - * Constructs a MessageComponent based on the type of the incoming data + * Constructs a component based on the type of the incoming data * @param {MessageComponentOptions} data Data for a MessageComponent * @param {Client|WebhookClient} [client] Client constructing this component - * @returns {?MessageComponent} + * @returns {?(MessageComponent|ModalComponent)} * @private */ static create(data, client) { @@ -79,6 +81,11 @@ class BaseMessageComponent { component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data); break; } + case MessageComponentTypes.INPUT_TEXT: { + const InputTextComponent = require('./InputTextComponent'); + component = data instanceof InputTextComponent ? data : new InputTextComponent(data); + break; + } default: if (client) { client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`); @@ -90,7 +97,7 @@ class BaseMessageComponent { } /** - * Resolves the type of a MessageComponent + * Resolves the type of a component * @param {MessageComponentTypeResolvable} type The type to resolve * @returns {MessageComponentType} * @private diff --git a/src/structures/InputTextComponent.js b/src/structures/InputTextComponent.js new file mode 100644 index 000000000000..af59ad3fcd3f --- /dev/null +++ b/src/structures/InputTextComponent.js @@ -0,0 +1,201 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const { RangeError } = require('../errors'); +const { InputTextStyles, MessageComponentTypes } = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * Represents an input text component in a modal + * @extends {BaseMessageComponent} + */ + +class InputTextComponent extends BaseMessageComponent { + /** + * @typedef {BaseMessageComponentOptions} InputTextComponentOptions + * @property {string} [custom_id] A unique string to be sent in the interaction when submitted + * @property {string} [label] The text to be displayed above this input text component + * @property {number} [maxLength] Maximum length of text that can be entered + * @property {number} [minLength] Minimum length of text required to be entered + * @property {string} [placeholder] Custom placeholder text to display when no text is entered + * @property {boolean} [required] Whether or not this input text component is required + * @property {InputTextStyleResolvable} [style] The style of this input text component + * @property {string} [value] Value of this input text component + */ + + /** + * @param {InputTextComponent|InputTextComponentOptions} [data={}] InputTextComponent to clone or raw data + */ + constructor(data = {}) { + super({ type: 'INPUT_TEXT' }); + + this.setup(data); + } + + setup(data) { + /** + * A unique string to be sent in the interaction when submitted + * @type {?string} + */ + this.customId = data.custom_id ?? data.customId ?? null; + + /** + * The text to be displayed above this input text component + * @type {?string} + */ + this.label = data.label ?? null; + + /** + * Maximum length of text that can be entered + * @type {?number} + */ + this.maxLength = data.max_length ?? data.maxLength ?? null; + + /** + * Minimum length of text required to be entered + * @type {?string} + */ + this.minLength = data.min_length ?? data.minLength ?? null; + + /** + * Custom placeholder text to display when no text is entered + * @type {?string} + */ + this.placeholder = data.placeholder ?? null; + + /** + * Whether or not this input text component is required + * @type {?boolean} + */ + this.required = data.required ?? false; + + /** + * The style of this input text component + * @type {?InputTextStyle} + */ + this.style = data.style ? InputTextComponent.resolveStyle(data.style) : null; + + /** + * Value of this input text component + * @type {?string} + */ + this.value = data.value ?? null; + } + + /** + * Sets the custom id of this input text component + * @param {string} customId A unique string to be sent in the interaction when submitted + * @returns {InputTextComponent} + */ + setCustomId(customId) { + this.customId = Util.verifyString(customId, RangeError, 'INPUT_TEXT_CUSTOM_ID'); + return this; + } + + /** + * Sets the label of this input text component + * @param {string} label The text to be displayed above this input text component + * @returns {InputTextComponent} + */ + setLabel(label) { + this.label = Util.verifyString(label, RangeError, 'INPUT_TEXT_LABEL'); + return this; + } + + /** + * Sets the input text component to be required for modal submission + * @param {boolean} [required=true] Whether this input text component is required + * @returns {InputTextComponent} + */ + setRequired(required = true) { + this.required = required; + return this; + } + + /** + * Sets the maximum length of text input required in this input text component + * @param {number} maxLength Maximum length of text to be required + * @returns {InputTextComponent} + */ + setMaxLength(maxLength) { + this.maxLength = maxLength; + return this; + } + + /** + * Sets the minimum length of text input required in this input text component + * @param {number} minLength Minimum length of text to be required + * @returns {InputTextComponent} + */ + setMinLength(minLength) { + this.minLength = minLength; + return this; + } + + /** + * Sets the placeholder of this input text component + * @param {string} placeholder Custom placeholder text to display when no text is entered + * @returns {InputTextComponent} + */ + setPlaceholder(placeholder) { + this.placeholder = Util.verifyString(placeholder, RangeError, 'INPUT_TEXT_PLACEHOLDER'); + return this; + } + + /** + * Sets the style of this input text component + * @param {InputTextStyleResolvable} style The style of this input text component + * @returns {InputTextComponent} + */ + setStyle(style) { + this.style = InputTextComponent.resolveStyle(style); + return this; + } + + /** + * Sets the value of this input text component + * @param {string} value Value of this input text component + * @returns {InputTextComponent} + */ + setValue(value) { + this.value = Util.verifyString(value, RangeError, 'INPUT_TEXT_VALUE'); + return this; + } + + /** + * Transforms the input text component into a plain object + * @returns {APIInputText} The raw data of this input text component + */ + toJSON() { + return { + custom_id: this.customId, + label: this.label, + max_length: this.maxLength, + min_length: this.minLength, + placeholder: this.placeholder, + required: this.required, + style: InputTextStyles[this.style], + type: MessageComponentTypes[this.type], + value: this.value, + }; + } + + /** + * Data that can be resolved to an InputTextStyle. This can be + * * InputTextStyle + * * number + * @typedef {number|InputTextStyle} InputTextStyleResolvable + */ + + /** + * Resolves the style of an input tetx component + * @param {InputTextStyleResolvable} style The style to resolve + * @returns {InputTextStyle} + * @private + */ + static resolveStyle(style) { + return typeof style === 'string' ? style : InputTextStyles[style]; + } +} + +module.exports = InputTextComponent; diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index e1a5d410fa7e..df40be28e9a9 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -173,6 +173,14 @@ class Interaction extends Base { return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId !== 'undefined'; } + /** + * Indicates whether this interaction is a {@link ModalSubmitInteraction} + * @returns {boolean} + */ + isModalSubmit() { + return InteractionTypes[this.type] === InteractionTypes.MODAL_SUBMIT; + } + /** * Indicates whether this interaction is a {@link UserContextMenuInteraction} * @returns {boolean} diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index dae0107e4b72..673bb282aa46 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -101,6 +101,7 @@ class MessageComponentInteraction extends Interaction { followUp() {} deferUpdate() {} update() {} + presentModal() {} } InteractionResponses.applyToClass(MessageComponentInteraction); diff --git a/src/structures/Modal.js b/src/structures/Modal.js new file mode 100644 index 000000000000..fa38d94b1b0d --- /dev/null +++ b/src/structures/Modal.js @@ -0,0 +1,100 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const Util = require('../util/Util'); + +class Modal { + /** + * @typedef {object} ModalOptions + * @property {string} [customId] A unique string to be sent in the interaction when clicked + * @property {string} [title] The title to be displayed on this modal + * @property {ModalActionRow[]|ModalActionRow[]} [components] + * Action rows containing interactive components for the modal (input text components) + */ + + /** + * @param {Modal|ModalOptions} data Modal to clone or raw data + * @param {Client} client The client constructing this Modal, if provided + */ + constructor(data = {}, client = null) { + /** + * A list of ModalActionRows in the modal + * @type {ModalActionRow[]} + */ + this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? []; + + /** + * A unique string to be sent in the interaction when submitted + * @type {?string} + */ + this.customId = data.custom_id ?? data.customId ?? null; + + /** + * The title to be displayed on this modal + * @type {?string} + */ + this.title = data.title ?? null; + } + + /** + * Adds components to the modal. + * @param {...ModalActionRowResolvable[]} components The components to add + * @returns {Modal} + */ + addComponents(...components) { + this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); + return this; + } + + /** + * Sets the components of the modal. + * @param {...ModalActionRowResolvable[]} components The components to set + * @returns {Modal} + */ + setComponents(...components) { + this.spliceComponents(0, this.components.length, components); + return this; + } + + /** + * Sets the custom id for this modal + * @param {string} customId A unique string to be sent in the interaction when submitted + * @returns {Modal} + */ + setCustomId(customId) { + this.customId = Util.verifyString(customId, RangeError, 'MODAL_CUSTOM_ID'); + return this; + } + + /** + * Removes, replaces, and inserts components in the modal. + * @param {number} index The index to start at + * @param {number} deleteCount The number of components to remove + * @param {...ModalActionRowResolvable[]} [components] The replacing components + * @returns {Modal} + */ + spliceComponents(index, deleteCount, ...components) { + this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); + return this; + } + + /** + * Sets the title of this modal + * @param {string} title The title to be displayed on this modal + * @returns {Modal} + */ + setTitle(title) { + this.title = Util.verifyString(title, RangeError, 'MODAL_TITLE'); + return this; + } + + toJSON() { + return { + components: this.components.map(c => c.toJSON()), + custom_id: this.customId, + title: this.title, + }; + } +} + +module.exports = Modal; diff --git a/src/structures/ModalActionRow.js b/src/structures/ModalActionRow.js new file mode 100644 index 000000000000..e2ac951010ec --- /dev/null +++ b/src/structures/ModalActionRow.js @@ -0,0 +1,99 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const { MessageComponentTypes } = require('../util/Constants'); + +/** + * Represents an action row containing message components. + * @extends {BaseMessageComponent} + */ +class ModalActionRow extends BaseMessageComponent { + /** + * Components that can be placed in an action row + * * InputTextComponent + * @typedef {InputTextComponent} ModalActionRowComponent + */ + + /** + * Options for components that can be placed in an action row + * * InputTextComponentOptions + * @typedef {InputTextComponentOptions} ModalActionRowComponentOptions + */ + + /** + * Data that can be resolved into components that can be placed in an action row + * * ModalActionRowComponent + * * ModalActionRowComponentOptions + * @typedef {ModalActionRowComponent|ModalActionRowComponentOptions} ModalActionRowComponentResolvable + */ + + /** + * @typedef {BaseMessageComponentOptions} ModalActionRowOptions + * @property {ModalActionRowComponentResolvable[]} [components] + * The components to place in this action row + */ + + /** + * @param {ModalActionRow|ModalActionRowOptions} [data={}] ModalActionRow to clone or raw data + * @param {Client} [client] The client constructing this ModalActionRow, if provided + */ + constructor(data = {}, client = null) { + super({ type: 'ACTION_ROW' }); + + /** + * The components in this action row + * @type {ModalActionRowComponent[]} + */ + this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? []; + } + + /** + * Adds components to the action row. + * @param {...ModalActionRowComponentResolvable[]} components The components to add + * @returns {ModalActionRow} + */ + addComponents(...components) { + this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); + return this; + } + + /** + * Sets the components of the action row. + * @param {...ModalActionRowComponentResolvable[]} components The components to set + * @returns {ModalActionRow} + */ + setComponents(...components) { + this.spliceComponents(0, this.components.length, components); + return this; + } + + /** + * Removes, replaces, and inserts components in the action row. + * @param {number} index The index to start at + * @param {number} deleteCount The number of components to remove + * @param {...ModalActionRowComponentResolvable[]} [components] The replacing components + * @returns {ModalActionRow} + */ + spliceComponents(index, deleteCount, ...components) { + this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); + return this; + } + + /** + * Transforms the action row to a plain object. + * @returns {APIMessageComponent} The raw data of this action row + */ + toJSON() { + return { + components: this.components.map(c => c.toJSON()), + type: MessageComponentTypes[this.type], + }; + } +} + +module.exports = ModalActionRow; + +/** + * @external APIMessageComponent + * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object} + */ diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js new file mode 100644 index 000000000000..07cfc1b17a13 --- /dev/null +++ b/src/structures/ModalSubmitInteraction.js @@ -0,0 +1,36 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const Interaction = require('./Interaction'); +const InteractionResponses = require('./interfaces/InteractionResponses'); + +class ModalSubmitInteraction extends Interaction { + constructor(client, data) { + super(client, data); + + /** + * The custom id of the modal. + * @type {string} + */ + this.customId = data.data.custom_id; + + /** + * The inputs within the modal + * @type {Array>} + */ + this.components = data.data.components?.map(c => BaseMessageComponent.create(c, this.client)) ?? []; + } + + // These are here only for documentation purposes - they are implemented by InteractionResponses + /* eslint-disable no-empty-function */ + deferReply() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} +} + +InteractionResponses.applyToClass(ModalSubmitInteraction, ['deferUpdate', 'update', 'presentModal']); + +module.exports = ModalSubmitInteraction; diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index 17badddefb57..f7c44df9ebff 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -4,6 +4,7 @@ const { Error } = require('../../errors'); const { InteractionResponseTypes } = require('../../util/Constants'); const MessageFlags = require('../../util/MessageFlags'); const MessagePayload = require('../MessagePayload'); +const Modal = require('../Modal'); /** * Interface for classes that support shared interaction response types. @@ -226,6 +227,21 @@ class InteractionResponses { return options.fetchReply ? this.fetchReply() : undefined; } + /** + * Presents a modal component + * @param {Modal|ModalOptions} modal The modal to present + * @returns {Promise} + */ + async presentModal(modal) { + const _modal = modal instanceof Modal ? modal : new Modal(modal); + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.MODAL, + data: _modal.toJSON(), + }, + }); + } + static applyToClass(structure, ignore = []) { const props = [ 'deferReply', @@ -236,6 +252,7 @@ class InteractionResponses { 'followUp', 'deferUpdate', 'update', + 'presentModal', ]; for (const prop of props) { diff --git a/src/util/Constants.js b/src/util/Constants.js index f9464442b14e..70cd3c0c3c17 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1061,6 +1061,15 @@ exports.ApplicationCommandOptionTypes = createEnum([ */ exports.ApplicationCommandPermissionTypes = createEnum([null, 'ROLE', 'USER']); +/** + * The style of an input text component + * * SHORT + * * LONG + * @typedef {string} InputTextStyle + * @see {@link https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles} + */ +exports.InputTextStyles = createEnum([null, 'SHORT', 'LONG']); + /** * The type of an {@link Interaction} object: * * PING @@ -1076,6 +1085,7 @@ exports.InteractionTypes = createEnum([ 'APPLICATION_COMMAND', 'MESSAGE_COMPONENT', 'APPLICATION_COMMAND_AUTOCOMPLETE', + 'MODAL_SUBMIT', ]); /** @@ -1099,6 +1109,7 @@ exports.InteractionResponseTypes = createEnum([ 'DEFERRED_MESSAGE_UPDATE', 'UPDATE_MESSAGE', 'APPLICATION_COMMAND_AUTOCOMPLETE_RESULT', + 'MODAL', ]); /** @@ -1109,7 +1120,7 @@ exports.InteractionResponseTypes = createEnum([ * @typedef {string} MessageComponentType * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} */ -exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU']); +exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU', 'INPUT_TEXT']); /** * The style of a message button diff --git a/typings/enums.d.ts b/typings/enums.d.ts index d67d310eb5f2..34d85cb2a579 100644 --- a/typings/enums.d.ts +++ b/typings/enums.d.ts @@ -104,6 +104,11 @@ export const enum GuildScheduledEventStatuses { CANCELED = 4, } +export const enum InputTextStyles { + SHORT = 1, + LONG = 2, +} + export const enum InteractionResponseTypes { PONG = 1, CHANNEL_MESSAGE_WITH_SOURCE = 4, @@ -111,6 +116,7 @@ export const enum InteractionResponseTypes { DEFERRED_MESSAGE_UPDATE = 6, UPDATE_MESSAGE = 7, APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8, + MODAL = 9, } export const enum InteractionTypes { @@ -118,6 +124,7 @@ export const enum InteractionTypes { APPLICATION_COMMAND = 2, MESSAGE_COMPONENT = 3, APPLICATION_COMMAND_AUTOCOMPLETE = 4, + MODAL_SUBMIT = 5, } export const enum InviteTargetType { @@ -142,6 +149,12 @@ export const enum MessageComponentTypes { ACTION_ROW = 1, BUTTON = 2, SELECT_MENU = 3, + INPUT_TEXT = 4, +} + +export const enum ModalComponentTypes { + ACTION_ROW = 1, + INPUT_TEXT = 4, } export const enum MFALevels { diff --git a/typings/index.d.ts b/typings/index.d.ts index de4e3f486586..ddb31ee33845 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -64,6 +64,7 @@ import { ChannelTypes, DefaultMessageNotificationLevels, ExplicitContentFilterLevels, + InputTextStyles, InteractionResponseTypes, InteractionTypes, InviteTargetType, @@ -71,6 +72,7 @@ import { MessageButtonStyles, MessageComponentTypes, MessageTypes, + ModalComponentTypes, MFALevels, NSFWLevels, OverwriteTypes, @@ -117,6 +119,8 @@ import { RawMessagePayloadData, RawMessageReactionData, RawMessageSelectMenuInteractionData, + RawModalActionRowComponentData, + RawModalSubmitInteractionData, RawOAuth2GuildData, RawPartialGroupDMChannelData, RawPartialMessageData, @@ -353,6 +357,7 @@ export abstract class BaseCommandInteraction>; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; + public presentModal(modal: Modal | ModalOptions): Promise; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; private transformOption( @@ -1259,6 +1264,28 @@ export class RateLimitError extends Error { public name: 'RateLimitError'; } +export class InputTextComponent extends BaseMessageComponent { + public constructor(data?: InputTextComponent | InputTextComponentOptions); + public customId: string | null; + public label: string | null; + public required: boolean; + public maxLength: number | null; + public minLength: number | null; + public placeholder: string | null; + public style: InputTextStyle; + public value: string | null; + public setCustomId(customId: string): this; + public setLabel(label: string): this; + public setRequired(required: boolean): this; + public setMaxLength(maxLength: number): this; + public setMinLength(minLength: number): this; + public setPlaceholder(placeholder: string): this; + public setStyle(style: InputTextStyleResolvable): this; + public setValue(value: string): this; + public toJSON(): unknown; + public static resolveStyle(style: InputTextStyleResolvable): InputTextStyle; +} + export class Integration extends Base { private constructor(client: Client, data: RawIntegrationData, guild: Guild); public account: IntegrationAccount; @@ -1351,6 +1378,7 @@ export class Interaction extends Base { public isUserContextMenu(): this is UserContextMenuInteraction; public isMessageContextMenu(): this is MessageContextMenuInteraction; public isMessageComponent(): this is MessageComponentInteraction; + public isModalSubmit(): this is ModalSubmitInteraction; public isSelectMenu(): this is SelectMenuInteraction; } @@ -1485,6 +1513,7 @@ export type MappedInteractionTypes = EnumValue BUTTON: ButtonInteraction>; SELECT_MENU: SelectMenuInteraction>; ACTION_ROW: MessageComponentInteraction>; + INPUT_TEXT: ModalSubmitInteraction>; } >; @@ -1580,6 +1609,24 @@ export class MessageActionRow extends BaseMessageComponent { public toJSON(): APIActionRowComponent; } +export class ModalActionRow extends BaseMessageComponent { + public constructor(data?: ModalActionRow | ModalActionRowOptions | RawModalActionRowComponentData); + public type: 'ACTION_ROW'; + public components: ModalActionRowComponent[]; + public addComponents( + ...components: ModalActionRowComponentResolvable[] | ModalActionRowComponentResolvable[][] + ): this; + public setComponents( + ...components: ModalActionRowComponentResolvable[] | ModalActionRowComponentResolvable[][] + ): this; + public spliceComponents( + index: number, + deleteCount: number, + ...components: ModalActionRowComponentResolvable[] | ModalActionRowComponentResolvable[][] + ): this; + public toJSON(): RawModalActionRowComponentData; +} + export class MessageAttachment { public constructor(attachment: BufferResolvable | Stream, name?: string, data?: RawMessageAttachmentData); @@ -1663,6 +1710,7 @@ export class MessageComponentInteraction e public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise>; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; + public presentModal(modal: Modal | ModalOptions): Promise; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise>; @@ -1826,6 +1874,44 @@ export class MessageSelectMenu extends BaseMessageComponent { public toJSON(): APISelectMenuComponent; } +export class Modal { + public constructor(data?: Modal | ModalOptions); + public components: MessageActionRow[]; + public customId: string; + public title: string; + public addComponents( + ...components: (ModalActionRow | (Required & ModalActionRowOptions))[] + ): this; + public setComponents( + ...components: (ModalActionRow | (Required & ModalActionRowOptions))[] + ): this; + public setCustomId(customId: string): this; + public spliceComponents( + index: number, + deleteCount: number, + ...components: (ModalActionRow | (Required & ModalActionRowOptions))[] + ): this; + public setTitle(title: string): this; + public toJSON(): APIModal; +} + +export class ModalSubmitInteraction extends Interaction { + protected constructor(client: Client, data: RawModalSubmitInteractionData); + public customId: string; + public components: ModalActionRow[]; + public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; + public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; + public deleteReply(): Promise; + public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise>; + public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise>; + public deferReply(options?: InteractionDeferReplyOptions): Promise; + public fetchReply(): Promise>; + public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; + public inGuild(): this is ModalSubmitInteraction<'present'>; + public inCachedGuild(): this is ModalSubmitInteraction<'cached'>; + public inRawGuild(): this is ModalSubmitInteraction<'raw'>; +} + export class NewsChannel extends BaseGuildTextChannel { public threads: ThreadManager; public type: 'GUILD_NEWS'; @@ -2845,10 +2931,12 @@ export const Constants: { MembershipStates: EnumHolder; ApplicationCommandOptionTypes: EnumHolder; ApplicationCommandPermissionTypes: EnumHolder; + InputTextStyles: EnumHolder; InteractionTypes: EnumHolder; InteractionResponseTypes: EnumHolder; MessageComponentTypes: EnumHolder; MessageButtonStyles: EnumHolder; + ModalComponentTypes: EnumHolder; MFALevels: EnumHolder; NSFWLevels: EnumHolder; PrivacyLevels: EnumHolder; @@ -3555,6 +3643,8 @@ export interface APIErrors { FAILED_TO_CREATE_STAGE_NEEDED_FOR_STAGE_EVENT: 180002; } +export interface APIModal {} + export interface APIRequest { method: 'get' | 'post' | 'delete' | 'patch' | 'put'; options: unknown; @@ -4887,6 +4977,20 @@ export interface ImageURLOptions extends Omit { format?: DynamicImageFormat; } +export interface InputTextComponentOptions extends BaseMessageComponentOptions { + customId?: string; + label?: string; + minLength?: number; + maxLength?: number; + placeholder?: string; + required?: boolean; + style?: InputTextStyleResolvable; + value?: string; +} + +export type InputTextStyle = keyof typeof InputTextStyles; + +export type InputTextStyleResolvable = InputTextStyle | InputTextStyles; export interface IntegrationAccount { id: string | Snowflake; name: string; @@ -5013,10 +5117,20 @@ export type MessageActionRowComponentOptions = export type MessageActionRowComponentResolvable = MessageActionRowComponent | MessageActionRowComponentOptions; +export type ModalActionRowComponent = InputTextComponent; + +export type ModalActionRowComponentOptions = InputTextComponentOptions; + +export type ModalActionRowComponentResolvable = ModalActionRowComponent | ModalActionRowComponentOptions; + export interface MessageActionRowOptions extends BaseMessageComponentOptions { components: MessageActionRowComponentResolvable[]; } +export interface ModalActionRowOptions extends BaseMessageComponentOptions { + components: ModalActionRowComponentResolvable[]; +} + export interface MessageActivity { partyId: string; type: number; @@ -5051,15 +5165,13 @@ export interface MessageCollectorOptions extends CollectorOptions<[Message]> { export type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton | MessageSelectMenu; -export type MessageComponentCollectorOptions = Omit< +export type MessageComponentCollectorOptions = Omit< InteractionCollectorOptions, 'channel' | 'message' | 'guild' | 'interactionType' >; -export type MessageChannelComponentCollectorOptions = Omit< - InteractionCollectorOptions, - 'channel' | 'guild' | 'interactionType' ->; +export type MessageChannelComponentCollectorOptions = + Omit, 'channel' | 'guild' | 'interactionType'>; export type MessageComponentOptions = | BaseMessageComponentOptions @@ -5243,6 +5355,12 @@ export type MessageType = keyof typeof MessageTypes; export type MFALevel = keyof typeof MFALevels; +export interface ModalOptions { + components: MessageActionRow[] | MessageActionRowOptions[]; + customId: string; + title: string; +} + export interface MultipleShardRespawnOptions { shardDelay?: number; respawnDelay?: number; diff --git a/typings/rawDataTypes.d.ts b/typings/rawDataTypes.d.ts index 885566d40d0c..2777b9dbb35d 100644 --- a/typings/rawDataTypes.d.ts +++ b/typings/rawDataTypes.d.ts @@ -76,8 +76,10 @@ import { RESTPostAPIWebhookWithTokenJSONBody, Snowflake, APIGuildScheduledEvent, + APIActionRowComponent, } from 'discord-api-types/v9'; -import { GuildChannel, Guild, PermissionOverwrites } from '.'; +import { GuildChannel, Guild, PermissionOverwrites, InteractionType } from '.'; +import type { InteractionTypes, MessageComponentTypes } from './enums'; export type RawActivityData = GatewayActivity; @@ -141,6 +143,26 @@ export type RawMessageComponentInteractionData = APIMessageComponentInteraction; export type RawMessageButtonInteractionData = APIMessageButtonInteractionData; export type RawMessageSelectMenuInteractionData = APIMessageSelectMenuInteractionData; +// TODO: Replace with discord-api-types definition +export type RawInputTextComponentData = { + type: MessageComponentTypes.INPUT_TEXT; + custom_id: string; + value: string; +}; + +// TODO: Replace with discord-api-types definition +export type RawModalActionRowComponentData = { + custom_id: string; + components: RawInputTextComponentData; +}; + +// TODO: Replace with discord-api-types definition +export type RawModalSubmitInteractionData = { + custom_id: string; + type: InteractionTypes.MODAL_SUBMIT; + components: RawModalActionRowComponentData[]; +}; + export type RawInviteData = | APIExtendedInvite | APIInvite From d536ec73b48d5284a63032bb295c8e74698fe2f2 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 9 Feb 2022 19:10:40 +1100 Subject: [PATCH 02/34] fix(types): raw modal data --- typings/index.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index ddb31ee33845..c5dfd82812b7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1892,7 +1892,7 @@ export class Modal { ...components: (ModalActionRow | (Required & ModalActionRowOptions))[] ): this; public setTitle(title: string): this; - public toJSON(): APIModal; + public toJSON(): RawModalSubmitInteractionData; } export class ModalSubmitInteraction extends Interaction { @@ -3643,8 +3643,6 @@ export interface APIErrors { FAILED_TO_CREATE_STAGE_NEEDED_FOR_STAGE_EVENT: 180002; } -export interface APIModal {} - export interface APIRequest { method: 'get' | 'post' | 'delete' | 'patch' | 'put'; options: unknown; From f73dcd849e952f888e470e406e2d89e856f2964b Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 9 Feb 2022 19:25:39 +1100 Subject: [PATCH 03/34] feat(types): attempt at making the action row types generic --- typings/index.d.ts | 70 +++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c5dfd82812b7..a1753314020f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1591,42 +1591,19 @@ export class Message extends Base { public inGuild(): this is Message & this; } -export class MessageActionRow extends BaseMessageComponent { - public constructor(data?: MessageActionRow | MessageActionRowOptions | APIActionRowComponent); +export class MessageActionRow< + T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent, + U = T extends ModalActionRowComponent ? ModalActionRowComponentResolvable : MessageActionRowComponentResolvable, +> extends BaseMessageComponent { + public constructor(data?: MessageActionRow | MessageActionRowOptions | APIActionRowComponent); public type: 'ACTION_ROW'; - public components: MessageActionRowComponent[]; - public addComponents( - ...components: MessageActionRowComponentResolvable[] | MessageActionRowComponentResolvable[][] - ): this; - public setComponents( - ...components: MessageActionRowComponentResolvable[] | MessageActionRowComponentResolvable[][] - ): this; - public spliceComponents( - index: number, - deleteCount: number, - ...components: MessageActionRowComponentResolvable[] | MessageActionRowComponentResolvable[][] - ): this; + public components: T[]; + public addComponents(...components: U[] | U[][]): this; + public setComponents(...components: U[] | U[][]): this; + public spliceComponents(index: number, deleteCount: number, ...components: U[] | U[][]): this; public toJSON(): APIActionRowComponent; } -export class ModalActionRow extends BaseMessageComponent { - public constructor(data?: ModalActionRow | ModalActionRowOptions | RawModalActionRowComponentData); - public type: 'ACTION_ROW'; - public components: ModalActionRowComponent[]; - public addComponents( - ...components: ModalActionRowComponentResolvable[] | ModalActionRowComponentResolvable[][] - ): this; - public setComponents( - ...components: ModalActionRowComponentResolvable[] | ModalActionRowComponentResolvable[][] - ): this; - public spliceComponents( - index: number, - deleteCount: number, - ...components: ModalActionRowComponentResolvable[] | ModalActionRowComponentResolvable[][] - ): this; - public toJSON(): RawModalActionRowComponentData; -} - export class MessageAttachment { public constructor(attachment: BufferResolvable | Stream, name?: string, data?: RawMessageAttachmentData); @@ -1880,16 +1857,25 @@ export class Modal { public customId: string; public title: string; public addComponents( - ...components: (ModalActionRow | (Required & ModalActionRowOptions))[] + ...components: ( + | MessageActionRow + | (Required & MessageActionRowOptions) + )[] ): this; public setComponents( - ...components: (ModalActionRow | (Required & ModalActionRowOptions))[] + ...components: ( + | MessageActionRow + | (Required & MessageActionRowOptions) + )[] ): this; public setCustomId(customId: string): this; public spliceComponents( index: number, deleteCount: number, - ...components: (ModalActionRow | (Required & ModalActionRowOptions))[] + ...components: ( + | MessageActionRow + | (Required & MessageActionRowOptions) + )[] ): this; public setTitle(title: string): this; public toJSON(): RawModalSubmitInteractionData; @@ -1898,7 +1884,7 @@ export class Modal { export class ModalSubmitInteraction extends Interaction { protected constructor(client: Client, data: RawModalSubmitInteractionData); public customId: string; - public components: ModalActionRow[]; + public components: MessageActionRow[]; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; public deleteReply(): Promise; @@ -5121,12 +5107,12 @@ export type ModalActionRowComponentOptions = InputTextComponentOptions; export type ModalActionRowComponentResolvable = ModalActionRowComponent | ModalActionRowComponentOptions; -export interface MessageActionRowOptions extends BaseMessageComponentOptions { - components: MessageActionRowComponentResolvable[]; -} - -export interface ModalActionRowOptions extends BaseMessageComponentOptions { - components: ModalActionRowComponentResolvable[]; +export interface MessageActionRowOptions< + T extends + | MessageActionRowComponentResolvable + | ModalActionRowComponentResolvable = MessageActionRowComponentResolvable, +> extends BaseMessageComponentOptions { + components: T[]; } export interface MessageActivity { From 20a2c8fa34b7bcc548d0b105654c9767d4f62498 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 9 Feb 2022 19:32:55 +1100 Subject: [PATCH 04/34] refactor: remove the separate ModalActionRow --- src/structures/MessageActionRow.js | 6 +- src/structures/Modal.js | 12 +-- src/structures/ModalActionRow.js | 99 ------------------------ src/structures/ModalSubmitInteraction.js | 2 +- 4 files changed, 11 insertions(+), 108 deletions(-) delete mode 100644 src/structures/ModalActionRow.js diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index c44b32a6018a..b1f346b2d77d 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -12,14 +12,16 @@ class MessageActionRow extends BaseMessageComponent { * Components that can be placed in an action row * * MessageButton * * MessageSelectMenu - * @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent + * * InputTextComponent + * @typedef {MessageButton|MessageSelectMenu|InputTextComponent} MessageActionRowComponent */ /** * Options for components that can be placed in an action row * * MessageButtonOptions * * MessageSelectMenuOptions - * @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions + * * InputTextComponentOptions + * @typedef {MessageButtonOptions|MessageSelectMenuOptions|InputTextComponentOptions} MessageActionRowComponentOptions */ /** diff --git a/src/structures/Modal.js b/src/structures/Modal.js index fa38d94b1b0d..bbee17be462f 100644 --- a/src/structures/Modal.js +++ b/src/structures/Modal.js @@ -8,7 +8,7 @@ class Modal { * @typedef {object} ModalOptions * @property {string} [customId] A unique string to be sent in the interaction when clicked * @property {string} [title] The title to be displayed on this modal - * @property {ModalActionRow[]|ModalActionRow[]} [components] + * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] * Action rows containing interactive components for the modal (input text components) */ @@ -18,8 +18,8 @@ class Modal { */ constructor(data = {}, client = null) { /** - * A list of ModalActionRows in the modal - * @type {ModalActionRow[]} + * A list of MessageActionRows in the modal + * @type {MessageActionRow[]} */ this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? []; @@ -38,7 +38,7 @@ class Modal { /** * Adds components to the modal. - * @param {...ModalActionRowResolvable[]} components The components to add + * @param {...MessageActionRowResolvable[]} components The components to add * @returns {Modal} */ addComponents(...components) { @@ -48,7 +48,7 @@ class Modal { /** * Sets the components of the modal. - * @param {...ModalActionRowResolvable[]} components The components to set + * @param {...MessageActionRowResolvable[]} components The components to set * @returns {Modal} */ setComponents(...components) { @@ -70,7 +70,7 @@ class Modal { * Removes, replaces, and inserts components in the modal. * @param {number} index The index to start at * @param {number} deleteCount The number of components to remove - * @param {...ModalActionRowResolvable[]} [components] The replacing components + * @param {...MessageActionRowResolvable[]} [components] The replacing components * @returns {Modal} */ spliceComponents(index, deleteCount, ...components) { diff --git a/src/structures/ModalActionRow.js b/src/structures/ModalActionRow.js deleted file mode 100644 index e2ac951010ec..000000000000 --- a/src/structures/ModalActionRow.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -const BaseMessageComponent = require('./BaseMessageComponent'); -const { MessageComponentTypes } = require('../util/Constants'); - -/** - * Represents an action row containing message components. - * @extends {BaseMessageComponent} - */ -class ModalActionRow extends BaseMessageComponent { - /** - * Components that can be placed in an action row - * * InputTextComponent - * @typedef {InputTextComponent} ModalActionRowComponent - */ - - /** - * Options for components that can be placed in an action row - * * InputTextComponentOptions - * @typedef {InputTextComponentOptions} ModalActionRowComponentOptions - */ - - /** - * Data that can be resolved into components that can be placed in an action row - * * ModalActionRowComponent - * * ModalActionRowComponentOptions - * @typedef {ModalActionRowComponent|ModalActionRowComponentOptions} ModalActionRowComponentResolvable - */ - - /** - * @typedef {BaseMessageComponentOptions} ModalActionRowOptions - * @property {ModalActionRowComponentResolvable[]} [components] - * The components to place in this action row - */ - - /** - * @param {ModalActionRow|ModalActionRowOptions} [data={}] ModalActionRow to clone or raw data - * @param {Client} [client] The client constructing this ModalActionRow, if provided - */ - constructor(data = {}, client = null) { - super({ type: 'ACTION_ROW' }); - - /** - * The components in this action row - * @type {ModalActionRowComponent[]} - */ - this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? []; - } - - /** - * Adds components to the action row. - * @param {...ModalActionRowComponentResolvable[]} components The components to add - * @returns {ModalActionRow} - */ - addComponents(...components) { - this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); - return this; - } - - /** - * Sets the components of the action row. - * @param {...ModalActionRowComponentResolvable[]} components The components to set - * @returns {ModalActionRow} - */ - setComponents(...components) { - this.spliceComponents(0, this.components.length, components); - return this; - } - - /** - * Removes, replaces, and inserts components in the action row. - * @param {number} index The index to start at - * @param {number} deleteCount The number of components to remove - * @param {...ModalActionRowComponentResolvable[]} [components] The replacing components - * @returns {ModalActionRow} - */ - spliceComponents(index, deleteCount, ...components) { - this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); - return this; - } - - /** - * Transforms the action row to a plain object. - * @returns {APIMessageComponent} The raw data of this action row - */ - toJSON() { - return { - components: this.components.map(c => c.toJSON()), - type: MessageComponentTypes[this.type], - }; - } -} - -module.exports = ModalActionRow; - -/** - * @external APIMessageComponent - * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object} - */ diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 07cfc1b17a13..0a2d42c66530 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -16,7 +16,7 @@ class ModalSubmitInteraction extends Interaction { /** * The inputs within the modal - * @type {Array>} + * @type {Array>} */ this.components = data.data.components?.map(c => BaseMessageComponent.create(c, this.client)) ?? []; } From aff3756f9f2e2359722f596b2002f28bd5f0a813 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 9 Feb 2022 21:33:28 +1100 Subject: [PATCH 05/34] fix(inputtext): fix customId casing --- src/structures/InputTextComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/InputTextComponent.js b/src/structures/InputTextComponent.js index af59ad3fcd3f..fc0582e89465 100644 --- a/src/structures/InputTextComponent.js +++ b/src/structures/InputTextComponent.js @@ -13,7 +13,7 @@ const Util = require('../util/Util'); class InputTextComponent extends BaseMessageComponent { /** * @typedef {BaseMessageComponentOptions} InputTextComponentOptions - * @property {string} [custom_id] A unique string to be sent in the interaction when submitted + * @property {string} [customId] A unique string to be sent in the interaction when submitted * @property {string} [label] The text to be displayed above this input text component * @property {number} [maxLength] Maximum length of text that can be entered * @property {number} [minLength] Minimum length of text required to be entered From 72881d859215ecfa71a6f89a4d2120782ed029fb Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 9 Feb 2022 23:28:36 +1100 Subject: [PATCH 06/34] refactor: rename component --- src/index.js | 2 +- src/structures/BaseMessageComponent.js | 8 +- src/structures/MessageActionRow.js | 8 +- ...TextComponent.js => TextInputComponent.js} | 52 +++++------ src/util/Constants.js | 18 ++-- typings/enums.d.ts | 10 +-- typings/index.d.ts | 90 +++++++++---------- typings/rawDataTypes.d.ts | 4 +- 8 files changed, 96 insertions(+), 96 deletions(-) rename src/structures/{InputTextComponent.js => TextInputComponent.js} (78%) diff --git a/src/index.js b/src/index.js index 63004f948dff..9e7906928feb 100644 --- a/src/index.js +++ b/src/index.js @@ -102,7 +102,6 @@ exports.GuildPreview = require('./structures/GuildPreview'); exports.GuildPreviewEmoji = require('./structures/GuildPreviewEmoji'); exports.GuildScheduledEvent = require('./structures/GuildScheduledEvent').GuildScheduledEvent; exports.GuildTemplate = require('./structures/GuildTemplate'); -exports.InputTextComponent = require('./structures/InputTextComponent'); exports.Integration = require('./structures/Integration'); exports.IntegrationApplication = require('./structures/IntegrationApplication'); exports.Interaction = require('./structures/Interaction'); @@ -143,6 +142,7 @@ exports.StoreChannel = require('./structures/StoreChannel'); exports.Team = require('./structures/Team'); exports.TeamMember = require('./structures/TeamMember'); exports.TextChannel = require('./structures/TextChannel'); +exports.TextInputComponent = require('./structures/TextInputComponent'); exports.ThreadChannel = require('./structures/ThreadChannel'); exports.ThreadMember = require('./structures/ThreadMember'); exports.Typing = require('./structures/Typing'); diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index 6fa11ca1b5c5..da939e0c0e39 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -19,7 +19,7 @@ class BaseMessageComponent { * * MessageActionRowOptions * * MessageButtonOptions * * MessageSelectMenuOptions - * * InputTextComponentOptions + * * TextInputComponentOptions * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions */ @@ -28,7 +28,7 @@ class BaseMessageComponent { * * MessageActionRow * * MessageButton * * MessageSelectMenu - * * InputTextComponent + * * TextInputComponent * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} */ @@ -82,8 +82,8 @@ class BaseMessageComponent { break; } case MessageComponentTypes.INPUT_TEXT: { - const InputTextComponent = require('./InputTextComponent'); - component = data instanceof InputTextComponent ? data : new InputTextComponent(data); + const TextInputComponent = require('./TextInputComponent'); + component = data instanceof TextInputComponent ? data : new TextInputComponent(data); break; } default: diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js index b1f346b2d77d..d2115d6c0594 100644 --- a/src/structures/MessageActionRow.js +++ b/src/structures/MessageActionRow.js @@ -12,16 +12,16 @@ class MessageActionRow extends BaseMessageComponent { * Components that can be placed in an action row * * MessageButton * * MessageSelectMenu - * * InputTextComponent - * @typedef {MessageButton|MessageSelectMenu|InputTextComponent} MessageActionRowComponent + * * TextInputComponent + * @typedef {MessageButton|MessageSelectMenu|TextInputComponent} MessageActionRowComponent */ /** * Options for components that can be placed in an action row * * MessageButtonOptions * * MessageSelectMenuOptions - * * InputTextComponentOptions - * @typedef {MessageButtonOptions|MessageSelectMenuOptions|InputTextComponentOptions} MessageActionRowComponentOptions + * * TextInputComponentOptions + * @typedef {MessageButtonOptions|MessageSelectMenuOptions|TextInputComponentOptions} MessageActionRowComponentOptions */ /** diff --git a/src/structures/InputTextComponent.js b/src/structures/TextInputComponent.js similarity index 78% rename from src/structures/InputTextComponent.js rename to src/structures/TextInputComponent.js index fc0582e89465..f8c8322fe49f 100644 --- a/src/structures/InputTextComponent.js +++ b/src/structures/TextInputComponent.js @@ -2,7 +2,7 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const { RangeError } = require('../errors'); -const { InputTextStyles, MessageComponentTypes } = require('../util/Constants'); +const { TextInputStyles, MessageComponentTypes } = require('../util/Constants'); const Util = require('../util/Util'); /** @@ -10,21 +10,21 @@ const Util = require('../util/Util'); * @extends {BaseMessageComponent} */ -class InputTextComponent extends BaseMessageComponent { +class TextInputComponent extends BaseMessageComponent { /** - * @typedef {BaseMessageComponentOptions} InputTextComponentOptions + * @typedef {BaseMessageComponentOptions} TextInputComponentOptions * @property {string} [customId] A unique string to be sent in the interaction when submitted * @property {string} [label] The text to be displayed above this input text component * @property {number} [maxLength] Maximum length of text that can be entered * @property {number} [minLength] Minimum length of text required to be entered * @property {string} [placeholder] Custom placeholder text to display when no text is entered * @property {boolean} [required] Whether or not this input text component is required - * @property {InputTextStyleResolvable} [style] The style of this input text component + * @property {TextInputStyleResolvable} [style] The style of this input text component * @property {string} [value] Value of this input text component */ /** - * @param {InputTextComponent|InputTextComponentOptions} [data={}] InputTextComponent to clone or raw data + * @param {TextInputComponent|TextInputComponentOptions} [data={}] TextInputComponent to clone or raw data */ constructor(data = {}) { super({ type: 'INPUT_TEXT' }); @@ -71,9 +71,9 @@ class InputTextComponent extends BaseMessageComponent { /** * The style of this input text component - * @type {?InputTextStyle} + * @type {?TextInputStyle} */ - this.style = data.style ? InputTextComponent.resolveStyle(data.style) : null; + this.style = data.style ? TextInputComponent.resolveStyle(data.style) : null; /** * Value of this input text component @@ -85,7 +85,7 @@ class InputTextComponent extends BaseMessageComponent { /** * Sets the custom id of this input text component * @param {string} customId A unique string to be sent in the interaction when submitted - * @returns {InputTextComponent} + * @returns {TextInputComponent} */ setCustomId(customId) { this.customId = Util.verifyString(customId, RangeError, 'INPUT_TEXT_CUSTOM_ID'); @@ -95,7 +95,7 @@ class InputTextComponent extends BaseMessageComponent { /** * Sets the label of this input text component * @param {string} label The text to be displayed above this input text component - * @returns {InputTextComponent} + * @returns {TextInputComponent} */ setLabel(label) { this.label = Util.verifyString(label, RangeError, 'INPUT_TEXT_LABEL'); @@ -105,7 +105,7 @@ class InputTextComponent extends BaseMessageComponent { /** * Sets the input text component to be required for modal submission * @param {boolean} [required=true] Whether this input text component is required - * @returns {InputTextComponent} + * @returns {TextInputComponent} */ setRequired(required = true) { this.required = required; @@ -115,7 +115,7 @@ class InputTextComponent extends BaseMessageComponent { /** * Sets the maximum length of text input required in this input text component * @param {number} maxLength Maximum length of text to be required - * @returns {InputTextComponent} + * @returns {TextInputComponent} */ setMaxLength(maxLength) { this.maxLength = maxLength; @@ -125,7 +125,7 @@ class InputTextComponent extends BaseMessageComponent { /** * Sets the minimum length of text input required in this input text component * @param {number} minLength Minimum length of text to be required - * @returns {InputTextComponent} + * @returns {TextInputComponent} */ setMinLength(minLength) { this.minLength = minLength; @@ -135,7 +135,7 @@ class InputTextComponent extends BaseMessageComponent { /** * Sets the placeholder of this input text component * @param {string} placeholder Custom placeholder text to display when no text is entered - * @returns {InputTextComponent} + * @returns {TextInputComponent} */ setPlaceholder(placeholder) { this.placeholder = Util.verifyString(placeholder, RangeError, 'INPUT_TEXT_PLACEHOLDER'); @@ -144,18 +144,18 @@ class InputTextComponent extends BaseMessageComponent { /** * Sets the style of this input text component - * @param {InputTextStyleResolvable} style The style of this input text component - * @returns {InputTextComponent} + * @param {TextInputStyleResolvable} style The style of this input text component + * @returns {TextInputComponent} */ setStyle(style) { - this.style = InputTextComponent.resolveStyle(style); + this.style = TextInputComponent.resolveStyle(style); return this; } /** * Sets the value of this input text component * @param {string} value Value of this input text component - * @returns {InputTextComponent} + * @returns {TextInputComponent} */ setValue(value) { this.value = Util.verifyString(value, RangeError, 'INPUT_TEXT_VALUE'); @@ -164,7 +164,7 @@ class InputTextComponent extends BaseMessageComponent { /** * Transforms the input text component into a plain object - * @returns {APIInputText} The raw data of this input text component + * @returns {APITextInput} The raw data of this input text component */ toJSON() { return { @@ -174,28 +174,28 @@ class InputTextComponent extends BaseMessageComponent { min_length: this.minLength, placeholder: this.placeholder, required: this.required, - style: InputTextStyles[this.style], + style: TextInputStyles[this.style], type: MessageComponentTypes[this.type], value: this.value, }; } /** - * Data that can be resolved to an InputTextStyle. This can be - * * InputTextStyle + * Data that can be resolved to an TextInputStyle. This can be + * * TextInputStyle * * number - * @typedef {number|InputTextStyle} InputTextStyleResolvable + * @typedef {number|TextInputStyle} TextInputStyleResolvable */ /** * Resolves the style of an input tetx component - * @param {InputTextStyleResolvable} style The style to resolve - * @returns {InputTextStyle} + * @param {TextInputStyleResolvable} style The style to resolve + * @returns {TextInputStyle} * @private */ static resolveStyle(style) { - return typeof style === 'string' ? style : InputTextStyles[style]; + return typeof style === 'string' ? style : TextInputStyles[style]; } } -module.exports = InputTextComponent; +module.exports = TextInputComponent; diff --git a/src/util/Constants.js b/src/util/Constants.js index 70cd3c0c3c17..ae1e4ce8002d 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1061,15 +1061,6 @@ exports.ApplicationCommandOptionTypes = createEnum([ */ exports.ApplicationCommandPermissionTypes = createEnum([null, 'ROLE', 'USER']); -/** - * The style of an input text component - * * SHORT - * * LONG - * @typedef {string} InputTextStyle - * @see {@link https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles} - */ -exports.InputTextStyles = createEnum([null, 'SHORT', 'LONG']); - /** * The type of an {@link Interaction} object: * * PING @@ -1163,6 +1154,15 @@ exports.NSFWLevels = createEnum(['DEFAULT', 'EXPLICIT', 'SAFE', 'AGE_RESTRICTED' */ exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']); +/** + * The style of an input text component + * * SHORT + * * LONG + * @typedef {string} TextInputStyle + * @see {@link https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles} + */ +exports.TextInputStyles = createEnum([null, 'SHORT', 'PARAGRAPH']); + /** * Privacy level of a {@link GuildScheduledEvent} object: * * GUILD_ONLY diff --git a/typings/enums.d.ts b/typings/enums.d.ts index 34d85cb2a579..fd3c4734f095 100644 --- a/typings/enums.d.ts +++ b/typings/enums.d.ts @@ -104,11 +104,6 @@ export const enum GuildScheduledEventStatuses { CANCELED = 4, } -export const enum InputTextStyles { - SHORT = 1, - LONG = 2, -} - export const enum InteractionResponseTypes { PONG = 1, CHANNEL_MESSAGE_WITH_SOURCE = 4, @@ -197,6 +192,11 @@ export const enum StickerTypes { GUILD = 2, } +export const enum TextInputStyles { + SHORT = 1, + PARAGRAPH = 2, +} + export const enum VerificationLevels { NONE = 0, LOW = 1, diff --git a/typings/index.d.ts b/typings/index.d.ts index a1753314020f..0677c4eb154b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -64,7 +64,6 @@ import { ChannelTypes, DefaultMessageNotificationLevels, ExplicitContentFilterLevels, - InputTextStyles, InteractionResponseTypes, InteractionTypes, InviteTargetType, @@ -80,6 +79,7 @@ import { PrivacyLevels, StickerFormatTypes, StickerTypes, + TextInputStyles, VerificationLevels, WebhookTypes, GuildScheduledEventEntityTypes, @@ -1264,28 +1264,6 @@ export class RateLimitError extends Error { public name: 'RateLimitError'; } -export class InputTextComponent extends BaseMessageComponent { - public constructor(data?: InputTextComponent | InputTextComponentOptions); - public customId: string | null; - public label: string | null; - public required: boolean; - public maxLength: number | null; - public minLength: number | null; - public placeholder: string | null; - public style: InputTextStyle; - public value: string | null; - public setCustomId(customId: string): this; - public setLabel(label: string): this; - public setRequired(required: boolean): this; - public setMaxLength(maxLength: number): this; - public setMinLength(minLength: number): this; - public setPlaceholder(placeholder: string): this; - public setStyle(style: InputTextStyleResolvable): this; - public setValue(value: string): this; - public toJSON(): unknown; - public static resolveStyle(style: InputTextStyleResolvable): InputTextStyle; -} - export class Integration extends Base { private constructor(client: Client, data: RawIntegrationData, guild: Guild); public account: IntegrationAccount; @@ -2375,6 +2353,28 @@ export class TextChannel extends BaseGuildTextChannel { public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; } +export class TextInputComponent extends BaseMessageComponent { + public constructor(data?: TextInputComponent | TextInputComponentOptions); + public customId: string | null; + public label: string | null; + public required: boolean; + public maxLength: number | null; + public minLength: number | null; + public placeholder: string | null; + public style: TextInputStyle; + public value: string | null; + public setCustomId(customId: string): this; + public setLabel(label: string): this; + public setRequired(required: boolean): this; + public setMaxLength(maxLength: number): this; + public setMinLength(minLength: number): this; + public setPlaceholder(placeholder: string): this; + public setStyle(style: TextInputStyleResolvable): this; + public setValue(value: string): this; + public toJSON(): unknown; + public static resolveStyle(style: TextInputStyleResolvable): TextInputStyle; +} + export class ThreadChannel extends TextBasedChannelMixin(Channel) { private constructor(guild: Guild, data?: RawThreadChannelData, client?: Client, fromInteraction?: boolean); public archived: boolean | null; @@ -2917,12 +2917,12 @@ export const Constants: { MembershipStates: EnumHolder; ApplicationCommandOptionTypes: EnumHolder; ApplicationCommandPermissionTypes: EnumHolder; - InputTextStyles: EnumHolder; InteractionTypes: EnumHolder; InteractionResponseTypes: EnumHolder; MessageComponentTypes: EnumHolder; MessageButtonStyles: EnumHolder; ModalComponentTypes: EnumHolder; + TextInputStyles: EnumHolder; MFALevels: EnumHolder; NSFWLevels: EnumHolder; PrivacyLevels: EnumHolder; @@ -4961,25 +4961,6 @@ export interface ImageURLOptions extends Omit { format?: DynamicImageFormat; } -export interface InputTextComponentOptions extends BaseMessageComponentOptions { - customId?: string; - label?: string; - minLength?: number; - maxLength?: number; - placeholder?: string; - required?: boolean; - style?: InputTextStyleResolvable; - value?: string; -} - -export type InputTextStyle = keyof typeof InputTextStyles; - -export type InputTextStyleResolvable = InputTextStyle | InputTextStyles; -export interface IntegrationAccount { - id: string | Snowflake; - name: string; -} - export type IntegrationType = 'twitch' | 'youtube' | 'discord'; export interface InteractionCollectorOptions @@ -5101,9 +5082,9 @@ export type MessageActionRowComponentOptions = export type MessageActionRowComponentResolvable = MessageActionRowComponent | MessageActionRowComponentOptions; -export type ModalActionRowComponent = InputTextComponent; +export type ModalActionRowComponent = TextInputComponent; -export type ModalActionRowComponentOptions = InputTextComponentOptions; +export type ModalActionRowComponentOptions = TextInputComponentOptions; export type ModalActionRowComponentResolvable = ModalActionRowComponent | ModalActionRowComponentOptions; @@ -5716,6 +5697,25 @@ export type TextBasedChannel = Extract export type TextBasedChannelTypes = TextBasedChannel['type']; +export interface TextInputComponentOptions extends BaseMessageComponentOptions { + customId?: string; + label?: string; + minLength?: number; + maxLength?: number; + placeholder?: string; + required?: boolean; + style?: TextInputStyleResolvable; + value?: string; +} + +export type TextInputStyle = keyof typeof TextInputStyles; + +export type TextInputStyleResolvable = TextInputStyle | TextInputStyles; +export interface IntegrationAccount { + id: string | Snowflake; + name: string; +} + export type VoiceBasedChannel = Extract; export type GuildBasedChannel = Extract; diff --git a/typings/rawDataTypes.d.ts b/typings/rawDataTypes.d.ts index 2777b9dbb35d..2893638cb025 100644 --- a/typings/rawDataTypes.d.ts +++ b/typings/rawDataTypes.d.ts @@ -144,7 +144,7 @@ export type RawMessageButtonInteractionData = APIMessageButtonInteractionData; export type RawMessageSelectMenuInteractionData = APIMessageSelectMenuInteractionData; // TODO: Replace with discord-api-types definition -export type RawInputTextComponentData = { +export type RawTextInputComponentData = { type: MessageComponentTypes.INPUT_TEXT; custom_id: string; value: string; @@ -153,7 +153,7 @@ export type RawInputTextComponentData = { // TODO: Replace with discord-api-types definition export type RawModalActionRowComponentData = { custom_id: string; - components: RawInputTextComponentData; + components: RawTextInputComponentData; }; // TODO: Replace with discord-api-types definition From 6daef2af8b001ae18b681aabcf90689860179d01 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 10 Feb 2022 00:00:37 +1100 Subject: [PATCH 07/34] refactor: missed some renaming --- src/structures/BaseMessageComponent.js | 2 +- src/structures/TextInputComponent.js | 10 +++++----- src/util/Constants.js | 2 +- typings/enums.d.ts | 4 ++-- typings/rawDataTypes.d.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index da939e0c0e39..4075827343eb 100644 --- a/src/structures/BaseMessageComponent.js +++ b/src/structures/BaseMessageComponent.js @@ -81,7 +81,7 @@ class BaseMessageComponent { component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data); break; } - case MessageComponentTypes.INPUT_TEXT: { + case MessageComponentTypes.TEXT_INPUT: { const TextInputComponent = require('./TextInputComponent'); component = data instanceof TextInputComponent ? data : new TextInputComponent(data); break; diff --git a/src/structures/TextInputComponent.js b/src/structures/TextInputComponent.js index f8c8322fe49f..16c6c2fad3f0 100644 --- a/src/structures/TextInputComponent.js +++ b/src/structures/TextInputComponent.js @@ -27,7 +27,7 @@ class TextInputComponent extends BaseMessageComponent { * @param {TextInputComponent|TextInputComponentOptions} [data={}] TextInputComponent to clone or raw data */ constructor(data = {}) { - super({ type: 'INPUT_TEXT' }); + super({ type: 'TEXT_INPUT' }); this.setup(data); } @@ -88,7 +88,7 @@ class TextInputComponent extends BaseMessageComponent { * @returns {TextInputComponent} */ setCustomId(customId) { - this.customId = Util.verifyString(customId, RangeError, 'INPUT_TEXT_CUSTOM_ID'); + this.customId = Util.verifyString(customId, RangeError, 'TEXT_INPUT_CUSTOM_ID'); return this; } @@ -98,7 +98,7 @@ class TextInputComponent extends BaseMessageComponent { * @returns {TextInputComponent} */ setLabel(label) { - this.label = Util.verifyString(label, RangeError, 'INPUT_TEXT_LABEL'); + this.label = Util.verifyString(label, RangeError, 'TEXT_INPUT_LABEL'); return this; } @@ -138,7 +138,7 @@ class TextInputComponent extends BaseMessageComponent { * @returns {TextInputComponent} */ setPlaceholder(placeholder) { - this.placeholder = Util.verifyString(placeholder, RangeError, 'INPUT_TEXT_PLACEHOLDER'); + this.placeholder = Util.verifyString(placeholder, RangeError, 'TEXT_INPUT_PLACEHOLDER'); return this; } @@ -158,7 +158,7 @@ class TextInputComponent extends BaseMessageComponent { * @returns {TextInputComponent} */ setValue(value) { - this.value = Util.verifyString(value, RangeError, 'INPUT_TEXT_VALUE'); + this.value = Util.verifyString(value, RangeError, 'TEXT_INPUT_VALUE'); return this; } diff --git a/src/util/Constants.js b/src/util/Constants.js index ae1e4ce8002d..1ba32d679e8f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1111,7 +1111,7 @@ exports.InteractionResponseTypes = createEnum([ * @typedef {string} MessageComponentType * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types} */ -exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU', 'INPUT_TEXT']); +exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU', 'TEXT_INPUT']); /** * The style of a message button diff --git a/typings/enums.d.ts b/typings/enums.d.ts index fd3c4734f095..f4067d90b694 100644 --- a/typings/enums.d.ts +++ b/typings/enums.d.ts @@ -144,12 +144,12 @@ export const enum MessageComponentTypes { ACTION_ROW = 1, BUTTON = 2, SELECT_MENU = 3, - INPUT_TEXT = 4, + TEXT_INPUT = 4, } export const enum ModalComponentTypes { ACTION_ROW = 1, - INPUT_TEXT = 4, + TEXT_INPUT = 4, } export const enum MFALevels { diff --git a/typings/rawDataTypes.d.ts b/typings/rawDataTypes.d.ts index 2893638cb025..88f8398ebbe8 100644 --- a/typings/rawDataTypes.d.ts +++ b/typings/rawDataTypes.d.ts @@ -145,7 +145,7 @@ export type RawMessageSelectMenuInteractionData = APIMessageSelectMenuInteractio // TODO: Replace with discord-api-types definition export type RawTextInputComponentData = { - type: MessageComponentTypes.INPUT_TEXT; + type: MessageComponentTypes.TEXT_INPUT; custom_id: string; value: string; }; From 4da9c39769e68eeaa3bbd8e99c5f3c2be1430616 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 10 Feb 2022 00:01:00 +1100 Subject: [PATCH 08/34] feat(interactions): provide value getter for modals --- src/structures/ModalSubmitInteraction.js | 19 +++++++++++++++++++ typings/index.d.ts | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 0a2d42c66530..2cf7a89d883e 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -3,6 +3,7 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const Interaction = require('./Interaction'); const InteractionResponses = require('./interfaces/InteractionResponses'); +const { MessageComponentTypes } = require('../util/Constants'); class ModalSubmitInteraction extends Interaction { constructor(client, data) { @@ -21,6 +22,24 @@ class ModalSubmitInteraction extends Interaction { this.components = data.data.components?.map(c => BaseMessageComponent.create(c, this.client)) ?? []; } + /** + * Get the value submitted in a text input component + * @param {string} customId Custom id of the text input component + * @returns {string} + */ + getTextInputValue(customId) { + for (const row of this.components) { + const field = row.components.find( + c => c.customId === customId && c.type === MessageComponentTypes[MessageComponentTypes.TEXT_INPUT], + ); + + if (field) { + return field.value; + } + } + return null; + } + // These are here only for documentation purposes - they are implemented by InteractionResponses /* eslint-disable no-empty-function */ deferReply() {} diff --git a/typings/index.d.ts b/typings/index.d.ts index 0677c4eb154b..a79fac59661d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1491,7 +1491,7 @@ export type MappedInteractionTypes = EnumValue BUTTON: ButtonInteraction>; SELECT_MENU: SelectMenuInteraction>; ACTION_ROW: MessageComponentInteraction>; - INPUT_TEXT: ModalSubmitInteraction>; + TEXT_INPUT: ModalSubmitInteraction>; } >; @@ -1863,6 +1863,7 @@ export class ModalSubmitInteraction extend protected constructor(client: Client, data: RawModalSubmitInteractionData); public customId: string; public components: MessageActionRow[]; + public getTextInputValue(customId: string): string; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; public deleteReply(): Promise; From 451aba6022e642ba637d5f797e602d2aff71949d Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 10 Feb 2022 09:02:03 +1100 Subject: [PATCH 09/34] fix(types): setRequired param is optional --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index a79fac59661d..571af0ee7b8d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2366,7 +2366,7 @@ export class TextInputComponent extends BaseMessageComponent { public value: string | null; public setCustomId(customId: string): this; public setLabel(label: string): this; - public setRequired(required: boolean): this; + public setRequired(required?: boolean): this; public setMaxLength(maxLength: number): this; public setMinLength(minLength: number): this; public setPlaceholder(placeholder: string): this; From 073415dd42935f208f975d881f18d5674020aadd Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 10 Feb 2022 07:57:43 +1100 Subject: [PATCH 10/34] fix: apply suggestions from code review Co-authored-by: Ben <88249114+BenjammingKirby@users.noreply.github.com> --- src/structures/Modal.js | 2 +- src/structures/TextInputComponent.js | 50 ++++++++++++++-------------- src/util/Constants.js | 2 +- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/structures/Modal.js b/src/structures/Modal.js index bbee17be462f..d65792849d36 100644 --- a/src/structures/Modal.js +++ b/src/structures/Modal.js @@ -9,7 +9,7 @@ class Modal { * @property {string} [customId] A unique string to be sent in the interaction when clicked * @property {string} [title] The title to be displayed on this modal * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] - * Action rows containing interactive components for the modal (input text components) + * Action rows containing interactive components for the modal (text input components) */ /** diff --git a/src/structures/TextInputComponent.js b/src/structures/TextInputComponent.js index 16c6c2fad3f0..721804800ddf 100644 --- a/src/structures/TextInputComponent.js +++ b/src/structures/TextInputComponent.js @@ -6,7 +6,7 @@ const { TextInputStyles, MessageComponentTypes } = require('../util/Constants'); const Util = require('../util/Util'); /** - * Represents an input text component in a modal + * Represents a text input component in a modal * @extends {BaseMessageComponent} */ @@ -14,13 +14,13 @@ class TextInputComponent extends BaseMessageComponent { /** * @typedef {BaseMessageComponentOptions} TextInputComponentOptions * @property {string} [customId] A unique string to be sent in the interaction when submitted - * @property {string} [label] The text to be displayed above this input text component + * @property {string} [label] The text to be displayed above this text input component * @property {number} [maxLength] Maximum length of text that can be entered * @property {number} [minLength] Minimum length of text required to be entered * @property {string} [placeholder] Custom placeholder text to display when no text is entered - * @property {boolean} [required] Whether or not this input text component is required - * @property {TextInputStyleResolvable} [style] The style of this input text component - * @property {string} [value] Value of this input text component + * @property {boolean} [required] Whether or not this text input component is required + * @property {TextInputStyleResolvable} [style] The style of this text input component + * @property {string} [value] Value of this text input component */ /** @@ -40,7 +40,7 @@ class TextInputComponent extends BaseMessageComponent { this.customId = data.custom_id ?? data.customId ?? null; /** - * The text to be displayed above this input text component + * The text to be displayed above this text input component * @type {?string} */ this.label = data.label ?? null; @@ -64,26 +64,26 @@ class TextInputComponent extends BaseMessageComponent { this.placeholder = data.placeholder ?? null; /** - * Whether or not this input text component is required + * Whether or not this text input component is required * @type {?boolean} */ this.required = data.required ?? false; /** - * The style of this input text component + * The style of this text input component * @type {?TextInputStyle} */ this.style = data.style ? TextInputComponent.resolveStyle(data.style) : null; /** - * Value of this input text component + * Value of this text input component * @type {?string} */ this.value = data.value ?? null; } /** - * Sets the custom id of this input text component + * Sets the custom id of this text input component * @param {string} customId A unique string to be sent in the interaction when submitted * @returns {TextInputComponent} */ @@ -93,8 +93,8 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Sets the label of this input text component - * @param {string} label The text to be displayed above this input text component + * Sets the label of this text input component + * @param {string} label The text to be displayed above this text input component * @returns {TextInputComponent} */ setLabel(label) { @@ -103,8 +103,8 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Sets the input text component to be required for modal submission - * @param {boolean} [required=true] Whether this input text component is required + * Sets the text input component to be required for modal submission + * @param {boolean} [required=true] Whether this text input component is required * @returns {TextInputComponent} */ setRequired(required = true) { @@ -113,7 +113,7 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Sets the maximum length of text input required in this input text component + * Sets the maximum length of text input required in this text input component * @param {number} maxLength Maximum length of text to be required * @returns {TextInputComponent} */ @@ -123,7 +123,7 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Sets the minimum length of text input required in this input text component + * Sets the minimum length of text input required in this text input component * @param {number} minLength Minimum length of text to be required * @returns {TextInputComponent} */ @@ -133,7 +133,7 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Sets the placeholder of this input text component + * Sets the placeholder of this text input component * @param {string} placeholder Custom placeholder text to display when no text is entered * @returns {TextInputComponent} */ @@ -143,8 +143,8 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Sets the style of this input text component - * @param {TextInputStyleResolvable} style The style of this input text component + * Sets the style of this text input component + * @param {TextInputStyleResolvable} style The style of this text input component * @returns {TextInputComponent} */ setStyle(style) { @@ -153,8 +153,8 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Sets the value of this input text component - * @param {string} value Value of this input text component + * Sets the value of this text input component + * @param {string} value Value of this text input component * @returns {TextInputComponent} */ setValue(value) { @@ -163,8 +163,8 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Transforms the input text component into a plain object - * @returns {APITextInput} The raw data of this input text component + * Transforms the text input component into a plain object + * @returns {APITextInput} The raw data of this text input component */ toJSON() { return { @@ -181,14 +181,14 @@ class TextInputComponent extends BaseMessageComponent { } /** - * Data that can be resolved to an TextInputStyle. This can be + * Data that can be resolved to a TextInputStyle. This can be * * TextInputStyle * * number * @typedef {number|TextInputStyle} TextInputStyleResolvable */ /** - * Resolves the style of an input tetx component + * Resolves the style of a text input component * @param {TextInputStyleResolvable} style The style to resolve * @returns {TextInputStyle} * @private diff --git a/src/util/Constants.js b/src/util/Constants.js index 1ba32d679e8f..41fdfe5f613e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1155,7 +1155,7 @@ exports.NSFWLevels = createEnum(['DEFAULT', 'EXPLICIT', 'SAFE', 'AGE_RESTRICTED' exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']); /** - * The style of an input text component + * The style of a text input component * * SHORT * * LONG * @typedef {string} TextInputStyle From fb242f677744495140be2a096a203330fcbe4dd4 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 10 Feb 2022 10:27:32 +1100 Subject: [PATCH 11/34] feat(resolver): fields resolver, missing errors --- src/errors/Messages.js | 12 +++++ src/structures/ModalSubmitFieldsResolver.js | 50 +++++++++++++++++++++ src/structures/ModalSubmitInteraction.js | 42 +++++++++++++++-- typings/index.d.ts | 20 +++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/structures/ModalSubmitFieldsResolver.js diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 97618abdee3e..3f70d922e8a1 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -58,6 +58,14 @@ const Messages = { SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string', SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string', + TEXT_INPUT_CUSTOM_ID: 'TextInputComponent customId must be a string', + TEXT_INPUT_LABEL: 'TextInputComponent label must be a string', + TEXT_INPUT_PLACEHOLDER: 'TextInputComponent placeholder must be a string', + TEXT_INPUT_VALUE: 'TextInputComponent value must be a string', + + MODAL_CUSTOM_ID: 'Modal customId must be a string', + MODAL_TITLE: 'Modal title 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}`, @@ -148,6 +156,10 @@ const Messages = { COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.', AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.', + MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`, + MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) => + `Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`, + INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite', NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`, diff --git a/src/structures/ModalSubmitFieldsResolver.js b/src/structures/ModalSubmitFieldsResolver.js new file mode 100644 index 000000000000..f368b98126e3 --- /dev/null +++ b/src/structures/ModalSubmitFieldsResolver.js @@ -0,0 +1,50 @@ +'use strict'; + +const { TypeError } = require('../errors'); +const { MessageComponentTypes } = require('../util/Constants'); + +class ModalSubmitFieldsResolver { + constructor(components) { + /** + * The components within the modal + * @type {PartialModalActionRow[]} The components in the modal + */ + this.components = components; + } + + /** + * The extracted fields from the modal + * @type {PartialInputTextData[]} The fields in the modal + * @private + */ + get _fields() { + return this.components.reduce((previous, next) => previous.concat(next), []); + } + + /** + * Gets a field given a custom id from a component + * @param {string} customId The custom id of the component + * @returns {?PartialInputTextData} + */ + getField(customId) { + const field = this._fields.find(f => f.customId === customId); + if (!field) throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND', customId); + return field; + } + + /** + * Gets the value of a text input component given a custom id + * @param {string} customId The custom id of the text input component + * @returns {?string} + */ + getTextInputValue(customId) { + const field = this.getField(customId); + const expectedType = MessageComponentTypes.TEXT_INPUT; + if (field.type !== expectedType) { + throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_TYPE', customId, field.type, expectedType); + } + return field.value; + } +} + +module.exports = ModalSubmitFieldsResolver; diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 2cf7a89d883e..a4b56efcb24c 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -1,7 +1,7 @@ 'use strict'; -const BaseMessageComponent = require('./BaseMessageComponent'); const Interaction = require('./Interaction'); +const ModalSubmitFieldsResolver = require('./ModalSubmitFieldsResolver'); const InteractionResponses = require('./interfaces/InteractionResponses'); const { MessageComponentTypes } = require('../util/Constants'); @@ -15,11 +15,34 @@ class ModalSubmitInteraction extends Interaction { */ this.customId = data.data.custom_id; + /** + * @typedef {object} PartialTextInputData + * @property {string} [customId] A unique string to be sent in the interaction when submitted + * @property {MessageComponentType} [type] The type of this component + * @property {string} [value] Value of this text input component + */ + + /** + * @typedef {object} PartialModalActionRow + * @property {MessageComponentType} [type] The type of this component + * @property {PartialTextInputData[]} [components] Partial text input components + */ + /** * The inputs within the modal - * @type {Array>} + * @type {PartialModalActionRow[]} */ - this.components = data.data.components?.map(c => BaseMessageComponent.create(c, this.client)) ?? []; + this.components = + data.data.components?.map(c => ({ + type: MessageComponentTypes[c.type], + components: ModalSubmitInteraction.transformComponent(c), + })) ?? []; + + /** + * The fields within the modal + * @type {ModalSubmitFieldsResolver} + */ + this.fields = new ModalSubmitFieldsResolver(this.components); } /** @@ -40,6 +63,19 @@ class ModalSubmitInteraction extends Interaction { return null; } + /** + * Transforms component data to discord.js-compatible data + * @param {*} rawComponent The data to transform + * @returns {PartialTextInputData[]} + */ + static transformComponent(rawComponent) { + return rawComponent.components.map(c => ({ + value: c.value, + type: MessageComponentTypes[c.type], + customId: c.custom_id, + })); + } + // These are here only for documentation purposes - they are implemented by InteractionResponses /* eslint-disable no-empty-function */ deferReply() {} diff --git a/typings/index.d.ts b/typings/index.d.ts index 571af0ee7b8d..3ae1111de96f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1859,10 +1859,18 @@ export class Modal { public toJSON(): RawModalSubmitInteractionData; } +export class ModalSubmitFieldsResolver { + constructor(components: PartialModalActionRow[]); + private readonly _fields: PartialTextInputData[]; + public getField(customId: string): PartialTextInputData; + public getTextInputValue(customId: string): string; +} + export class ModalSubmitInteraction extends Interaction { protected constructor(client: Client, data: RawModalSubmitInteractionData); public customId: string; public components: MessageActionRow[]; + public fields: ModalSubmitFieldsResolver; public getTextInputValue(customId: string): string; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; @@ -5352,6 +5360,18 @@ export type OverwriteResolvable = PermissionOverwrites | OverwriteData; export type OverwriteType = 'member' | 'role'; +export interface PartialModalActionRow { + type: MessageComponentType; + components: PartialTextInputData[]; +} + +export interface PartialTextInputData { + value: string; + // TODO: use dapi types + type: MessageComponentType; + customId: string; +} + export type PermissionFlags = Record; export type PermissionOverwriteOptions = Partial>; From 01cb2d2f33e716a8ed94076d6298b73096b4d659 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 10 Feb 2022 10:31:30 +1100 Subject: [PATCH 12/34] fix(resolver): some v13ish things --- src/structures/ModalSubmitFieldsResolver.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/ModalSubmitFieldsResolver.js b/src/structures/ModalSubmitFieldsResolver.js index f368b98126e3..c1c4861950d2 100644 --- a/src/structures/ModalSubmitFieldsResolver.js +++ b/src/structures/ModalSubmitFieldsResolver.js @@ -18,7 +18,7 @@ class ModalSubmitFieldsResolver { * @private */ get _fields() { - return this.components.reduce((previous, next) => previous.concat(next), []); + return this.components.reduce((previous, next) => previous.concat(next.components), []); } /** @@ -39,7 +39,7 @@ class ModalSubmitFieldsResolver { */ getTextInputValue(customId) { const field = this.getField(customId); - const expectedType = MessageComponentTypes.TEXT_INPUT; + const expectedType = MessageComponentTypes[MessageComponentTypes.TEXT_INPUT]; if (field.type !== expectedType) { throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_TYPE', customId, field.type, expectedType); } From 8f5fa3f9e775f68d55ec3c7d4dc1aaa1a0e6d9ad Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 10 Feb 2022 10:50:36 +1100 Subject: [PATCH 13/34] fix: apply suggestions from code review Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com> --- src/structures/Modal.js | 2 +- src/structures/ModalSubmitInteraction.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/Modal.js b/src/structures/Modal.js index d65792849d36..0227dc296715 100644 --- a/src/structures/Modal.js +++ b/src/structures/Modal.js @@ -5,7 +5,7 @@ const Util = require('../util/Util'); class Modal { /** - * @typedef {object} ModalOptions + * @typedef {Object} ModalOptions * @property {string} [customId] A unique string to be sent in the interaction when clicked * @property {string} [title] The title to be displayed on this modal * @property {MessageActionRow[]|MessageActionRowOptions[]} [components] diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index a4b56efcb24c..0e54b167f154 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -16,14 +16,14 @@ class ModalSubmitInteraction extends Interaction { this.customId = data.data.custom_id; /** - * @typedef {object} PartialTextInputData + * @typedef {Object} PartialTextInputData * @property {string} [customId] A unique string to be sent in the interaction when submitted * @property {MessageComponentType} [type] The type of this component * @property {string} [value] Value of this text input component */ /** - * @typedef {object} PartialModalActionRow + * @typedef {Object} PartialModalActionRow * @property {MessageComponentType} [type] The type of this component * @property {PartialTextInputData[]} [components] Partial text input components */ From 1b351084cb6fd240c9b8ac79988c0f9b1ed1531f Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 10 Feb 2022 15:51:41 +1100 Subject: [PATCH 14/34] fix(types): action rows in modals --- typings/index.d.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 3ae1111de96f..fec34b683e55 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1831,7 +1831,7 @@ export class MessageSelectMenu extends BaseMessageComponent { export class Modal { public constructor(data?: Modal | ModalOptions); - public components: MessageActionRow[]; + public components: MessageActionRow[]; public customId: string; public title: string; public addComponents( @@ -1869,7 +1869,7 @@ export class ModalSubmitFieldsResolver { export class ModalSubmitInteraction extends Interaction { protected constructor(client: Client, data: RawModalSubmitInteractionData); public customId: string; - public components: MessageActionRow[]; + public components: PartialModalActionRow[]; public fields: ModalSubmitFieldsResolver; public getTextInputValue(customId: string): string; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; @@ -5330,7 +5330,9 @@ export type MessageType = keyof typeof MessageTypes; export type MFALevel = keyof typeof MFALevels; export interface ModalOptions { - components: MessageActionRow[] | MessageActionRowOptions[]; + components: + | MessageActionRow[] + | MessageActionRowOptions[]; customId: string; title: string; } From ee1529494e6f36ae326df5a9e22abe25e1b808fa Mon Sep 17 00:00:00 2001 From: monbrey Date: Sun, 13 Feb 2022 07:35:05 +1100 Subject: [PATCH 15/34] fix(modal submit): missing props --- src/structures/ModalSubmitInteraction.js | 25 ++++++++++++++++++++++++ typings/index.d.ts | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 0e54b167f154..8b122f58b508 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -1,6 +1,7 @@ 'use strict'; const Interaction = require('./Interaction'); +const InteractionWebhook = require('./InteractionWebhook'); const ModalSubmitFieldsResolver = require('./ModalSubmitFieldsResolver'); const InteractionResponses = require('./interfaces/InteractionResponses'); const { MessageComponentTypes } = require('../util/Constants'); @@ -43,6 +44,30 @@ class ModalSubmitInteraction extends Interaction { * @type {ModalSubmitFieldsResolver} */ this.fields = new ModalSubmitFieldsResolver(this.components); + + /** + * Whether the reply to this interaction has been deferred + * @type {boolean} + */ + this.deferred = false; + + /** + * Whether the reply to this interaction is ephemeral + * @type {?boolean} + */ + this.ephemeral = null; + + /** + * Whether this interaction has already been replied to + * @type {boolean} + */ + this.replied = false; + + /** + * An associated interaction webhook, can be used to further interact with this interaction + * @type {InteractionWebhook} + */ + this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index fec34b683e55..6c72c273a4b4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1870,7 +1870,11 @@ export class ModalSubmitInteraction extend protected constructor(client: Client, data: RawModalSubmitInteractionData); public customId: string; public components: PartialModalActionRow[]; + public deferred: boolean; + public ephemeral: boolean | null; public fields: ModalSubmitFieldsResolver; + public replied: false; + public webhook: InteractionWebhook; public getTextInputValue(customId: string): string; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; From 1614ee23c193f154a000073ca9b9134649c630b5 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Sun, 13 Feb 2022 13:34:42 +1100 Subject: [PATCH 16/34] Fox: update src/util/Constants.js Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --- 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 41fdfe5f613e..ad8bd920f0aa 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1157,7 +1157,7 @@ exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']); /** * The style of a text input component * * SHORT - * * LONG + * * PARAGRAPH * @typedef {string} TextInputStyle * @see {@link https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles} */ From 5b72ff13b05231d139f006ddf3d477870c260386 Mon Sep 17 00:00:00 2001 From: monbrey Date: Mon, 14 Feb 2022 07:56:15 +1100 Subject: [PATCH 17/34] refactor: rename presentModal to showModal --- src/structures/MessageComponentInteraction.js | 2 +- src/structures/ModalSubmitInteraction.js | 2 +- src/structures/interfaces/InteractionResponses.js | 4 ++-- typings/index.d.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index 673bb282aa46..a729b6d28d6a 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -101,7 +101,7 @@ class MessageComponentInteraction extends Interaction { followUp() {} deferUpdate() {} update() {} - presentModal() {} + showModal() {} } InteractionResponses.applyToClass(MessageComponentInteraction); diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 8b122f58b508..3f1a7e16944c 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -111,6 +111,6 @@ class ModalSubmitInteraction extends Interaction { followUp() {} } -InteractionResponses.applyToClass(ModalSubmitInteraction, ['deferUpdate', 'update', 'presentModal']); +InteractionResponses.applyToClass(ModalSubmitInteraction, ['deferUpdate', 'update', 'showModal']); module.exports = ModalSubmitInteraction; diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index f7c44df9ebff..6eef0cece5a5 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -232,7 +232,7 @@ class InteractionResponses { * @param {Modal|ModalOptions} modal The modal to present * @returns {Promise} */ - async presentModal(modal) { + async showModal(modal) { const _modal = modal instanceof Modal ? modal : new Modal(modal); await this.client.api.interactions(this.id, this.token).callback.post({ data: { @@ -252,7 +252,7 @@ class InteractionResponses { 'followUp', 'deferUpdate', 'update', - 'presentModal', + 'showModal', ]; for (const prop of props) { diff --git a/typings/index.d.ts b/typings/index.d.ts index 6c72c273a4b4..60d6d69e1602 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -357,9 +357,9 @@ export abstract class BaseCommandInteraction>; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; - public presentModal(modal: Modal | ModalOptions): Promise; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; + public showModal(modal: Modal | ModalOptions): Promise; private transformOption( option: APIApplicationCommandOption, resolved: APIApplicationCommandInteractionData['resolved'], @@ -1665,9 +1665,9 @@ export class MessageComponentInteraction e public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise>; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; - public presentModal(modal: Modal | ModalOptions): Promise; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; + public showModal(modal: Modal | ModalOptions): Promise; public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise>; public update(options: string | MessagePayload | InteractionUpdateOptions): Promise; From b2cb2194a2cf1bd00576aa9846f461270aa1f387 Mon Sep 17 00:00:00 2001 From: monbrey Date: Mon, 14 Feb 2022 08:46:18 +1100 Subject: [PATCH 18/34] docs: some were missing --- src/structures/Modal.js | 3 +++ src/structures/ModalSubmitFieldsResolver.js | 3 +++ src/structures/ModalSubmitInteraction.js | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/src/structures/Modal.js b/src/structures/Modal.js index 0227dc296715..dd56ece65fa3 100644 --- a/src/structures/Modal.js +++ b/src/structures/Modal.js @@ -3,6 +3,9 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const Util = require('../util/Util'); +/** + * Represents an modal to be shown + */ class Modal { /** * @typedef {Object} ModalOptions diff --git a/src/structures/ModalSubmitFieldsResolver.js b/src/structures/ModalSubmitFieldsResolver.js index c1c4861950d2..c60d1183f3c1 100644 --- a/src/structures/ModalSubmitFieldsResolver.js +++ b/src/structures/ModalSubmitFieldsResolver.js @@ -3,6 +3,9 @@ const { TypeError } = require('../errors'); const { MessageComponentTypes } = require('../util/Constants'); +/** + * A resolver for modal submit interaction text inputs. + */ class ModalSubmitFieldsResolver { constructor(components) { /** diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 3f1a7e16944c..11ecb292bbeb 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -6,6 +6,11 @@ const ModalSubmitFieldsResolver = require('./ModalSubmitFieldsResolver'); const InteractionResponses = require('./interfaces/InteractionResponses'); const { MessageComponentTypes } = require('../util/Constants'); +/** + * Represents a modal submit interaction. + * @extends {Interaction} + * @implements {InteractionResponses} + */ class ModalSubmitInteraction extends Interaction { constructor(client, data) { super(client, data); From 2c18bf047e55c7fb731deb7ed8c350098f46ec74 Mon Sep 17 00:00:00 2001 From: monbrey Date: Mon, 14 Feb 2022 08:52:18 +1100 Subject: [PATCH 19/34] types(modal): nullable props --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 60d6d69e1602..60dfc92e8222 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1832,8 +1832,8 @@ export class MessageSelectMenu extends BaseMessageComponent { export class Modal { public constructor(data?: Modal | ModalOptions); public components: MessageActionRow[]; - public customId: string; - public title: string; + public customId: string | null; + public title: string | null; public addComponents( ...components: ( | MessageActionRow From c9d7db3d2dc55cf830b706228152fd8dae1ac8dd Mon Sep 17 00:00:00 2001 From: monbrey Date: Mon, 14 Feb 2022 09:10:17 +1100 Subject: [PATCH 20/34] docs: improve wording --- src/structures/Modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Modal.js b/src/structures/Modal.js index dd56ece65fa3..91ec04cd0307 100644 --- a/src/structures/Modal.js +++ b/src/structures/Modal.js @@ -4,7 +4,7 @@ const BaseMessageComponent = require('./BaseMessageComponent'); const Util = require('../util/Util'); /** - * Represents an modal to be shown + * Represents a modal (form) to be shown in response to an interaction */ class Modal { /** From f7bde7d3f4a9e0ba2d24841bb2d7bb808734b37b Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Mon, 14 Feb 2022 12:06:17 +1100 Subject: [PATCH 21/34] docs; update wording Co-authored-by: Parbez --- src/structures/interfaces/InteractionResponses.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index 6eef0cece5a5..f5b01aa0ba0b 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -228,8 +228,8 @@ class InteractionResponses { } /** - * Presents a modal component - * @param {Modal|ModalOptions} modal The modal to present + * Shows a modal component + * @param {Modal|ModalOptions} modal The modal to show * @returns {Promise} */ async showModal(modal) { From b88e699664346a0be1866ac138e3d19de4379ec2 Mon Sep 17 00:00:00 2001 From: monbrey Date: Tue, 15 Feb 2022 11:45:38 +1100 Subject: [PATCH 22/34] feat(modals): awaitModalSubmit --- src/structures/BaseCommandInteraction.js | 2 ++ src/structures/MessageComponentInteraction.js | 1 + src/structures/ModalSubmitInteraction.js | 2 +- .../interfaces/InteractionResponses.js | 36 ++++++++++++++++++- typings/index.d.ts | 18 ++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/structures/BaseCommandInteraction.js b/src/structures/BaseCommandInteraction.js index a24a5f0c893b..38cc73b97fe4 100644 --- a/src/structures/BaseCommandInteraction.js +++ b/src/structures/BaseCommandInteraction.js @@ -196,6 +196,8 @@ class BaseCommandInteraction extends Interaction { editReply() {} deleteReply() {} followUp() {} + showModal() {} + awaitModalSubmit() {} } InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']); diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index a729b6d28d6a..d97d170bc278 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -102,6 +102,7 @@ class MessageComponentInteraction extends Interaction { deferUpdate() {} update() {} showModal() {} + awaitModalSubmit() {} } InteractionResponses.applyToClass(MessageComponentInteraction); diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 11ecb292bbeb..7d6f6f8f145c 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -116,6 +116,6 @@ class ModalSubmitInteraction extends Interaction { followUp() {} } -InteractionResponses.applyToClass(ModalSubmitInteraction, ['deferUpdate', 'update', 'showModal']); +InteractionResponses.applyToClass(ModalSubmitInteraction, ['deferUpdate', 'update', 'showModal', 'awaitModalSubmit']); module.exports = ModalSubmitInteraction; diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index f5b01aa0ba0b..a149d7715cc0 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -1,8 +1,9 @@ 'use strict'; const { Error } = require('../../errors'); -const { InteractionResponseTypes } = require('../../util/Constants'); +const { InteractionResponseTypes, InteractionTypes } = require('../../util/Constants'); const MessageFlags = require('../../util/MessageFlags'); +const InteractionCollector = require('../InteractionCollector'); const MessagePayload = require('../MessagePayload'); const Modal = require('../Modal'); @@ -242,6 +243,38 @@ class InteractionResponses { }); } + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {Object} AwaitModalSubmitOptions + * @property {CollectorFilter} [filter] The filter applied to this collector + * @property {number} time Time to wait for an interaction before rejecting + */ + + /** + * Collects a single modal submit interaction that passes the filter. + * The Promise will reject if the time expires. + * @param {AwaitModalSubmitOptions} options Options to pass to the internal collector + * @returns {Promise} + * @example + * // Collect a modal submit interaction + * const filter = (interaction) => interaction.customId === 'modal'; + * interaction.awaitModalSubmit({ filter, time: 15_000 }) + * .then(interaction => console.log(`${interaction.customId} was submitted!`)) + * .catch(console.error); + */ + awaitModalSubmit(options) { + if (typeof options.time !== 'number') throw new Error('INVALID_TYPE', 'time', 'number'); + const _options = { ...options, max: 1, interactionType: InteractionTypes.MODAL_SUBMIT }; + return new Promise((resolve, reject) => { + const collector = new InteractionCollector(this.client, _options); + collector.once('end', (interactions, reason) => { + const interaction = interactions.first(); + if (interaction) resolve(interaction); + else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason)); + }); + }); + } + static applyToClass(structure, ignore = []) { const props = [ 'deferReply', @@ -253,6 +286,7 @@ class InteractionResponses { 'deferUpdate', 'update', 'showModal', + 'awaitModalSubmit', ]; for (const prop of props) { diff --git a/typings/index.d.ts b/typings/index.d.ts index 60dfc92e8222..435c873246fc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -348,6 +348,9 @@ export abstract class BaseCommandInteraction, + ): Promise>; public inGuild(): this is BaseCommandInteraction<'raw' | 'cached'>; public inCachedGuild(): this is BaseCommandInteraction<'cached'>; public inRawGuild(): this is BaseCommandInteraction<'raw'>; @@ -1654,6 +1657,9 @@ export class MessageComponentInteraction e public message: GuildCacheMessage; public replied: boolean; public webhook: InteractionWebhook; + public awaitModalSubmit( + options: AwaitModalSubmitOptions, + ): Promise>; public inGuild(): this is MessageComponentInteraction<'raw' | 'cached'>; public inCachedGuild(): this is MessageComponentInteraction<'cached'>; public inRawGuild(): this is MessageComponentInteraction<'raw'>; @@ -3862,6 +3868,13 @@ export interface AwaitMessagesOptions extends MessageCollectorOptions { errors?: string[]; } +export type AwaitModalSubmitOptions = Omit< + ModalSubmitInteractionCollectorOptions, + 'max' | 'maxComponents' | 'maxUsers' +> & { + time: number; +}; + export interface AwaitReactionsOptions extends ReactionCollectorOptions { errors?: string[]; } @@ -5341,6 +5354,11 @@ export interface ModalOptions { title: string; } +export type ModalSubmitInteractionCollectorOptions = Omit< + InteractionCollectorOptions, + 'channel' | 'message' | 'guild' | 'interactionType' +>; + export interface MultipleShardRespawnOptions { shardDelay?: number; respawnDelay?: number; From 4ef3fe69d35be52614741a2c346a8811cb739b83 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 16 Feb 2022 07:53:17 +1100 Subject: [PATCH 23/34] refactor: remove duplicate code --- src/structures/ModalSubmitInteraction.js | 18 ------------------ typings/index.d.ts | 1 - 2 files changed, 19 deletions(-) diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 7d6f6f8f145c..4a0ab5343d31 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -75,24 +75,6 @@ class ModalSubmitInteraction extends Interaction { this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token); } - /** - * Get the value submitted in a text input component - * @param {string} customId Custom id of the text input component - * @returns {string} - */ - getTextInputValue(customId) { - for (const row of this.components) { - const field = row.components.find( - c => c.customId === customId && c.type === MessageComponentTypes[MessageComponentTypes.TEXT_INPUT], - ); - - if (field) { - return field.value; - } - } - return null; - } - /** * Transforms component data to discord.js-compatible data * @param {*} rawComponent The data to transform diff --git a/typings/index.d.ts b/typings/index.d.ts index 435c873246fc..9dbbb259ceea 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1881,7 +1881,6 @@ export class ModalSubmitInteraction extend public fields: ModalSubmitFieldsResolver; public replied: false; public webhook: InteractionWebhook; - public getTextInputValue(customId: string): string; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; public deleteReply(): Promise; From 52a1d3fcb489468bc4b3ac09f8c9b2935e1f4c4a Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 16 Feb 2022 08:11:45 +1100 Subject: [PATCH 24/34] feat: bump api-types dep and use new types --- package-lock.json | 31 +++++++++++++++++++++---------- package.json | 2 +- typings/index.d.ts | 30 +++++++++++++++++++++--------- typings/rawDataTypes.d.ts | 24 +++++------------------- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50ff64b626eb..0d851c4550d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@sapphire/async-queue": "^1.1.9", "@types/node-fetch": "^2.5.12", "@types/ws": "^8.2.2", - "discord-api-types": "^0.26.0", + "discord-api-types": "^0.27.1", "form-data": "^4.0.0", "node-fetch": "^2.6.1", "ws": "^8.4.0" @@ -1009,6 +1009,15 @@ "npm": ">=7.0.0" } }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz", + "integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==", + "deprecated": "No longer supported. Install the latest release!", + "engines": { + "node": ">=12" + } + }, "node_modules/@discordjs/builders/node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -4291,12 +4300,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.0.tgz", - "integrity": "sha512-bnUltSHpQLzTVZTMjm+iNgVhAbtm5oAKHrhtiPaZoxprbm1UtuCZCsG0yXM61NamWfeSz7xnLvgFc50YzVJ5cQ==", - "engines": { - "node": ">=12" - } + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.27.1.tgz", + "integrity": "sha512-NhOrRs3TDx/p/e7+VCzcvtVz/Wkqa/olS82HJb2aM/oI0CLcnB+lJMXWa8wjn57XviFBcMMR0poqUMXx0IqTkQ==" }, "node_modules/dmd": { "version": "4.0.6", @@ -13474,6 +13480,11 @@ "zod": "^3.11.6" }, "dependencies": { + "discord-api-types": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz", + "integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==" + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", @@ -16053,9 +16064,9 @@ } }, "discord-api-types": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.0.tgz", - "integrity": "sha512-bnUltSHpQLzTVZTMjm+iNgVhAbtm5oAKHrhtiPaZoxprbm1UtuCZCsG0yXM61NamWfeSz7xnLvgFc50YzVJ5cQ==" + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.27.1.tgz", + "integrity": "sha512-NhOrRs3TDx/p/e7+VCzcvtVz/Wkqa/olS82HJb2aM/oI0CLcnB+lJMXWa8wjn57XviFBcMMR0poqUMXx0IqTkQ==" }, "dmd": { "version": "4.0.6", diff --git a/package.json b/package.json index 1746ad38df4d..cb64a6a480ba 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@sapphire/async-queue": "^1.1.9", "@types/node-fetch": "^2.5.12", "@types/ws": "^8.2.2", - "discord-api-types": "^0.26.0", + "discord-api-types": "^0.27.1", "form-data": "^4.0.0", "node-fetch": "^2.6.1", "ws": "^8.4.0" diff --git a/typings/index.d.ts b/typings/index.d.ts index 9dbbb259ceea..558ce1b12c60 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -22,6 +22,7 @@ import { import { Collection } from '@discordjs/collection'; import { APIActionRowComponent, + APIActionRowComponentTypes, APIApplicationCommand, APIApplicationCommandInteractionData, APIApplicationCommandOption, @@ -35,7 +36,9 @@ import { APIInteractionDataResolvedGuildMember, APIInteractionGuildMember, APIMessage, + APIMessageActionRowComponent, APIMessageComponent, + APIModalActionRowComponent, APIOverwrite, APIPartialChannel, APIPartialEmoji, @@ -43,6 +46,7 @@ import { APIRole, APISelectMenuComponent, APITemplateSerializedSourceGuild, + APITextInputComponent, APIUser, GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData, @@ -119,7 +123,6 @@ import { RawMessagePayloadData, RawMessageReactionData, RawMessageSelectMenuInteractionData, - RawModalActionRowComponentData, RawModalSubmitInteractionData, RawOAuth2GuildData, RawPartialGroupDMChannelData, @@ -134,6 +137,7 @@ import { RawStickerPackData, RawTeamData, RawTeamMemberData, + RawTextInputComponentData, RawThreadChannelData, RawThreadMemberData, RawTypingData, @@ -1576,13 +1580,15 @@ export class MessageActionRow< T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent, U = T extends ModalActionRowComponent ? ModalActionRowComponentResolvable : MessageActionRowComponentResolvable, > extends BaseMessageComponent { - public constructor(data?: MessageActionRow | MessageActionRowOptions | APIActionRowComponent); + public constructor( + data?: MessageActionRow | MessageActionRowOptions | APIActionRowComponent, + ); public type: 'ACTION_ROW'; public components: T[]; public addComponents(...components: U[] | U[][]): this; public setComponents(...components: U[] | U[][]): this; public spliceComponents(index: number, deleteCount: number, ...components: U[] | U[][]): this; - public toJSON(): APIActionRowComponent; + public toJSON(): APIActionRowComponent; } export class MessageAttachment { @@ -1645,9 +1651,9 @@ export class MessageComponentInteraction e public readonly component: CacheTypeReducer< Cached, MessageActionRowComponent, - Exclude, - MessageActionRowComponent | Exclude, - MessageActionRowComponent | Exclude + Exclude>, + MessageActionRowComponent | Exclude>, + MessageActionRowComponent | Exclude> >; public componentType: Exclude; public customId: string; @@ -2389,7 +2395,7 @@ export class TextInputComponent extends BaseMessageComponent { public setPlaceholder(placeholder: string): this; public setStyle(style: TextInputStyleResolvable): this; public setValue(value: string): this; - public toJSON(): unknown; + public toJSON(): RawTextInputComponentData; public static resolveStyle(style: TextInputStyleResolvable): TextInputStyle; } @@ -5105,13 +5111,19 @@ export type MessageActionRowComponentOptions = | (Required & MessageButtonOptions) | (Required & MessageSelectMenuOptions); -export type MessageActionRowComponentResolvable = MessageActionRowComponent | MessageActionRowComponentOptions; +export type MessageActionRowComponentResolvable = + | MessageActionRowComponent + | MessageActionRowComponentOptions + | APIMessageActionRowComponent; export type ModalActionRowComponent = TextInputComponent; export type ModalActionRowComponentOptions = TextInputComponentOptions; -export type ModalActionRowComponentResolvable = ModalActionRowComponent | ModalActionRowComponentOptions; +export type ModalActionRowComponentResolvable = + | ModalActionRowComponent + | ModalActionRowComponentOptions + | APIModalActionRowComponent; export interface MessageActionRowOptions< T extends diff --git a/typings/rawDataTypes.d.ts b/typings/rawDataTypes.d.ts index 88f8398ebbe8..7c69081965e3 100644 --- a/typings/rawDataTypes.d.ts +++ b/typings/rawDataTypes.d.ts @@ -77,6 +77,9 @@ import { Snowflake, APIGuildScheduledEvent, APIActionRowComponent, + APITextInputComponent, + APIModalActionRowComponent, + APIModalSubmitInteraction, } from 'discord-api-types/v9'; import { GuildChannel, Guild, PermissionOverwrites, InteractionType } from '.'; import type { InteractionTypes, MessageComponentTypes } from './enums'; @@ -143,25 +146,8 @@ export type RawMessageComponentInteractionData = APIMessageComponentInteraction; export type RawMessageButtonInteractionData = APIMessageButtonInteractionData; export type RawMessageSelectMenuInteractionData = APIMessageSelectMenuInteractionData; -// TODO: Replace with discord-api-types definition -export type RawTextInputComponentData = { - type: MessageComponentTypes.TEXT_INPUT; - custom_id: string; - value: string; -}; - -// TODO: Replace with discord-api-types definition -export type RawModalActionRowComponentData = { - custom_id: string; - components: RawTextInputComponentData; -}; - -// TODO: Replace with discord-api-types definition -export type RawModalSubmitInteractionData = { - custom_id: string; - type: InteractionTypes.MODAL_SUBMIT; - components: RawModalActionRowComponentData[]; -}; +export type RawTextInputComponentData = APITextInputComponent; +export type RawModalSubmitInteractionData = APIModalSubmitInteraction; export type RawInviteData = | APIExtendedInvite From 065af021643bdc942901b65b17019d5f101ace5f Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 16 Feb 2022 08:16:30 +1100 Subject: [PATCH 25/34] types: a little stricter --- typings/index.d.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 558ce1b12c60..431124a41191 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1579,16 +1579,17 @@ export class Message extends Base { export class MessageActionRow< T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent, U = T extends ModalActionRowComponent ? ModalActionRowComponentResolvable : MessageActionRowComponentResolvable, + V = T extends ModalActionRowComponent + ? APIActionRowComponent + : APIActionRowComponent, > extends BaseMessageComponent { - public constructor( - data?: MessageActionRow | MessageActionRowOptions | APIActionRowComponent, - ); + public constructor(data?: MessageActionRow | MessageActionRowOptions | V); public type: 'ACTION_ROW'; public components: T[]; public addComponents(...components: U[] | U[][]): this; public setComponents(...components: U[] | U[][]): this; public spliceComponents(index: number, deleteCount: number, ...components: U[] | U[][]): this; - public toJSON(): APIActionRowComponent; + public toJSON(): V; } export class MessageAttachment { From 8ba97d9e8b512b11795577bce8b8440dff6fefd3 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 16 Feb 2022 10:37:32 +1100 Subject: [PATCH 26/34] refactor: align with v14 --- src/structures/Interaction.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index df40be28e9a9..edb89ecaf87b 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -177,7 +177,7 @@ class Interaction extends Base { * Indicates whether this interaction is a {@link ModalSubmitInteraction} * @returns {boolean} */ - isModalSubmit() { + isModalSubmission() { return InteractionTypes[this.type] === InteractionTypes.MODAL_SUBMIT; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 431124a41191..918df2d165b1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1363,7 +1363,7 @@ export class Interaction extends Base { public isUserContextMenu(): this is UserContextMenuInteraction; public isMessageContextMenu(): this is MessageContextMenuInteraction; public isMessageComponent(): this is MessageComponentInteraction; - public isModalSubmit(): this is ModalSubmitInteraction; + public isModalSubmission(): this is ModalSubmitInteraction; public isSelectMenu(): this is SelectMenuInteraction; } From 1235bf5af0d02933c5ee0f40d81264fe347b0dfa Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 17 Feb 2022 08:10:25 +1100 Subject: [PATCH 27/34] fix: remove unused directive and relocate test --- typings/index.test-d.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 9d9281bc1422..dc5403dd1099 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -678,6 +678,8 @@ client.on('interaction', async interaction => { void new MessageActionRow(); + void new MessageActionRow({}); + const button = new MessageButton(); const actionRow = new MessageActionRow({ components: [button] }); @@ -687,9 +689,6 @@ client.on('interaction', async interaction => { // @ts-expect-error interaction.reply({ content: 'Hi!', components: [[button]] }); - // @ts-expect-error - void new MessageActionRow({}); - // @ts-expect-error await interaction.reply({ content: 'Hi!', components: [button] }); From 0a1d0325c47dcc9819a49d00bcdaf0f429353076 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 17 Feb 2022 09:45:59 +1100 Subject: [PATCH 28/34] refactor: revert renaming --- src/structures/Interaction.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js index edb89ecaf87b..df40be28e9a9 100644 --- a/src/structures/Interaction.js +++ b/src/structures/Interaction.js @@ -177,7 +177,7 @@ class Interaction extends Base { * Indicates whether this interaction is a {@link ModalSubmitInteraction} * @returns {boolean} */ - isModalSubmission() { + isModalSubmit() { return InteractionTypes[this.type] === InteractionTypes.MODAL_SUBMIT; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 918df2d165b1..431124a41191 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1363,7 +1363,7 @@ export class Interaction extends Base { public isUserContextMenu(): this is UserContextMenuInteraction; public isMessageContextMenu(): this is MessageContextMenuInteraction; public isMessageComponent(): this is MessageComponentInteraction; - public isModalSubmission(): this is ModalSubmitInteraction; + public isModalSubmit(): this is ModalSubmitInteraction; public isSelectMenu(): this is SelectMenuInteraction; } From 5275590e76be124519f8defa67b2e8f360a39fe3 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 17 Feb 2022 09:49:39 +1100 Subject: [PATCH 29/34] feat: support for update and deferUpdate --- src/structures/ModalSubmitInteraction.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 4a0ab5343d31..6b03cf96a0f0 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -96,8 +96,10 @@ class ModalSubmitInteraction extends Interaction { editReply() {} deleteReply() {} followUp() {} + update() {} + deferUpdate() {} } -InteractionResponses.applyToClass(ModalSubmitInteraction, ['deferUpdate', 'update', 'showModal', 'awaitModalSubmit']); +InteractionResponses.applyToClass(ModalSubmitInteraction, ['showModal', 'awaitModalSubmit']); module.exports = ModalSubmitInteraction; From 16990c61895d60bd4c284183c455daa846261958 Mon Sep 17 00:00:00 2001 From: monbrey Date: Thu, 17 Feb 2022 10:23:27 +1100 Subject: [PATCH 30/34] feat: message reference and typeguarding --- src/structures/ModalSubmitInteraction.js | 6 ++++++ typings/index.d.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 6b03cf96a0f0..3d7f3a2596c8 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -44,6 +44,12 @@ class ModalSubmitInteraction extends Interaction { components: ModalSubmitInteraction.transformComponent(c), })) ?? []; + /** + * The message associated with this interaction + * @type {Message|APIMessage|null} + */ + this.message = this.channel?.messages._add(data.message) ?? data.message ?? null; + /** * The fields within the modal * @type {ModalSubmitFieldsResolver} diff --git a/typings/index.d.ts b/typings/index.d.ts index 431124a41191..0b1cd3123239 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1879,6 +1879,18 @@ export class ModalSubmitFieldsResolver { public getTextInputValue(customId: string): string; } +export interface ModalMessageModalSubmitInteraction + extends ModalSubmitInteraction { + message: GuildCacheMessage | null; + update(options: InteractionUpdateOptions & { fetchReply: true }): Promise>; + update(options: string | MessagePayload | InteractionUpdateOptions): Promise; + deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise>; + deferUpdate(options?: InteractionDeferUpdateOptions): Promise; + inGuild(): this is ModalMessageModalSubmitInteraction<'present'>; + inCachedGuild(): this is ModalMessageModalSubmitInteraction<'cached'>; + inRawGuild(): this is ModalMessageModalSubmitInteraction<'raw'>; +} + export class ModalSubmitInteraction extends Interaction { protected constructor(client: Client, data: RawModalSubmitInteractionData); public customId: string; @@ -1899,6 +1911,7 @@ export class ModalSubmitInteraction extend public inGuild(): this is ModalSubmitInteraction<'present'>; public inCachedGuild(): this is ModalSubmitInteraction<'cached'>; public inRawGuild(): this is ModalSubmitInteraction<'raw'>; + public isFromMessage(): this is ModalMessageModalSubmitInteraction; } export class NewsChannel extends BaseGuildTextChannel { From e4db99d6b4db8524d1c1dd0441933c6e0b7913c4 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 23 Feb 2022 08:20:49 +1100 Subject: [PATCH 31/34] types(modal): add update typings --- typings/index.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0b1cd3123239..4f9748bd48ad 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1906,12 +1906,16 @@ export class ModalSubmitInteraction extend public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise>; public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise>; public deferReply(options?: InteractionDeferReplyOptions): Promise; + public deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise>; + public deferUpdate(options?: InteractionDeferUpdateOptions): Promise; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; public inGuild(): this is ModalSubmitInteraction<'present'>; public inCachedGuild(): this is ModalSubmitInteraction<'cached'>; public inRawGuild(): this is ModalSubmitInteraction<'raw'>; public isFromMessage(): this is ModalMessageModalSubmitInteraction; + public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise>; + public update(options: string | MessagePayload | InteractionUpdateOptions): Promise; } export class NewsChannel extends BaseGuildTextChannel { From d4ae3287773e83c90bcbe2923f5b74b8891314e3 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 23 Feb 2022 08:26:49 +1100 Subject: [PATCH 32/34] fix(modal): check and set replied flags --- src/structures/interfaces/InteractionResponses.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index a149d7715cc0..4d9a6f6ccbbb 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -234,6 +234,8 @@ class InteractionResponses { * @returns {Promise} */ async showModal(modal) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + const _modal = modal instanceof Modal ? modal : new Modal(modal); await this.client.api.interactions(this.id, this.token).callback.post({ data: { @@ -241,6 +243,7 @@ class InteractionResponses { data: _modal.toJSON(), }, }); + this.replied = true; } /** From 28b6e695d0247c5b19e3438c967e672fbc1b4711 Mon Sep 17 00:00:00 2001 From: monbrey Date: Wed, 23 Feb 2022 08:27:12 +1100 Subject: [PATCH 33/34] fix(modals): correctly set message prop on interaction if present --- src/structures/ModalSubmitInteraction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/ModalSubmitInteraction.js b/src/structures/ModalSubmitInteraction.js index 3d7f3a2596c8..306320f29cc3 100644 --- a/src/structures/ModalSubmitInteraction.js +++ b/src/structures/ModalSubmitInteraction.js @@ -48,7 +48,7 @@ class ModalSubmitInteraction extends Interaction { * The message associated with this interaction * @type {Message|APIMessage|null} */ - this.message = this.channel?.messages._add(data.message) ?? data.message ?? null; + this.message = data.message ? this.channel?.messages._add(data.message) ?? data.message : null; /** * The fields within the modal From 77604a62045ef027e361e6c15bad6e612154defb Mon Sep 17 00:00:00 2001 From: monbrey Date: Sat, 26 Mar 2022 11:28:39 +1100 Subject: [PATCH 34/34] types: fix after rebase --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4f9748bd48ad..9a629c998a21 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1886,7 +1886,7 @@ export interface ModalMessageModalSubmitInteraction; deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise>; deferUpdate(options?: InteractionDeferUpdateOptions): Promise; - inGuild(): this is ModalMessageModalSubmitInteraction<'present'>; + inGuild(): this is ModalMessageModalSubmitInteraction<'raw' | 'cached'>; inCachedGuild(): this is ModalMessageModalSubmitInteraction<'cached'>; inRawGuild(): this is ModalMessageModalSubmitInteraction<'raw'>; } @@ -1910,7 +1910,7 @@ export class ModalSubmitInteraction extend public deferUpdate(options?: InteractionDeferUpdateOptions): Promise; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; - public inGuild(): this is ModalSubmitInteraction<'present'>; + public inGuild(): this is ModalSubmitInteraction<'raw' | 'cached'>; public inCachedGuild(): this is ModalSubmitInteraction<'cached'>; public inRawGuild(): this is ModalSubmitInteraction<'raw'>; public isFromMessage(): this is ModalMessageModalSubmitInteraction;