diff --git a/packages/discord.js/src/errors/ErrorCodes.js b/packages/discord.js/src/errors/ErrorCodes.js index 1ac26a946..cb9a351c8 100644 --- a/packages/discord.js/src/errors/ErrorCodes.js +++ b/packages/discord.js/src/errors/ErrorCodes.js @@ -114,8 +114,10 @@ * @property {'CommandInteractionOptionInvalidChannelType'} CommandInteractionOptionInvalidChannelType * @property {'AutocompleteInteractionOptionNoFocusedOption'} AutocompleteInteractionOptionNoFocusedOption * - * @property {'ModalSubmitInteractionFieldNotFound'} ModalSubmitInteractionFieldNotFound - * @property {'ModalSubmitInteractionFieldType'} ModalSubmitInteractionFieldType + * @property {'ModalSubmitInteractionComponentNotFound'} ModalSubmitInteractionComponentNotFound + * @property {'ModalSubmitInteractionComponentType'} ModalSubmitInteractionComponentType + * @property {'ModalSubmitInteractionComponentEmpty'} ModalSubmitInteractionComponentEmpty + * @property {'ModalSubmitInteractionComponentInvalidChannelType'} ModalSubmitInteractionComponentInvalidChannelType * * @property {'InvalidMissingScopes'} InvalidMissingScopes * @property {'InvalidScopesWithPermissions'} InvalidScopesWithPermissions @@ -248,8 +250,10 @@ const keys = [ 'CommandInteractionOptionInvalidChannelType', 'AutocompleteInteractionOptionNoFocusedOption', - 'ModalSubmitInteractionFieldNotFound', - 'ModalSubmitInteractionFieldType', + 'ModalSubmitInteractionComponentNotFound', + 'ModalSubmitInteractionComponentType', + 'ModalSubmitInteractionComponentEmpty', + 'ModalSubmitInteractionComponentInvalidChannelType', 'InvalidMissingScopes', 'InvalidScopesWithPermissions', diff --git a/packages/discord.js/src/errors/Messages.js b/packages/discord.js/src/errors/Messages.js index 1b00d3a4f..cec5a9ead 100644 --- a/packages/discord.js/src/errors/Messages.js +++ b/packages/discord.js/src/errors/Messages.js @@ -127,10 +127,14 @@ const Messages = { `The type of channel of the option "${name}" is: ${type}; expected ${expected}.`, [ErrorCodes.AutocompleteInteractionOptionNoFocusedOption]: 'No focused option for autocomplete interaction.', - [ErrorCodes.ModalSubmitInteractionFieldNotFound]: customId => - `Required field with custom id "${customId}" not found.`, - [ErrorCodes.ModalSubmitInteractionFieldType]: (customId, type, expected) => - `Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`, + [ErrorCodes.ModalSubmitInteractionComponentNotFound]: customId => + `Required component with custom id "${customId}" not found.`, + [ErrorCodes.ModalSubmitInteractionComponentType]: (customId, type, expected) => + `Component with custom id "${customId}" is of type: ${type}; expected ${expected}.`, + [ErrorCodes.ModalSubmitInteractionComponentEmpty]: (customId, type) => + `Required component with custom id "${customId}" is of type: ${type}; expected a non-empty value.`, + [ErrorCodes.ModalSubmitInteractionComponentInvalidChannelType]: (customId, type, expected) => + `The type of channel of the component with custom id "${customId}" is: ${type}; expected ${expected}.`, [ErrorCodes.InvalidMissingScopes]: 'At least one valid scope must be provided for the invite', [ErrorCodes.InvalidScopesWithPermissions]: 'Permissions cannot be set without the bot scope.', diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 1850749fe..6f9ecc520 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -199,7 +199,7 @@ exports.MessageContextMenuCommandInteraction = exports.MessageMentions = require('./structures/MessageMentions.js').MessageMentions; exports.MessagePayload = require('./structures/MessagePayload.js').MessagePayload; exports.MessageReaction = require('./structures/MessageReaction.js').MessageReaction; -exports.ModalSubmitFields = require('./structures/ModalSubmitFields.js').ModalSubmitFields; +exports.ModalComponentResolver = require('./structures/ModalComponentResolver.js').ModalComponentResolver; exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction.js').ModalSubmitInteraction; exports.OAuth2Guild = require('./structures/OAuth2Guild.js').OAuth2Guild; exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel.js').PartialGroupDMChannel; diff --git a/packages/discord.js/src/structures/CommandInteraction.js b/packages/discord.js/src/structures/CommandInteraction.js index b30ce72d8..11aa38579 100644 --- a/packages/discord.js/src/structures/CommandInteraction.js +++ b/packages/discord.js/src/structures/CommandInteraction.js @@ -91,13 +91,17 @@ class CommandInteraction extends BaseInteraction { } /** - * Represents the resolved data of a received command interaction. - * - * @typedef {Object} CommandInteractionResolvedData + * @typedef {Object} BaseInteractionResolvedData * @property {Collection} [users] The resolved users * @property {Collection} [members] The resolved guild members * @property {Collection} [roles] The resolved roles * @property {Collection} [channels] The resolved channels + */ + + /** + * Represents the resolved data of a received command interaction. + * + * @typedef {BaseInteractionResolvedData} CommandInteractionResolvedData * @property {Collection} [messages] The resolved messages * @property {Collection} [attachments] The resolved attachments */ diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index c3b85e888..fd49f1d72 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -634,7 +634,7 @@ class Message extends Base { * Resolves with a collection of reactions that pass the specified filter. * * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector - * @returns {Promise>} + * @returns {Promise>} * @example * // Create a reaction collector * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId' diff --git a/packages/discord.js/src/structures/ModalComponentResolver.js b/packages/discord.js/src/structures/ModalComponentResolver.js new file mode 100644 index 000000000..0ab7fcc53 --- /dev/null +++ b/packages/discord.js/src/structures/ModalComponentResolver.js @@ -0,0 +1,228 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { ComponentType } = require('discord-api-types/v10'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors/index.js'); + +/** + * @typedef {Object} ModalSelectedMentionables + * @property {Collection} users The selected users + * @property {Collection} members The selected members + * @property {Collection} roles The selected roles + */ + +/** + * A resolver for modal submit components + */ +class ModalComponentResolver { + constructor(client, components, resolved) { + /** + * The client that instantiated this. + * + * @name ModalComponentResolver#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The interaction resolved data + * + * @name ModalComponentResolver#resolved + * @type {?Readonly} + */ + Object.defineProperty(this, 'resolved', { value: resolved ? Object.freeze(resolved) : null }); + + /** + * The components within the modal + * + * @type {Array} + */ + this.data = components; + + /** + * The bottom-level components of the interaction + * + * @type {Collection} + */ + this.hoistedComponents = components.reduce((accumulator, next) => { + // For legacy support of action rows + if ('components' in next) { + for (const component of next.components) accumulator.set(component.customId, component); + } + + // For label components + if ('component' in next) { + accumulator.set(next.component.customId, next.component); + } + + return accumulator; + }, new Collection()); + } + + /** + * Gets a component by custom id. + * + * @property {string} customId The custom id of the component. + * @returns {ModalData} + */ + getComponent(customId) { + const component = this.hoistedComponents.get(customId); + + if (!component) throw new DiscordjsTypeError(ErrorCodes.ModalSubmitInteractionComponentNotFound, customId); + + return component; + } + + /** + * Gets a component by custom id and property and checks its type. + * + * @param {string} customId The custom id of the component. + * @param {ComponentType[]} allowedTypes The allowed types of the component. + * @param {string[]} properties The properties to check for for `required`. + * @param {boolean} required Whether to throw an error if the component value(s) are not found. + * @returns {ModalData} The option, if found. + * @private + */ + _getTypedComponent(customId, allowedTypes, properties, required) { + const component = this.getComponent(customId); + if (!allowedTypes.includes(component.type)) { + throw new DiscordjsTypeError( + ErrorCodes.ModalSubmitInteractionComponentType, + customId, + component.type, + allowedTypes.join(', '), + ); + } else if (required && properties.every(prop => component[prop] === null || component[prop] === undefined)) { + throw new DiscordjsTypeError(ErrorCodes.ModalSubmitInteractionComponentEmpty, customId, component.type); + } + + return component; + } + + /** + * Gets the value of a text input component + * + * @param {string} customId The custom id of the text input component + * @param {?boolean} required Whether to throw an error if the component value is not found or empty + * @returns {?string} + */ + getTextInputValue(customId, required = false) { + return this._getTypedComponent(customId, [ComponentType.TextInput], ['value'], required).value ?? null; + } + + /** + * Gets the values of a string select component + * + * @param {string} customId The custom id of the string select component + * @returns {string[]} + */ + getStringSelectValues(customId) { + return this._getTypedComponent(customId, [ComponentType.StringSelect]).values; + } + + /** + * Gets users component + * + * @param {string} customId The custom id of the component + * @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty + * @returns {?Collection} The selected users, or null if none were selected and not required + */ + getSelectedUsers(customId, required = false) { + const component = this._getTypedComponent( + customId, + [ComponentType.UserSelect, ComponentType.MentionableSelect], + ['users'], + required, + ); + return component.users ?? null; + } + + /** + * Gets roles component + * + * @param {string} customId The custom id of the component + * @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty + * @returns {?Collection} The selected roles, or null if none were selected and not required + */ + getSelectedRoles(customId, required = false) { + const component = this._getTypedComponent( + customId, + [ComponentType.RoleSelect, ComponentType.MentionableSelect], + ['roles'], + required, + ); + return component.roles ?? null; + } + + /** + * Gets channels component + * + * @param {string} customId The custom id of the component + * @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty + * @param {ChannelType[]} [channelTypes=[]] The allowed types of channels. If empty, all channel types are allowed. + * @returns {?Collection} The selected channels, or null if none were selected and not required + */ + getSelectedChannels(customId, required = false, channelTypes = []) { + const component = this._getTypedComponent(customId, [ComponentType.ChannelSelect], ['channels'], required); + const channels = component.channels; + if (channels && channelTypes.length > 0) { + for (const channel of channels.values()) { + if (!channelTypes.includes(channel.type)) { + throw new DiscordjsTypeError( + ErrorCodes.ModalSubmitInteractionComponentInvalidChannelType, + customId, + channel.type, + channelTypes.join(', '), + ); + } + } + } + + return channels ?? null; + } + + /** + * Gets members component + * + * @param {string} customId The custom id of the component + * @returns {?Collection} The selected members, or null if none were selected or the users were not present in the guild + */ + getSelectedMembers(customId) { + const component = this._getTypedComponent( + customId, + [ComponentType.UserSelect, ComponentType.MentionableSelect], + ['members'], + false, + ); + return component.members ?? null; + } + + /** + * Gets mentionables component + * + * @param {string} customId The custom id of the component + * @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty + * @returns {?ModalSelectedMentionables} The selected mentionables, or null if none were selected and not required + */ + getSelectedMentionables(customId, required = false) { + const component = this._getTypedComponent( + customId, + [ComponentType.MentionableSelect], + ['users', 'members', 'roles'], + required, + ); + + if (component.users || component.members || component.roles) { + return { + users: component.users ?? new Collection(), + members: component.members ?? new Collection(), + roles: component.roles ?? new Collection(), + }; + } + + return null; + } +} + +exports.ModalComponentResolver = ModalComponentResolver; diff --git a/packages/discord.js/src/structures/ModalSubmitFields.js b/packages/discord.js/src/structures/ModalSubmitFields.js deleted file mode 100644 index a1b580dd0..000000000 --- a/packages/discord.js/src/structures/ModalSubmitFields.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const { ComponentType } = require('discord-api-types/v10'); -const { DiscordjsTypeError, ErrorCodes } = require('../errors/index.js'); - -/** - * Represents the serialized fields from a modal submit interaction - */ -class ModalSubmitFields { - constructor(components) { - /** - * The components within the modal - * - * @type {Array} - */ - this.components = components; - - /** - * The extracted fields from the modal - * - * @type {Collection} - */ - this.fields = components.reduce((accumulator, next) => { - // NOTE: for legacy support of action rows in modals, which has `components` - if ('components' in next) { - for (const component of next.components) accumulator.set(component.customId, component); - } - - // For label component - if ('component' in next) { - accumulator.set(next.component.customId, next.component); - } - - return accumulator; - }, new Collection()); - } - - /** - * Gets a field given a custom id from a component - * - * @param {string} customId The custom id of the component - * @param {ComponentType} [type] The type of the component - * @returns {ModalData} - */ - getField(customId, type) { - const field = this.fields.get(customId); - if (!field) throw new DiscordjsTypeError(ErrorCodes.ModalSubmitInteractionFieldNotFound, customId); - - if (type !== undefined && type !== field.type) { - throw new DiscordjsTypeError(ErrorCodes.ModalSubmitInteractionFieldType, customId, field.type, type); - } - - 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) { - return this.getField(customId, ComponentType.TextInput).value; - } - - /** - * Gets the values of a string select component given a custom id - * - * @param {string} customId The custom id of the string select component - * @returns {string[]} - */ - getStringSelectValues(customId) { - return this.getField(customId, ComponentType.StringSelect).values; - } -} - -exports.ModalSubmitFields = ModalSubmitFields; diff --git a/packages/discord.js/src/structures/ModalSubmitInteraction.js b/packages/discord.js/src/structures/ModalSubmitInteraction.js index 6b5fa0e37..7e595757d 100644 --- a/packages/discord.js/src/structures/ModalSubmitInteraction.js +++ b/packages/discord.js/src/structures/ModalSubmitInteraction.js @@ -1,46 +1,53 @@ 'use strict'; +const { Collection } = require('@discordjs/collection'); const { lazy } = require('@discordjs/util'); +const { transformResolved } = require('../util/Util.js'); const { BaseInteraction } = require('./BaseInteraction.js'); const { InteractionWebhook } = require('./InteractionWebhook.js'); -const { ModalSubmitFields } = require('./ModalSubmitFields.js'); +const { ModalComponentResolver } = require('./ModalComponentResolver.js'); const { InteractionResponses } = require('./interfaces/InteractionResponses.js'); const getMessage = lazy(() => require('./Message.js').Message); /** * @typedef {Object} BaseModalData - * @property {ComponentType} type The component type of the field - * @property {string} customId The custom id of the field - * @property {number} id The id of the field + * @property {ComponentType} type The component type of the component + * @property {number} id The id of the component + */ + +/** + * @typedef {BaseModalData} SelectMenuModalData + * @property {string} customId The custom id of the component + * @property {string[]} values The values of the component + * @property {Collection} [members] The resolved members + * @property {Collection} [users] The resolved users + * @property {Collection} [roles] The resolved roles + * @property {Collection} [channels] The resolved channels */ /** * @typedef {BaseModalData} TextInputModalData - * @property {string} value The value of the field + * @property {string} customId The custom id of the component + * @property {string} value The value of the component */ /** - * @typedef {BaseModalData} StringSelectModalData - * @property {string[]} values The values of the field + * @typedef {BaseModalData} TextDisplayModalData */ /** - * @typedef {TextInputModalData | StringSelectModalData} ModalData + * @typedef {SelectMenuModalData|TextInputModalData} ModalData */ /** - * @typedef {Object} LabelModalData + * @typedef {BaseModalData} LabelModalData * @property {ModalData} component The component within the label - * @property {ComponentType} type The component type of the label - * @property {number} id The id of the label */ /** - * @typedef {Object} ActionRowModalData + * @typedef {BaseModalData} ActionRowModalData * @property {TextInputModalData[]} components The components of this action row - * @property {ComponentType} type The component type of the action row - * @property {number} id The id of the action row */ /** @@ -73,16 +80,13 @@ class ModalSubmitInteraction extends BaseInteraction { /** * The components within the modal * - * @type {Array} + * @type {ModalComponentResolver} */ - this.components = data.data.components?.map(component => ModalSubmitInteraction.transformComponent(component)); - - /** - * The fields within the modal - * - * @type {ModalSubmitFields} - */ - this.fields = new ModalSubmitFields(this.components); + this.components = new ModalComponentResolver( + this.client, + data.data.components?.map(component => this.transformComponent(component, data.data.resolved)), + transformResolved({ client: this.client, guild: this.guild, channel: this.channel }, data.data.resolved), + ); /** * Whether the reply to this interaction has been deferred @@ -117,15 +121,16 @@ class ModalSubmitInteraction extends BaseInteraction { * Transforms component data to discord.js-compatible data * * @param {*} rawComponent The data to transform + * @param {APIInteractionDataResolved} resolved The resolved data for the interaction * @returns {ModalData[]} * @private */ - static transformComponent(rawComponent) { + transformComponent(rawComponent, resolved) { if ('components' in rawComponent) { return { type: rawComponent.type, id: rawComponent.id, - components: rawComponent.components.map(component => this.transformComponent(component)), + components: rawComponent.components.map(component => this.transformComponent(component, resolved)), }; } @@ -133,18 +138,50 @@ class ModalSubmitInteraction extends BaseInteraction { return { type: rawComponent.type, id: rawComponent.id, - component: this.transformComponent(rawComponent.component), + component: this.transformComponent(rawComponent.component, resolved), }; } const data = { type: rawComponent.type, - customId: rawComponent.custom_id, id: rawComponent.id, }; + // Text display components do not have custom ids. + if (rawComponent.custom_id) data.customId = rawComponent.custom_id; + if (rawComponent.value) data.value = rawComponent.value; - if (rawComponent.values) data.values = rawComponent.values; + + if (rawComponent.values) { + data.values = rawComponent.values; + if (resolved) { + const resolveCollection = (resolvedData, resolver) => { + const collection = new Collection(); + for (const value of data.values) { + if (resolvedData?.[value]) { + collection.set(value, resolver(resolvedData[value])); + } + } + + return collection.size ? collection : null; + }; + + const users = resolveCollection(resolved.users, user => this.client.users._add(user)); + if (users) data.users = users; + + const channels = resolveCollection( + resolved.channels, + channel => this.client.channels._add(channel, this.guild) ?? channel, + ); + if (channels) data.channels = channels; + + const members = resolveCollection(resolved.members, member => this.guild?.members._add(member) ?? member); + if (members) data.members = members; + + const roles = resolveCollection(resolved.roles, role => this.guild?.roles._add(role) ?? role); + if (roles) data.roles = roles; + } + } return data; } diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 9bda6020e..7a8488e49 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -19,11 +19,12 @@ const { ComponentType } = require('discord-api-types/v10'); * @typedef {Object} ModalComponentData * @property {string} title The title of the modal * @property {string} customId The custom id of the modal - * @property {LabelData[]} components The components within this modal + * @property {Array} components The components within this modal */ /** - * @typedef {StringSelectMenuComponentData|TextInputComponentData} ComponentInLabelData + * @typedef {StringSelectMenuComponentData|TextInputComponentData|UserSelectMenuComponentData| + * RoleSelectMenuComponentData|MentionableSelectMenuComponentData|ChannelSelectMenuComponentData} ComponentInLabelData */ /** @@ -44,16 +45,41 @@ const { ComponentType } = require('discord-api-types/v10'); */ /** - * @typedef {BaseComponentData} StringSelectMenuComponentData + * @typedef {BaseComponentData} BaseSelectMenuComponentData * @property {string} customId The custom id of the select menu * @property {boolean} [disabled] Whether the select menu is disabled or not * @property {number} [maxValues] The maximum amount of options that can be selected * @property {number} [minValues] The minimum amount of options that can be selected - * @property {SelectMenuComponentOptionData[]} [options] The options in this select menu * @property {string} [placeholder] The placeholder of the select menu * @property {boolean} [required] Whether this component is required in modals */ +/** + * @typedef {BaseSelectMenuComponentData} StringSelectMenuComponentData + * @property {SelectMenuComponentOptionData[]} [options] The options in this select menu + */ + +/** + * @typedef {BaseSelectMenuComponentData} UserSelectMenuComponentData + * @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu + */ + +/** + * @typedef {BaseSelectMenuComponentData} RoleSelectMenuComponentData + * @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu + */ + +/** + * @typedef {BaseSelectMenuComponentData} MentionableSelectMenuComponentData + * @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu + */ + +/** + * @typedef {BaseSelectMenuComponentData} ChannelSelectMenuComponentData + * @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu + * @property {ChannelType[]} [channelTypes] The types of channels that can be selected + */ + /** * @typedef {Object} SelectMenuComponentOptionData * @property {string} label The label of the option diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index d2e78ff43..ea7b29c80 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -272,7 +272,13 @@ export interface ActionRowData { - customId: string; id: number; type: Type; } export interface TextInputModalData extends BaseModalData { + customId: string; value: string; } -export interface StringSelectModalData extends BaseModalData { +export interface SelectMenuModalData + extends BaseModalData< + | ComponentType.ChannelSelect + | ComponentType.MentionableSelect + | ComponentType.RoleSelect + | ComponentType.StringSelect + | ComponentType.UserSelect + > { + channels?: ReadonlyCollection< + Snowflake, + CacheTypeReducer + >; + customId: string; + members?: ReadonlyCollection>; + roles?: ReadonlyCollection>; + users?: ReadonlyCollection; values: readonly string[]; } -export type ModalData = StringSelectModalData | TextInputModalData; +export type ModalData = SelectMenuModalData | TextInputModalData; -export interface LabelModalData { +export interface LabelModalData extends BaseModalData { component: readonly ModalData[]; - id: number; - type: ComponentType.Label; } -export interface ActionRowModalData { +export interface ActionRowModalData extends BaseModalData { components: readonly TextInputModalData[]; - type: ComponentType.ActionRow; } -export class ModalSubmitFields { - private constructor(components: readonly (ActionRowModalData | LabelModalData)[]); - public components: (ActionRowModalData | LabelModalData)[]; - public fields: Collection; - public getField( +export interface TextDisplayModalData extends BaseModalData {} + +export interface ModalSelectedMentionables { + members: NonNullable['members']>; + roles: NonNullable['roles']>; + users: NonNullable['users']>; +} +export class ModalComponentResolver { + private constructor(client: Client, components: readonly ModalData[], resolved: BaseInteractionResolvedData); + public readonly client: Client; + public readonly data: readonly (ActionRowModalData | LabelModalData | TextDisplayModalData)[]; + public readonly resolved: Readonly> | null; + public readonly hoistedComponents: ReadonlyCollection; + public getComponent(customId: string): ModalData; + private _getTypedComponent( customId: string, - type: Type, - ): { type: Type } & (StringSelectModalData | TextInputModalData); - public getField(customId: string, type?: ComponentType): StringSelectModalData | TextInputModalData; - public getTextInputValue(customId: string): string; + allowedTypes: readonly ComponentType[], + properties: string, + required: boolean, + ): ModalData; + public getTextInputValue(customId: string, required: true): string; + public getTextInputValue(customId: string, required?: boolean): string | null; public getStringSelectValues(customId: string): readonly string[]; + public getSelectedUsers(customId: string, required: true): ReadonlyCollection; + public getSelectedUsers(customId: string, required?: boolean): ReadonlyCollection | null; + public getSelectedMembers(customId: string): NonNullable['members']> | null; + public getSelectedChannels( + customId: string, + required: true, + channelTypes?: readonly Type[], + ): ReadonlyCollection< + Snowflake, + Extract< + NonNullable['channel']>, + { + type: Type extends ChannelType.AnnouncementThread | ChannelType.PublicThread + ? ChannelType.AnnouncementThread | ChannelType.PublicThread + : Type; + } + > + >; + public getSelectedChannels( + customId: string, + required?: boolean, + channelTypes?: readonly Type[], + ): ReadonlyCollection< + Snowflake, + Extract< + NonNullable['channel']>, + { + type: Type extends ChannelType.AnnouncementThread | ChannelType.PublicThread + ? ChannelType.AnnouncementThread | ChannelType.PublicThread + : Type; + } + > + > | null; + + public getSelectedRoles(customId: string, required: true): NonNullable['roles']>; + public getSelectedRoles( + customId: string, + required?: boolean, + ): NonNullable['roles']> | null; + + public getSelectedMentionables(customId: string, required: true): ModalSelectedMentionables; + public getSelectedMentionables(customId: string, required?: boolean): ModalSelectedMentionables | null; } export interface ModalMessageModalSubmitInteraction @@ -2604,8 +2676,7 @@ export class ModalSubmitInteraction extend private constructor(client: Client, data: APIModalSubmitInteraction); public type: InteractionType.ModalSubmit; public readonly customId: string; - public readonly components: (ActionRowModalData | LabelModalData)[]; - public readonly fields: ModalSubmitFields; + public readonly components: ModalComponentResolver; public deferred: boolean; public ephemeral: boolean | null; public message: Message> | null; @@ -4021,8 +4092,10 @@ export enum DiscordjsErrorCodes { CommandInteractionOptionNoSubcommandGroup = 'CommandInteractionOptionNoSubcommandGroup', AutocompleteInteractionOptionNoFocusedOption = 'AutocompleteInteractionOptionNoFocusedOption', - ModalSubmitInteractionFieldNotFound = 'ModalSubmitInteractionFieldNotFound', - ModalSubmitInteractionFieldType = 'ModalSubmitInteractionFieldType', + ModalSubmitInteractionComponentNotFound = 'ModalSubmitInteractionComponentNotFound', + ModalSubmitInteractionComponentType = 'ModalSubmitInteractionComponentType', + ModalSubmitInteractionComponentEmpty = 'ModalSubmitInteractionComponentEmpty', + ModalSubmitInteractionComponentInvalidChannelType = 'ModalSubmitInteractionComponentInvalidChannelType', InvalidMissingScopes = 'InvalidMissingScopes', InvalidScopesWithPermissions = 'InvalidScopesWithPermissions', @@ -5548,15 +5621,19 @@ export interface CommandInteractionOption value?: boolean | number | string; } -export interface CommandInteractionResolvedData { - attachments?: ReadonlyCollection; +export interface BaseInteractionResolvedData { channels?: ReadonlyCollection>; members?: ReadonlyCollection>; - messages?: ReadonlyCollection>; roles?: ReadonlyCollection>; users?: ReadonlyCollection; } +export interface CommandInteractionResolvedData + extends BaseInteractionResolvedData { + attachments?: ReadonlyCollection; + messages?: ReadonlyCollection>; +} + export interface AutocompleteFocusedOption { focused: true; name: string; @@ -6691,11 +6768,11 @@ export interface BaseSelectMenuComponentData extends BaseComponentData { maxValues?: number; minValues?: number; placeholder?: string; + required?: boolean; } export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData { options: readonly SelectMenuComponentOptionData[]; - required?: boolean; type: ComponentType.StringSelect; } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 3f6b5dd90..d9621e31b 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -2609,6 +2609,31 @@ await chatInputInteraction.showModal({ }, label: 'yo', }, + { + type: ComponentType.Label, + component: { + type: ComponentType.UserSelect, + customId: 'user', + }, + label: 'aa', + }, + { + type: ComponentType.Label, + component: { + type: ComponentType.RoleSelect, + customId: 'role', + }, + label: 'bb', + }, + { + type: ComponentType.Label, + component: { + type: ComponentType.ChannelSelect, + customId: 'channel', + channelTypes: [ChannelType.GuildText, ChannelType.GuildVoice], + }, + label: 'cc', + }, ], });