From 429dbccc85cabd9986b2e8bf443bf384e4ddc61a Mon Sep 17 00:00:00 2001 From: Almeida Date: Tue, 20 Dec 2022 20:32:45 +0000 Subject: [PATCH] feat(CommandInteractionOptionResolver): add `channelTypes` option to `getChannel` (#8934) * feat(CommandInteractionOptionResolver): add `channelTypes` option to `getChannel` * fix: thread types Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/discord.js/src/errors/ErrorCodes.js | 2 ++ packages/discord.js/src/errors/Messages.js | 2 ++ .../CommandInteractionOptionResolver.js | 16 ++++++++-- packages/discord.js/typings/index.d.ts | 32 +++++++++++++++++-- packages/discord.js/typings/index.test-d.ts | 18 +++++++++++ 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/discord.js/src/errors/ErrorCodes.js b/packages/discord.js/src/errors/ErrorCodes.js index b5faf0c1c..3f074a1da 100644 --- a/packages/discord.js/src/errors/ErrorCodes.js +++ b/packages/discord.js/src/errors/ErrorCodes.js @@ -135,6 +135,7 @@ * @property {'CommandInteractionOptionEmpty'} CommandInteractionOptionEmpty * @property {'CommandInteractionOptionNoSubcommand'} CommandInteractionOptionNoSubcommand * @property {'CommandInteractionOptionNoSubcommandGroup'} CommandInteractionOptionNoSubcommandGroup + * @property {'CommandInteractionOptionInvalidChannelType'} CommandInteractionOptionInvalidChannelType * @property {'AutocompleteInteractionOptionNoFocusedOption'} AutocompleteInteractionOptionNoFocusedOption * @property {'ModalSubmitInteractionFieldNotFound'} ModalSubmitInteractionFieldNotFound @@ -281,6 +282,7 @@ const keys = [ 'CommandInteractionOptionEmpty', 'CommandInteractionOptionNoSubcommand', 'CommandInteractionOptionNoSubcommandGroup', + 'CommandInteractionOptionInvalidChannelType', 'AutocompleteInteractionOptionNoFocusedOption', 'ModalSubmitInteractionFieldNotFound', diff --git a/packages/discord.js/src/errors/Messages.js b/packages/discord.js/src/errors/Messages.js index 24437b502..1b79ec030 100644 --- a/packages/discord.js/src/errors/Messages.js +++ b/packages/discord.js/src/errors/Messages.js @@ -145,6 +145,8 @@ const Messages = { `Required option "${name}" is of type: ${type}; expected a non-empty value.`, [DjsErrorCodes.CommandInteractionOptionNoSubcommand]: 'No subcommand specified for interaction.', [DjsErrorCodes.CommandInteractionOptionNoSubcommandGroup]: 'No subcommand group specified for interaction.', + [DjsErrorCodes.CommandInteractionOptionInvalidChannelType]: (name, type, expected) => + `The type of channel of the option "${name}" is: ${type}; expected ${expected}.`, [DjsErrorCodes.AutocompleteInteractionOptionNoFocusedOption]: 'No focused option for autocomplete interaction.', [DjsErrorCodes.ModalSubmitInteractionFieldNotFound]: customId => diff --git a/packages/discord.js/src/structures/CommandInteractionOptionResolver.js b/packages/discord.js/src/structures/CommandInteractionOptionResolver.js index 23ff96264..1e85412e2 100644 --- a/packages/discord.js/src/structures/CommandInteractionOptionResolver.js +++ b/packages/discord.js/src/structures/CommandInteractionOptionResolver.js @@ -142,12 +142,24 @@ class CommandInteractionOptionResolver { * 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. + * @param {ChannelType[]} [channelTypes=[]] The allowed types of channels. If empty, all channel types are allowed. * @returns {?(GuildChannel|ThreadChannel|APIChannel)} * The value of the option, or null if not set and not required. */ - getChannel(name, required = false) { + getChannel(name, required = false, channelTypes = []) { const option = this._getTypedOption(name, [ApplicationCommandOptionType.Channel], ['channel'], required); - return option?.channel ?? null; + const channel = option?.channel ?? null; + + if (channel && channelTypes.length > 0 && !channelTypes.includes(channel.type)) { + throw new DiscordjsTypeError( + ErrorCodes.CommandInteractionOptionInvalidChannelType, + name, + channel.type, + channelTypes.join(', '), + ); + } + + return channel; } /** diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 6f758eecb..6860e4228 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -1113,8 +1113,36 @@ export class CommandInteractionOptionResolver['channel']>; - public getChannel(name: string, required?: boolean): NonNullable['channel']> | null; + public getChannel( + name: string, + required: true, + channelTypes?: T[], + ): Extract< + NonNullable['channel']>, + { + // The `type` property of the PublicThreadChannel class is typed as `ChannelType.PublicThread | ChannelType.AnnouncementThread` + // If the user only passed one of those channel types, the Extract<> would have resolved to `never` + // Hence the need for this ternary + type: T extends ChannelType.PublicThread | ChannelType.AnnouncementThread + ? ChannelType.PublicThread | ChannelType.AnnouncementThread + : T; + } + >; + public getChannel( + name: string, + required?: boolean, + channelTypes?: T[], + ): Extract< + NonNullable['channel']>, + { + // The `type` property of the PublicThreadChannel class is typed as `ChannelType.PublicThread | ChannelType.AnnouncementThread` + // If the user only passed one of those channel types, the Extract<> would have resolved to `never` + // Hence the need for this ternary + type: T extends ChannelType.PublicThread | ChannelType.AnnouncementThread + ? ChannelType.PublicThread | ChannelType.AnnouncementThread + : T; + } + > | null; public getString(name: string, required: true): string; public getString(name: string, required?: boolean): string | null; public getInteger(name: string, required: true): number; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index bbde22286..905f4bbff 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -152,6 +152,8 @@ import { AutoModerationActionExecution, AutoModerationRule, AutoModerationRuleManager, + PrivateThreadChannel, + PublicThreadChannel, } from '.'; import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; @@ -1744,6 +1746,22 @@ client.on('interactionCreate', async interaction => { expectType(interaction.options.getChannel('test', true)); expectType(interaction.options.getRole('test', true)); + + expectType(interaction.options.getChannel('test', true, [ChannelType.PublicThread])); + expectType(interaction.options.getChannel('test', true, [ChannelType.AnnouncementThread])); + expectType( + interaction.options.getChannel('test', true, [ChannelType.PublicThread, ChannelType.AnnouncementThread]), + ); + expectType(interaction.options.getChannel('test', true, [ChannelType.PrivateThread])); + + expectType(interaction.options.getChannel('test', true, [ChannelType.GuildText])); + expectType(interaction.options.getChannel('test', false, [ChannelType.GuildText])); + expectType( + interaction.options.getChannel('test', true, [ChannelType.GuildForum, ChannelType.GuildVoice]), + ); + expectType( + interaction.options.getChannel('test', false, [ChannelType.GuildForum, ChannelType.GuildVoice]), + ); } else { // @ts-expect-error consumeCachedCommand(interaction);