diff --git a/src/api/handlers/webhook.ts b/src/api/handlers/webhook.ts index 15f9039d1..1d2b56d8e 100644 --- a/src/api/handlers/webhook.ts +++ b/src/api/handlers/webhook.ts @@ -9,6 +9,9 @@ import { ExecuteWebhookOptions, MessageCreateOptions, SlashCommand, + SlashCommandOption, + SlashCommandOptionChoice, + SlashCommandOptionType, SlashCommandResponseOptions, UpsertSlashCommandOptions, UpsertSlashCommandsOptions, @@ -270,6 +273,82 @@ export async function deleteWebhookMessage( return result; } +function validateSlashOptionChoices( + choices: SlashCommandOptionChoice[], + optionType: SlashCommandOptionType, +) { + for (const choice of choices) { + if ([...choice.name].length < 1 || [...choice.name].length > 100) { + throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); + } + + if ( + (optionType === SlashCommandOptionType.STRING && + (typeof choice.value !== "string" || choice.value.length < 1 || + choice.value.length > 100)) || + (optionType === SlashCommandOptionType.INTEGER && + typeof choice.value !== "number") + ) { + throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); + } + } +} + +function validateSlashOptions(options: SlashCommandOption[]) { + for (const option of options) { + if ( + (option.choices?.length && option.choices.length > 25) || + option.type !== SlashCommandOptionType.STRING && + option.type !== SlashCommandOptionType.INTEGER + ) { + throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); + } + + if ( + ([...option.name].length < 1 || [...option.name].length > 32) || + ([...option.description].length < 1 || + [...option.description].length > 100) + ) { + throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); + } + + if (option.choices) { + validateSlashOptionChoices(option.choices, option.type); + } + } +} + +function validateSlashCommands( + commands: UpsertSlashCommandOptions[], + create = false, +) { + for (const command of commands) { + if ( + (command.name && !SLASH_COMMANDS_NAME_REGEX.test(command.name)) || + (create && !command.name) + ) { + throw new Error(Errors.INVALID_SLASH_NAME); + } + + if ( + (command.description && + ([...command.description].length < 1 || + [...command.description].length > 100)) || + (create && !command.description) + ) { + throw new Error(Errors.INVALID_SLASH_DESCRIPTION); + } + + if (command.options?.length) { + if (command.options.length > 25) { + throw new Error(Errors.INVALID_SLASH_OPTIONS); + } + + validateSlashOptions(command.options); + } + } +} + /** * There are two kinds of Slash Commands: global commands and guild commands. Global commands are available for every guild that adds your app; guild commands are specific to the guild you specify when making them. Command names are unique per application within each scope (global and guild). That means: * @@ -282,15 +361,7 @@ export async function deleteWebhookMessage( * Guild commands update **instantly**. We recommend you use guild commands for quick testing, and global commands when they're ready for public use. */ export async function createSlashCommand(options: CreateSlashCommandOptions) { - if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) { - throw new Error(Errors.INVALID_SLASH_NAME); - } - - if ( - [...options.description].length < 1 || [...options.description].length > 100 - ) { - throw new Error(Errors.INVALID_SLASH_DESCRIPTION); - } + validateSlashCommands([options], true); const result = await RequestManager.post( options.guildID @@ -335,17 +406,7 @@ export async function upsertSlashCommand( options: UpsertSlashCommandOptions, guildID?: string, ) { - if (options.name && !SLASH_COMMANDS_NAME_REGEX.test(options.name)) { - throw new Error(Errors.INVALID_SLASH_NAME); - } - - if ( - options.description && - ([...options.description].length < 1 || - [...options.description].length > 100) - ) { - throw new Error(Errors.INVALID_SLASH_DESCRIPTION); - } + validateSlashCommands([options]); const result = await RequestManager.patch( guildID @@ -370,27 +431,13 @@ export async function upsertSlashCommands( options: UpsertSlashCommandsOptions[], guildID?: string, ) { - const data = options.map((option) => { - if (option.name && !SLASH_COMMANDS_NAME_REGEX.test(option.name)) { - throw new Error(Errors.INVALID_SLASH_NAME); - } - - if ( - option.description && - ([...option.description].length < 1 || - [...option.description].length > 100) - ) { - throw new Error(Errors.INVALID_SLASH_DESCRIPTION); - } - - return option; - }); + validateSlashCommands(options); const result = await RequestManager.put( guildID ? endpoints.COMMANDS_GUILD(applicationID, guildID) : endpoints.COMMANDS(applicationID), - data, + options, ); return result; diff --git a/src/types/errors.ts b/src/types/errors.ts index cc64e5b3c..f3895022a 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -20,6 +20,8 @@ export enum Errors { // Interaction Errors INVALID_SLASH_DESCRIPTION = "INVALID_SLASH_DESCRIPTION", INVALID_SLASH_NAME = "INVALID_SLASH_NAME", + INVALID_SLASH_OPTIONS = "INVALID_SLASH_OPTIONS", + INVALID_SLASH_OPTIONS_CHOICES = "INVALID_SLASH_OPTIONS_CHOICES", // Webhook Errors INVALID_WEBHOOK_NAME = "INVALID_WEBHOOK_NAME", INVALID_WEBHOOK_OPTIONS = "INVALID_WEBHOOK_OPTIONS",