From e1cdcfa9a6baed1d373cc5474630d32ce38db31e Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Sat, 9 Apr 2022 19:36:49 +1000 Subject: [PATCH] feat(modals): modals, input text components and modal submits, v13 style (#7431) --- package-lock.json | 31 ++- package.json | 2 +- src/client/actions/InteractionCreate.js | 4 + src/errors/Messages.js | 12 + src/index.js | 3 + src/structures/BaseCommandInteraction.js | 2 + src/structures/BaseMessageComponent.js | 19 +- src/structures/Interaction.js | 8 + src/structures/MessageActionRow.js | 6 +- src/structures/MessageComponentInteraction.js | 2 + src/structures/Modal.js | 103 ++++++++ src/structures/ModalSubmitFieldsResolver.js | 53 ++++ src/structures/ModalSubmitInteraction.js | 111 ++++++++ src/structures/TextInputComponent.js | 201 +++++++++++++++ .../interfaces/InteractionResponses.js | 56 ++++- src/util/Constants.js | 13 +- typings/enums.d.ts | 13 + typings/index.d.ts | 238 +++++++++++++++--- typings/index.test-d.ts | 5 +- typings/rawDataTypes.d.ts | 10 +- 20 files changed, 836 insertions(+), 56 deletions(-) create mode 100644 src/structures/Modal.js create mode 100644 src/structures/ModalSubmitFieldsResolver.js create mode 100644 src/structures/ModalSubmitInteraction.js create mode 100644 src/structures/TextInputComponent.js 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/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/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/index.js b/src/index.js index eb99a644de2e..9e7906928feb 100644 --- a/src/index.js +++ b/src/index.js @@ -122,6 +122,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'); @@ -140,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/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/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js index c2470e0c1d62..4075827343eb 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 + * * TextInputComponentOptions * @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 + * * TextInputComponent * @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.TEXT_INPUT: { + const TextInputComponent = require('./TextInputComponent'); + component = data instanceof TextInputComponent ? data : new TextInputComponent(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/Interaction.js b/src/structures/Interaction.js index 4676a3484097..7d7d2f2364fd 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/MessageActionRow.js b/src/structures/MessageActionRow.js index c44b32a6018a..d2115d6c0594 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 + * * TextInputComponent + * @typedef {MessageButton|MessageSelectMenu|TextInputComponent} MessageActionRowComponent */ /** * Options for components that can be placed in an action row * * MessageButtonOptions * * MessageSelectMenuOptions - * @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions + * * TextInputComponentOptions + * @typedef {MessageButtonOptions|MessageSelectMenuOptions|TextInputComponentOptions} MessageActionRowComponentOptions */ /** diff --git a/src/structures/MessageComponentInteraction.js b/src/structures/MessageComponentInteraction.js index dae0107e4b72..d97d170bc278 100644 --- a/src/structures/MessageComponentInteraction.js +++ b/src/structures/MessageComponentInteraction.js @@ -101,6 +101,8 @@ class MessageComponentInteraction extends Interaction { followUp() {} deferUpdate() {} update() {} + showModal() {} + awaitModalSubmit() {} } InteractionResponses.applyToClass(MessageComponentInteraction); diff --git a/src/structures/Modal.js b/src/structures/Modal.js new file mode 100644 index 000000000000..91ec04cd0307 --- /dev/null +++ b/src/structures/Modal.js @@ -0,0 +1,103 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const Util = require('../util/Util'); + +/** + * Represents a modal (form) to be shown in response to an interaction + */ +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 {MessageActionRow[]|MessageActionRowOptions[]} [components] + * Action rows containing interactive components for the modal (text input 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 MessageActionRows in the modal + * @type {MessageActionRow[]} + */ + 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 {...MessageActionRowResolvable[]} 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 {...MessageActionRowResolvable[]} 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 {...MessageActionRowResolvable[]} [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/ModalSubmitFieldsResolver.js b/src/structures/ModalSubmitFieldsResolver.js new file mode 100644 index 000000000000..c60d1183f3c1 --- /dev/null +++ b/src/structures/ModalSubmitFieldsResolver.js @@ -0,0 +1,53 @@ +'use strict'; + +const { TypeError } = require('../errors'); +const { MessageComponentTypes } = require('../util/Constants'); + +/** + * A resolver for modal submit interaction text inputs. + */ +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.components), []); + } + + /** + * 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[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 new file mode 100644 index 000000000000..306320f29cc3 --- /dev/null +++ b/src/structures/ModalSubmitInteraction.js @@ -0,0 +1,111 @@ +'use strict'; + +const Interaction = require('./Interaction'); +const InteractionWebhook = require('./InteractionWebhook'); +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); + + /** + * The custom id of the modal. + * @type {string} + */ + 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 {PartialModalActionRow[]} + */ + this.components = + data.data.components?.map(c => ({ + type: MessageComponentTypes[c.type], + components: ModalSubmitInteraction.transformComponent(c), + })) ?? []; + + /** + * The message associated with this interaction + * @type {Message|APIMessage|null} + */ + this.message = data.message ? this.channel?.messages._add(data.message) ?? data.message : null; + + /** + * The fields within the modal + * @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); + } + + /** + * 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() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} + update() {} + deferUpdate() {} +} + +InteractionResponses.applyToClass(ModalSubmitInteraction, ['showModal', 'awaitModalSubmit']); + +module.exports = ModalSubmitInteraction; diff --git a/src/structures/TextInputComponent.js b/src/structures/TextInputComponent.js new file mode 100644 index 000000000000..721804800ddf --- /dev/null +++ b/src/structures/TextInputComponent.js @@ -0,0 +1,201 @@ +'use strict'; + +const BaseMessageComponent = require('./BaseMessageComponent'); +const { RangeError } = require('../errors'); +const { TextInputStyles, MessageComponentTypes } = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * Represents a text input component in a modal + * @extends {BaseMessageComponent} + */ + +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 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 text input component is required + * @property {TextInputStyleResolvable} [style] The style of this text input component + * @property {string} [value] Value of this text input component + */ + + /** + * @param {TextInputComponent|TextInputComponentOptions} [data={}] TextInputComponent to clone or raw data + */ + constructor(data = {}) { + super({ type: 'TEXT_INPUT' }); + + 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 text input 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 text input component is required + * @type {?boolean} + */ + this.required = data.required ?? false; + + /** + * The style of this text input component + * @type {?TextInputStyle} + */ + this.style = data.style ? TextInputComponent.resolveStyle(data.style) : null; + + /** + * Value of this text input component + * @type {?string} + */ + this.value = data.value ?? null; + } + + /** + * 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} + */ + setCustomId(customId) { + this.customId = Util.verifyString(customId, RangeError, 'TEXT_INPUT_CUSTOM_ID'); + return this; + } + + /** + * 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) { + this.label = Util.verifyString(label, RangeError, 'TEXT_INPUT_LABEL'); + return this; + } + + /** + * 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) { + this.required = required; + return this; + } + + /** + * 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} + */ + setMaxLength(maxLength) { + this.maxLength = maxLength; + return this; + } + + /** + * 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} + */ + setMinLength(minLength) { + this.minLength = minLength; + return this; + } + + /** + * Sets the placeholder of this text input component + * @param {string} placeholder Custom placeholder text to display when no text is entered + * @returns {TextInputComponent} + */ + setPlaceholder(placeholder) { + this.placeholder = Util.verifyString(placeholder, RangeError, 'TEXT_INPUT_PLACEHOLDER'); + return this; + } + + /** + * Sets the style of this text input component + * @param {TextInputStyleResolvable} style The style of this text input component + * @returns {TextInputComponent} + */ + setStyle(style) { + this.style = TextInputComponent.resolveStyle(style); + return this; + } + + /** + * Sets the value of this text input component + * @param {string} value Value of this text input component + * @returns {TextInputComponent} + */ + setValue(value) { + this.value = Util.verifyString(value, RangeError, 'TEXT_INPUT_VALUE'); + return this; + } + + /** + * Transforms the text input component into a plain object + * @returns {APITextInput} The raw data of this text input 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: TextInputStyles[this.style], + type: MessageComponentTypes[this.type], + value: this.value, + }; + } + + /** + * Data that can be resolved to a TextInputStyle. This can be + * * TextInputStyle + * * number + * @typedef {number|TextInputStyle} TextInputStyleResolvable + */ + + /** + * Resolves the style of a text input component + * @param {TextInputStyleResolvable} style The style to resolve + * @returns {TextInputStyle} + * @private + */ + static resolveStyle(style) { + return typeof style === 'string' ? style : TextInputStyles[style]; + } +} + +module.exports = TextInputComponent; diff --git a/src/structures/interfaces/InteractionResponses.js b/src/structures/interfaces/InteractionResponses.js index 17badddefb57..4d9a6f6ccbbb 100644 --- a/src/structures/interfaces/InteractionResponses.js +++ b/src/structures/interfaces/InteractionResponses.js @@ -1,9 +1,11 @@ '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'); /** * Interface for classes that support shared interaction response types. @@ -226,6 +228,56 @@ class InteractionResponses { return options.fetchReply ? this.fetchReply() : undefined; } + /** + * Shows a modal component + * @param {Modal|ModalOptions} modal The modal to show + * @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: { + type: InteractionResponseTypes.MODAL, + data: _modal.toJSON(), + }, + }); + this.replied = true; + } + + /** + * 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', @@ -236,6 +288,8 @@ class InteractionResponses { 'followUp', 'deferUpdate', 'update', + 'showModal', + 'awaitModalSubmit', ]; for (const prop of props) { diff --git a/src/util/Constants.js b/src/util/Constants.js index f9464442b14e..ad8bd920f0aa 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1076,6 +1076,7 @@ exports.InteractionTypes = createEnum([ 'APPLICATION_COMMAND', 'MESSAGE_COMPONENT', 'APPLICATION_COMMAND_AUTOCOMPLETE', + 'MODAL_SUBMIT', ]); /** @@ -1099,6 +1100,7 @@ exports.InteractionResponseTypes = createEnum([ 'DEFERRED_MESSAGE_UPDATE', 'UPDATE_MESSAGE', 'APPLICATION_COMMAND_AUTOCOMPLETE_RESULT', + 'MODAL', ]); /** @@ -1109,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']); +exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU', 'TEXT_INPUT']); /** * The style of a message button @@ -1152,6 +1154,15 @@ exports.NSFWLevels = createEnum(['DEFAULT', 'EXPLICIT', 'SAFE', 'AGE_RESTRICTED' */ exports.PrivacyLevels = createEnum([null, 'PUBLIC', 'GUILD_ONLY']); +/** + * The style of a text input component + * * SHORT + * * PARAGRAPH + * @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 d67d310eb5f2..f4067d90b694 100644 --- a/typings/enums.d.ts +++ b/typings/enums.d.ts @@ -111,6 +111,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 +119,7 @@ export const enum InteractionTypes { APPLICATION_COMMAND = 2, MESSAGE_COMPONENT = 3, APPLICATION_COMMAND_AUTOCOMPLETE = 4, + MODAL_SUBMIT = 5, } export const enum InviteTargetType { @@ -142,6 +144,12 @@ export const enum MessageComponentTypes { ACTION_ROW = 1, BUTTON = 2, SELECT_MENU = 3, + TEXT_INPUT = 4, +} + +export const enum ModalComponentTypes { + ACTION_ROW = 1, + TEXT_INPUT = 4, } export const enum MFALevels { @@ -184,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 6d2a8c77c094..160133970fe1 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, @@ -71,6 +75,7 @@ import { MessageButtonStyles, MessageComponentTypes, MessageTypes, + ModalComponentTypes, MFALevels, NSFWLevels, OverwriteTypes, @@ -78,6 +83,7 @@ import { PrivacyLevels, StickerFormatTypes, StickerTypes, + TextInputStyles, VerificationLevels, WebhookTypes, GuildScheduledEventEntityTypes, @@ -117,6 +123,7 @@ import { RawMessagePayloadData, RawMessageReactionData, RawMessageSelectMenuInteractionData, + RawModalSubmitInteractionData, RawOAuth2GuildData, RawPartialGroupDMChannelData, RawPartialMessageData, @@ -130,6 +137,7 @@ import { RawStickerPackData, RawTeamData, RawTeamMemberData, + RawTextInputComponentData, RawThreadChannelData, RawThreadMemberData, RawTypingData, @@ -360,6 +368,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'>; @@ -371,6 +382,7 @@ export abstract class BaseCommandInteraction>; 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'], @@ -1366,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; public isRepliable(): this is this & InteractionResponseFields; } @@ -1501,6 +1514,7 @@ export type MappedInteractionTypes = EnumValue BUTTON: ButtonInteraction>; SELECT_MENU: SelectMenuInteraction>; ACTION_ROW: MessageComponentInteraction>; + TEXT_INPUT: ModalSubmitInteraction>; } >; @@ -1578,22 +1592,20 @@ 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, + V = T extends ModalActionRowComponent + ? APIActionRowComponent + : APIActionRowComponent, +> extends BaseMessageComponent { + public constructor(data?: MessageActionRow | MessageActionRowOptions | V); 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 toJSON(): APIActionRowComponent; + 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(): V; } export class MessageAttachment { @@ -1656,9 +1668,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; @@ -1668,6 +1680,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'>; @@ -1681,6 +1696,7 @@ export class MessageComponentInteraction e public followUp(options: string | MessagePayload | InteractionReplyOptions): 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; @@ -1842,6 +1858,82 @@ export class MessageSelectMenu extends BaseMessageComponent { public toJSON(): APISelectMenuComponent; } +export class Modal { + public constructor(data?: Modal | ModalOptions); + public components: MessageActionRow[]; + public customId: string | null; + public title: string | null; + public addComponents( + ...components: ( + | MessageActionRow + | (Required & MessageActionRowOptions) + )[] + ): this; + public setComponents( + ...components: ( + | MessageActionRow + | (Required & MessageActionRowOptions) + )[] + ): this; + public setCustomId(customId: string): this; + public spliceComponents( + index: number, + deleteCount: number, + ...components: ( + | MessageActionRow + | (Required & MessageActionRowOptions) + )[] + ): this; + public setTitle(title: string): this; + public toJSON(): RawModalSubmitInteractionData; +} + +export class ModalSubmitFieldsResolver { + constructor(components: PartialModalActionRow[]); + private readonly _fields: PartialTextInputData[]; + public getField(customId: string): PartialTextInputData; + 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<'raw' | 'cached'>; + inCachedGuild(): this is ModalMessageModalSubmitInteraction<'cached'>; + inRawGuild(): this is ModalMessageModalSubmitInteraction<'raw'>; +} + +export class ModalSubmitInteraction extends Interaction { + 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 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 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<'raw' | 'cached'>; + 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 { public threads: ThreadManager; public type: 'GUILD_NEWS'; @@ -2319,6 +2411,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(): RawTextInputComponentData; + 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; @@ -2865,6 +2979,8 @@ export const Constants: { InteractionResponseTypes: EnumHolder; MessageComponentTypes: EnumHolder; MessageButtonStyles: EnumHolder; + ModalComponentTypes: EnumHolder; + TextInputStyles: EnumHolder; MFALevels: EnumHolder; NSFWLevels: EnumHolder; PrivacyLevels: EnumHolder; @@ -3808,6 +3924,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[]; } @@ -4920,11 +5043,6 @@ export interface ImageURLOptions extends Omit { format?: DynamicImageFormat; } -export interface IntegrationAccount { - id: string | Snowflake; - name: string; -} - export type IntegrationType = 'twitch' | 'youtube' | 'discord'; export interface InteractionCollectorOptions @@ -5044,10 +5162,26 @@ export type MessageActionRowComponentOptions = | (Required & MessageButtonOptions) | (Required & MessageSelectMenuOptions); -export type MessageActionRowComponentResolvable = MessageActionRowComponent | MessageActionRowComponentOptions; +export type MessageActionRowComponentResolvable = + | MessageActionRowComponent + | MessageActionRowComponentOptions + | APIMessageActionRowComponent; + +export type ModalActionRowComponent = TextInputComponent; -export interface MessageActionRowOptions extends BaseMessageComponentOptions { - components: MessageActionRowComponentResolvable[]; +export type ModalActionRowComponentOptions = TextInputComponentOptions; + +export type ModalActionRowComponentResolvable = + | ModalActionRowComponent + | ModalActionRowComponentOptions + | APIModalActionRowComponent; + +export interface MessageActionRowOptions< + T extends + | MessageActionRowComponentResolvable + | ModalActionRowComponentResolvable = MessageActionRowComponentResolvable, +> extends BaseMessageComponentOptions { + components: T[]; } export interface MessageActivity { @@ -5084,15 +5218,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 @@ -5276,6 +5408,19 @@ export type MessageType = keyof typeof MessageTypes; export type MFALevel = keyof typeof MFALevels; +export interface ModalOptions { + components: + | MessageActionRow[] + | MessageActionRowOptions[]; + customId: string; + title: string; +} + +export type ModalSubmitInteractionCollectorOptions = Omit< + InteractionCollectorOptions, + 'channel' | 'message' | 'guild' | 'interactionType' +>; + export interface MultipleShardRespawnOptions { shardDelay?: number; respawnDelay?: number; @@ -5301,6 +5446,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>; @@ -5647,6 +5804,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/index.test-d.ts b/typings/index.test-d.ts index 0a78e3093440..30be75af7392 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -679,6 +679,8 @@ client.on('interaction', async interaction => { void new MessageActionRow(); + void new MessageActionRow({}); + const button = new MessageButton(); const actionRow = new MessageActionRow({ components: [button] }); @@ -688,9 +690,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] }); diff --git a/typings/rawDataTypes.d.ts b/typings/rawDataTypes.d.ts index 885566d40d0c..7c69081965e3 100644 --- a/typings/rawDataTypes.d.ts +++ b/typings/rawDataTypes.d.ts @@ -76,8 +76,13 @@ import { RESTPostAPIWebhookWithTokenJSONBody, Snowflake, APIGuildScheduledEvent, + APIActionRowComponent, + APITextInputComponent, + APIModalActionRowComponent, + APIModalSubmitInteraction, } 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 +146,9 @@ export type RawMessageComponentInteractionData = APIMessageComponentInteraction; export type RawMessageButtonInteractionData = APIMessageButtonInteractionData; export type RawMessageSelectMenuInteractionData = APIMessageSelectMenuInteractionData; +export type RawTextInputComponentData = APITextInputComponent; +export type RawModalSubmitInteractionData = APIModalSubmitInteraction; + export type RawInviteData = | APIExtendedInvite | APIInvite