mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
refactor: move full validation to ChatInputCommandBuilder (#11304)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
@@ -379,21 +379,9 @@ describe('ChatInput Commands', () => {
|
|||||||
describe('Subcommand builder and subcommand group builder', () => {
|
describe('Subcommand builder and subcommand group builder', () => {
|
||||||
test('GIVEN both types THEN does not throw error', () => {
|
test('GIVEN both types THEN does not throw error', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
getBuilder()
|
getNamedBuilder()
|
||||||
.setName('test')
|
.addSubcommands(getSubcommand())
|
||||||
.setDescription('Test command')
|
.addSubcommandGroups(getSubcommandGroup().addSubcommands(getSubcommand()))
|
||||||
.addSubcommands((subcommand) =>
|
|
||||||
subcommand.setName('subcommand').setDescription('Description of subcommand'),
|
|
||||||
)
|
|
||||||
.addSubcommandGroups((subcommandGroup) =>
|
|
||||||
subcommandGroup
|
|
||||||
.setName('group')
|
|
||||||
.setDescription('Description of group')
|
|
||||||
|
|
||||||
.addSubcommands((subcommand) =>
|
|
||||||
subcommand.setName('subcommand').setDescription('Description of group subcommand'),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
).not.toThrowError();
|
).not.toThrowError();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ApplicationIntegrationType,
|
ApplicationIntegrationType,
|
||||||
InteractionContextType,
|
InteractionContextType,
|
||||||
ApplicationCommandOptionType,
|
ApplicationCommandOptionType,
|
||||||
|
ApplicationCommandType,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
|
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
|
||||||
@@ -47,38 +48,55 @@ const choiceBasePredicate = z.object({
|
|||||||
name: choiceValueStringPredicate,
|
name: choiceValueStringPredicate,
|
||||||
name_localizations: localeMapPredicate.optional(),
|
name_localizations: localeMapPredicate.optional(),
|
||||||
});
|
});
|
||||||
const choiceStringPredicate = choiceBasePredicate.extend({
|
const choiceStringPredicate = z.object({
|
||||||
|
...choiceBasePredicate.shape,
|
||||||
value: choiceValueStringPredicate,
|
value: choiceValueStringPredicate,
|
||||||
});
|
});
|
||||||
const choiceNumberPredicate = choiceBasePredicate.extend({
|
const choiceNumberPredicate = z.object({
|
||||||
|
...choiceBasePredicate.shape,
|
||||||
value: choiceValueNumberPredicate,
|
value: choiceValueNumberPredicate,
|
||||||
});
|
});
|
||||||
|
|
||||||
const choiceBaseMixinPredicate = z.object({
|
const choiceBaseMixinPredicate = z.object({
|
||||||
autocomplete: z.literal(false).optional(),
|
autocomplete: z.literal(false).optional(),
|
||||||
});
|
});
|
||||||
const choiceStringMixinPredicate = choiceBaseMixinPredicate.extend({
|
const choiceStringMixinPredicate = z.object({
|
||||||
|
...choiceBaseMixinPredicate.shape,
|
||||||
choices: choiceStringPredicate.array().max(25).optional(),
|
choices: choiceStringPredicate.array().max(25).optional(),
|
||||||
});
|
});
|
||||||
const choiceNumberMixinPredicate = choiceBaseMixinPredicate.extend({
|
const choiceNumberMixinPredicate = z.object({
|
||||||
|
...choiceBaseMixinPredicate.shape,
|
||||||
choices: choiceNumberPredicate.array().max(25).optional(),
|
choices: choiceNumberPredicate.array().max(25).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const basicOptionTypesPredicate = z.literal([
|
export const baseBasicOptionPredicate = z.object({
|
||||||
ApplicationCommandOptionType.Attachment,
|
...sharedNameAndDescriptionPredicate.shape,
|
||||||
ApplicationCommandOptionType.Boolean,
|
|
||||||
ApplicationCommandOptionType.Channel,
|
|
||||||
ApplicationCommandOptionType.Integer,
|
|
||||||
ApplicationCommandOptionType.Mentionable,
|
|
||||||
ApplicationCommandOptionType.Number,
|
|
||||||
ApplicationCommandOptionType.Role,
|
|
||||||
ApplicationCommandOptionType.String,
|
|
||||||
ApplicationCommandOptionType.User,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const basicOptionPredicate = sharedNameAndDescriptionPredicate.extend({
|
|
||||||
required: z.boolean().optional(),
|
required: z.boolean().optional(),
|
||||||
type: basicOptionTypesPredicate,
|
});
|
||||||
|
|
||||||
|
export const attachmentOptionPredicate = z.object({
|
||||||
|
...baseBasicOptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Attachment),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const booleanOptionPredicate = z.object({
|
||||||
|
...baseBasicOptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Boolean),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mentionableOptionPredicate = z.object({
|
||||||
|
...baseBasicOptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Mentionable),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const roleOptionPredicate = z.object({
|
||||||
|
...baseBasicOptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Role),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userOptionPredicate = z.object({
|
||||||
|
...baseBasicOptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.User),
|
||||||
});
|
});
|
||||||
|
|
||||||
const autocompleteOrStringChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
|
const autocompleteOrStringChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
|
||||||
@@ -92,64 +110,71 @@ const autocompleteOrNumberChoicesMixinOptionPredicate = z.discriminatedUnion('au
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const channelOptionPredicate = z.object({
|
export const channelOptionPredicate = z.object({
|
||||||
...basicOptionPredicate.shape,
|
...baseBasicOptionPredicate.shape,
|
||||||
...channelMixinOptionPredicate.shape,
|
...channelMixinOptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Channel),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const integerOptionPredicate = z
|
export const integerOptionPredicate = z
|
||||||
.object({
|
.object({
|
||||||
...basicOptionPredicate.shape,
|
...baseBasicOptionPredicate.shape,
|
||||||
...numericMixinIntegerOptionPredicate.shape,
|
...numericMixinIntegerOptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Integer),
|
||||||
})
|
})
|
||||||
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
|
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
|
||||||
|
|
||||||
export const numberOptionPredicate = z
|
export const numberOptionPredicate = z
|
||||||
.object({
|
.object({
|
||||||
...basicOptionPredicate.shape,
|
...baseBasicOptionPredicate.shape,
|
||||||
...numericMixinNumberOptionPredicate.shape,
|
...numericMixinNumberOptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Number),
|
||||||
})
|
})
|
||||||
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
|
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
|
||||||
|
|
||||||
export const stringOptionPredicate = basicOptionPredicate
|
export const stringOptionPredicate = z
|
||||||
.extend({
|
.object({
|
||||||
|
...baseBasicOptionPredicate.shape,
|
||||||
max_length: z.number().min(1).max(6_000).optional(),
|
max_length: z.number().min(1).max(6_000).optional(),
|
||||||
min_length: z.number().min(0).max(6_000).optional(),
|
min_length: z.number().min(0).max(6_000).optional(),
|
||||||
|
type: z.literal(ApplicationCommandOptionType.String),
|
||||||
})
|
})
|
||||||
.and(autocompleteOrStringChoicesMixinOptionPredicate);
|
.and(autocompleteOrStringChoicesMixinOptionPredicate);
|
||||||
|
|
||||||
const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
|
const basicOptionPredicates = [
|
||||||
|
attachmentOptionPredicate,
|
||||||
|
booleanOptionPredicate,
|
||||||
|
channelOptionPredicate,
|
||||||
|
integerOptionPredicate,
|
||||||
|
mentionableOptionPredicate,
|
||||||
|
numberOptionPredicate,
|
||||||
|
roleOptionPredicate,
|
||||||
|
stringOptionPredicate,
|
||||||
|
userOptionPredicate,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const chatInputCommandSubcommandPredicate = z.object({
|
||||||
|
...sharedNameAndDescriptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Subcommand),
|
||||||
|
options: z.array(z.union(basicOptionPredicates)).max(25).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const chatInputCommandSubcommandGroupPredicate = z.object({
|
||||||
|
...sharedNameAndDescriptionPredicate.shape,
|
||||||
|
type: z.literal(ApplicationCommandOptionType.SubcommandGroup),
|
||||||
|
options: z.array(chatInputCommandSubcommandPredicate).min(1).max(25),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const chatInputCommandPredicate = z.object({
|
||||||
|
...sharedNameAndDescriptionPredicate.shape,
|
||||||
contexts: z.array(z.enum(InteractionContextType)).optional(),
|
contexts: z.array(z.enum(InteractionContextType)).optional(),
|
||||||
default_member_permissions: memberPermissionsPredicate.optional(),
|
default_member_permissions: memberPermissionsPredicate.optional(),
|
||||||
integration_types: z.array(z.enum(ApplicationIntegrationType)).optional(),
|
integration_types: z.array(z.enum(ApplicationIntegrationType)).optional(),
|
||||||
nsfw: z.boolean().optional(),
|
nsfw: z.boolean().optional(),
|
||||||
});
|
|
||||||
|
|
||||||
// Because you can only add options via builders, there's no need to validate whole objects here otherwise
|
|
||||||
const chatInputCommandOptionsPredicate = z.union([
|
|
||||||
z.object({ type: basicOptionTypesPredicate }).array(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
type: z.union([
|
|
||||||
z.literal(ApplicationCommandOptionType.Subcommand),
|
|
||||||
z.literal(ApplicationCommandOptionType.SubcommandGroup),
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
.array(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({
|
|
||||||
options: chatInputCommandOptionsPredicate.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({
|
|
||||||
type: z.literal(ApplicationCommandOptionType.SubcommandGroup),
|
|
||||||
options: z
|
options: z
|
||||||
.array(z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }))
|
.union([
|
||||||
.min(1)
|
z.array(z.union(basicOptionPredicates)).max(25),
|
||||||
.max(25),
|
z.array(z.union([chatInputCommandSubcommandPredicate, chatInputCommandSubcommandGroupPredicate])).max(25),
|
||||||
});
|
])
|
||||||
|
.optional(),
|
||||||
export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({
|
type: z.literal(ApplicationCommandType.ChatInput).optional(),
|
||||||
type: z.literal(ApplicationCommandOptionType.Subcommand),
|
|
||||||
options: z.array(z.object({ type: basicOptionTypesPredicate })).max(25),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class ChatInputCommandBuilder extends Mixin(
|
|||||||
const data: RESTPostAPIChatInputApplicationCommandsJSONBody = {
|
const data: RESTPostAPIChatInputApplicationCommandsJSONBody = {
|
||||||
...structuredClone(rest as Omit<RESTPostAPIChatInputApplicationCommandsJSONBody, 'options'>),
|
...structuredClone(rest as Omit<RESTPostAPIChatInputApplicationCommandsJSONBody, 'options'>),
|
||||||
type: ApplicationCommandType.ChatInput,
|
type: ApplicationCommandType.ChatInput,
|
||||||
options: options?.map((option) => option.toJSON(validationOverride)),
|
options: options?.map((option) => option.toJSON(false)),
|
||||||
};
|
};
|
||||||
|
|
||||||
validate(chatInputCommandPredicate, data, validationOverride);
|
validate(chatInputCommandPredicate, data, validationOverride);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import type { z } from 'zod';
|
|||||||
import { validate } from '../../../../util/validation.js';
|
import { validate } from '../../../../util/validation.js';
|
||||||
import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js';
|
import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js';
|
||||||
import { SharedNameAndDescription } from '../../SharedNameAndDescription.js';
|
import { SharedNameAndDescription } from '../../SharedNameAndDescription.js';
|
||||||
import { basicOptionPredicate } from '../Assertions.js';
|
|
||||||
|
|
||||||
export interface ApplicationCommandOptionBaseData extends Partial<Pick<APIApplicationCommandOption, 'required'>> {
|
export interface ApplicationCommandOptionBaseData extends Partial<Pick<APIApplicationCommandOption, 'required'>> {
|
||||||
type: ApplicationCommandOptionType;
|
type: ApplicationCommandOptionType;
|
||||||
@@ -24,7 +23,7 @@ export abstract class ApplicationCommandOptionBase
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
protected static readonly predicate: z.ZodType = basicOptionPredicate;
|
protected static readonly predicate: z.ZodType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { attachmentOptionPredicate } from '../Assertions.js';
|
||||||
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chat input command attachment option.
|
* A chat input command attachment option.
|
||||||
*/
|
*/
|
||||||
export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBase {
|
export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBase {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected static override readonly predicate = attachmentOptionPredicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new attachment option.
|
* Creates a new attachment option.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { booleanOptionPredicate } from '../Assertions.js';
|
||||||
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chat input command boolean option.
|
* A chat input command boolean option.
|
||||||
*/
|
*/
|
||||||
export class ChatInputCommandBooleanOption extends ApplicationCommandOptionBase {
|
export class ChatInputCommandBooleanOption extends ApplicationCommandOptionBase {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected static override readonly predicate = booleanOptionPredicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new boolean option.
|
* Creates a new boolean option.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { mentionableOptionPredicate } from '../Assertions.js';
|
||||||
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chat input command mentionable option.
|
* A chat input command mentionable option.
|
||||||
*/
|
*/
|
||||||
export class ChatInputCommandMentionableOption extends ApplicationCommandOptionBase {
|
export class ChatInputCommandMentionableOption extends ApplicationCommandOptionBase {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected static override readonly predicate = mentionableOptionPredicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new mentionable option.
|
* Creates a new mentionable option.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { roleOptionPredicate } from '../Assertions.js';
|
||||||
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chat input command role option.
|
* A chat input command role option.
|
||||||
*/
|
*/
|
||||||
export class ChatInputCommandRoleOption extends ApplicationCommandOptionBase {
|
export class ChatInputCommandRoleOption extends ApplicationCommandOptionBase {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected static override readonly predicate = roleOptionPredicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new role option.
|
* Creates a new role option.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { userOptionPredicate } from '../Assertions.js';
|
||||||
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chat input command user option.
|
* A chat input command user option.
|
||||||
*/
|
*/
|
||||||
export class ChatInputCommandUserOption extends ApplicationCommandOptionBase {
|
export class ChatInputCommandUserOption extends ApplicationCommandOptionBase {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
protected static override readonly predicate = userOptionPredicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new user option.
|
* Creates a new user option.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user