diff --git a/src/errors/Messages.js b/src/errors/Messages.js index f082ceaed1fe..404658a5505f 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -133,6 +133,14 @@ const Messages = { INTERACTION_EPHEMERAL_REPLIED: 'Ephemeral responses cannot be fetched or deleted.', INTERACTION_FETCH_EPHEMERAL: 'Ephemeral responses cannot be fetched.', + COMMAND_INTERACTION_OPTION_NOT_FOUND: name => `Required option "${name}" not found.`, + COMMAND_INTERACTION_OPTION_TYPE: (name, type, expected) => + `Option "${name}" is of type: ${type}; expected one of: ${expected.join(', ')}`, + COMMAND_INTERACTION_OPTION_EMPTY: (name, type) => + `Required option "${name}" is of type: ${type}; expected a non-empty value.`, + COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND: 'No sub-command specified for interaction.', + COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No sub-command group specified for interaction.', + 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/CommandInteraction.js b/src/structures/CommandInteraction.js index f2504a2959b8..e8d59f3d5243 100644 --- a/src/structures/CommandInteraction.js +++ b/src/structures/CommandInteraction.js @@ -1,9 +1,9 @@ 'use strict'; +const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver'); const Interaction = require('./Interaction'); const InteractionWebhook = require('./InteractionWebhook'); const InteractionResponses = require('./interfaces/InteractionResponses'); -const Collection = require('../util/Collection'); const { ApplicationCommandOptionTypes } = require('../util/Constants'); /** @@ -48,9 +48,12 @@ class CommandInteraction extends Interaction { /** * The options passed to the command. - * @type {Collection} + * @type {CommandInteractionOptionResolver} */ - this.options = this._createOptionsCollection(data.data.options, data.data.resolved); + this.options = new CommandInteractionOptionResolver( + this.client, + data.data.options?.map(option => this.transformOption(option, data.data.resolved)), + ); /** * Whether this interaction has already been replied to @@ -108,7 +111,7 @@ class CommandInteraction extends Interaction { }; if ('value' in option) result.value = option.value; - if ('options' in option) result.options = this._createOptionsCollection(option.options, resolved); + if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt, resolved)); if (resolved) { const user = resolved.users?.[option.value]; @@ -127,22 +130,6 @@ class CommandInteraction extends Interaction { return result; } - /** - * Creates a collection of options from the received options array. - * @param {APIApplicationCommandOption[]} options The received options - * @param {APIApplicationCommandOptionResolved} resolved The resolved interaction data - * @returns {Collection} - * @private - */ - _createOptionsCollection(options, resolved) { - const optionsCollection = new Collection(); - if (typeof options === 'undefined') return optionsCollection; - for (const option of options) { - optionsCollection.set(option.name, this.transformOption(option, resolved)); - } - return optionsCollection; - } - // These are here only for documentation purposes - they are implemented by InteractionResponses /* eslint-disable no-empty-function */ defer() {} diff --git a/src/structures/CommandInteractionOptionResolver.js b/src/structures/CommandInteractionOptionResolver.js new file mode 100644 index 000000000000..f8bc6ee29cf3 --- /dev/null +++ b/src/structures/CommandInteractionOptionResolver.js @@ -0,0 +1,200 @@ +'use strict'; + +const { TypeError } = require('../errors'); + +/** + * A resolver for command interaction options. + */ +class CommandInteractionOptionResolver { + constructor(client, options) { + /** + * The client that instantiated this. + * @name CommandInteractionOptionResolver#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The interaction options array. + * @type {CommandInteractionOption[]} + * @private + */ + this._options = options ?? []; + + /** + * The name of the sub-command group. + * @type {?string} + * @private + */ + this._group = null; + + /** + * The name of the sub-command. + * @type {?string} + * @private + */ + this._subCommand = null; + if (this._options[0]?.type === 'SUB_COMMAND_GROUP') { + this._group = this._options[0].name; + this._options = this._options[0].options ?? []; + } + if (this._options[0]?.type === 'SUB_COMMAND') { + this._subCommand = this._options[0].name; + this._options = this._options[0].options ?? []; + } + } + + /** + * Gets an option by its name. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?CommandInteractionOption} The option, if found. + */ + get(name, required = false) { + const option = this._options.find(opt => opt.name === name); + if (!option) { + if (required) { + throw new TypeError('COMMAND_INTERACTION_OPTION_NOT_FOUND', name); + } + return null; + } + return option; + } + + /** + * Gets an option by name and property and checks its type. + * @param {string} name The name of the option. + * @param {ApplicationCommandOptionType[]} types The type of the option. + * @param {string[]} properties The properties to check for for `required`. + * @param {boolean} required Whether to throw an error if the option is not found. + * @returns {?CommandInteractionOption} The option, if found. + * @private + */ + _getTypedOption(name, types, properties, required) { + const option = this.get(name, required); + if (!option) { + return null; + } else if (!types.includes(option.type)) { + throw new TypeError('COMMAND_INTERACTION_OPTION_TYPE', name, option.type, types); + } else if (required && properties.every(prop => option[prop] === null || typeof option[prop] === 'undefined')) { + throw new TypeError('COMMAND_INTERACTION_OPTION_EMPTY', name, option.type); + } + return option; + } + + /** + * Gets the selected sub-command. + * @returns {string} The name of the selected sub-command. + */ + getSubCommand() { + if (!this._subCommand) { + throw new TypeError('COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND'); + } + return this._subCommand; + } + + /** + * Gets the selected sub-command group. + * @returns {string} The name of the selected sub-command group. + */ + getSubCommandGroup() { + if (!this._group) { + throw new TypeError('COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP'); + } + return this._group; + } + + /** + * Gets a boolean option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?boolean} The value of the option, or null if not set and not required. + */ + getBoolean(name, required = false) { + const option = this._getTypedOption(name, ['BOOLEAN'], ['value'], required); + return option?.value ?? null; + } + + /** + * Gets a channel option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?(GuildChannel|APIInteractionDataResolvedChannel)} + * The value of the option, or null if not set and not required. + */ + getChannel(name, required = false) { + const option = this._getTypedOption(name, ['CHANNEL'], ['channel'], required); + return option?.channel ?? null; + } + + /** + * Gets a string option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?string} The value of the option, or null if not set and not required. + */ + getString(name, required = false) { + const option = this._getTypedOption(name, ['STRING'], ['value'], required); + return option?.value ?? null; + } + + /** + * Gets an integer option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?number} The value of the option, or null if not set and not required. + */ + getInteger(name, required = false) { + const option = this._getTypedOption(name, ['INTEGER'], ['value'], required); + return option?.value ?? null; + } + + /** + * Gets a user option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?User} The value of the option, or null if not set and not required. + */ + getUser(name, required = false) { + const option = this._getTypedOption(name, ['USER'], ['user'], required); + return option?.user ?? null; + } + + /** + * Gets a member option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?(GuildMember|APIInteractionDataResolvedGuildMember)} + * The value of the option, or null if not set and not required. + */ + getMember(name, required = false) { + const option = this._getTypedOption(name, ['MEMBER'], ['member'], required); + return option?.member ?? null; + } + + /** + * Gets a role option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?(Role|APIRole)} The value of the option, or null if not set and not required. + */ + getRole(name, required = false) { + const option = this._getTypedOption(name, ['ROLE'], ['role'], required); + return option?.role ?? null; + } + + /** + * Gets a mentionable option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?(User|GuildMember|APIInteractionDataResolvedGuildMember|Role|APIRole)} + * The value of the option, or null if not set and not required. + */ + getMentionable(name, required = false) { + const option = this._getTypedOption(name, ['MENTIONABLE'], ['user', 'member', 'role'], required); + return option?.member ?? option?.user ?? option?.role ?? null; + } +} + +module.exports = CommandInteractionOptionResolver; diff --git a/typings/index.d.ts b/typings/index.d.ts index 4f78a786d27a..4f9f2449d04b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -416,7 +416,7 @@ export class CommandInteraction extends Interaction { public commandName: string; public deferred: boolean; public ephemeral: boolean | null; - public options: Collection; + public options: CommandInteractionOptionResolver; public replied: boolean; public webhook: InteractionWebhook; public defer(options?: InteractionDeferOptions & { fetchReply: true }): Promise; @@ -431,6 +431,53 @@ export class CommandInteraction extends Interaction { private _createOptionsCollection(options: unknown, resolved: unknown): Collection; } +export class CommandInteractionOptionResolver { + public constructor(client: Client, options: CommandInteractionOption[]); + public readonly client: Client; + private _options: CommandInteractionOption[]; + private _group: string | null; + private _subCommand: string | null; + private _getTypedOption( + name: string, + types: ApplicationCommandOptionType[], + properties: (keyof ApplicationCommandOption)[], + required: true, + ): CommandInteractionOption; + private _getTypedOption( + name: string, + types: ApplicationCommandOptionType[], + properties: (keyof ApplicationCommandOption)[], + required: boolean, + ): CommandInteractionOption | null; + + public get(name: string, required: true): CommandInteractionOption; + public get(name: string, required?: boolean): CommandInteractionOption | null; + public getSubCommand(): string; + public getSubCommandGroup(): string; + public getBoolean(name: string, required: true): boolean; + public getBoolean(name: string, required?: boolean): boolean | null; + public getChannel(name: string, required: true): NonNullable; + public getChannel(name: string, required?: boolean): NonNullable | null; + public getString(name: string, required: true): string; + public getString(name: string, required?: boolean): string | null; + public getInteger(name: string, required: true): number; + public getInteger(name: string, required?: boolean): number | null; + public getUser(name: string, required: true): NonNullable; + public getUser(name: string, required?: boolean): NonNullable | null; + public getMember(name: string, required: true): NonNullable; + public getMember(name: string, required?: boolean): NonNullable | null; + public getRole(name: string, required: true): NonNullable; + public getRole(name: string, required?: boolean): NonNullable | null; + public getMentionable( + name: string, + required: true, + ): NonNullable; + public getMentionable( + name: string, + required?: boolean, + ): NonNullable | null; +} + export class DataResolver extends null { private constructor(); public static resolveBase64(data: Base64Resolvable): string;