mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-17 12:03:31 +01:00
refactor: builders (#10448)
BREAKING CHANGE: formatters export removed (prev. deprecated) BREAKING CHANGE: `SelectMenuBuilder` and `SelectMenuOptionBuilder` have been removed (prev. deprecated) BREAKING CHANGE: `EmbedBuilder` no longer takes camalCase options BREAKING CHANGE: `ActionRowBuilder` now has specialized `[add/set]X` methods as opposed to the current `[add/set]Components` BREAKING CHANGE: Removed `equals` methods BREAKING CHANGE: Sapphire -> zod for validation BREAKING CHANGE: Removed the ability to pass `null`/`undefined` to clear fields, use `clearX()` instead BREAKING CHANGE: Renamed all "slash command" symbols to instead use "chat input command" BREAKING CHANGE: Removed `ContextMenuCommandBuilder` in favor of `MessageCommandBuilder` and `UserCommandBuilder` BREAKING CHANGE: Removed support for passing the "string key"s of enums BREAKING CHANGE: Removed `Button` class in favor for specialized classes depending on the style BREAKING CHANGE: Removed nested `addX` styled-methods in favor of plural `addXs` Co-authored-by: Vlad Frangu <me@vladfrangu.dev> Co-authored-by: Almeida <github@almeidx.dev>
This commit is contained in:
@@ -1,127 +1,168 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../util/validation.js';
|
||||
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';
|
||||
import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { customIdPredicate, refineURLPredicate } from '../Assertions.js';
|
||||
|
||||
export const customIdValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(100)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
const labelPredicate = z.string().min(1).max(80);
|
||||
|
||||
export const emojiValidator = s
|
||||
export const emojiPredicate = z
|
||||
.object({
|
||||
id: s.string(),
|
||||
name: s.string(),
|
||||
animated: s.boolean(),
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(2).max(32).optional(),
|
||||
animated: z.boolean().optional(),
|
||||
})
|
||||
.partial()
|
||||
.strict()
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
.refine((data) => data.id !== undefined || data.name !== undefined, {
|
||||
message: "Either 'id' or 'name' must be provided",
|
||||
});
|
||||
|
||||
export const disabledValidator = s.boolean();
|
||||
const buttonPredicateBase = z.object({
|
||||
type: z.literal(ComponentType.Button),
|
||||
disabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const buttonLabelValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(80)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
const buttonCustomIdPredicateBase = buttonPredicateBase.extend({
|
||||
custom_id: customIdPredicate,
|
||||
emoji: emojiPredicate.optional(),
|
||||
label: labelPredicate,
|
||||
});
|
||||
|
||||
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
|
||||
const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) }).strict();
|
||||
const buttonSecondaryPredicate = buttonCustomIdPredicateBase
|
||||
.extend({ style: z.literal(ButtonStyle.Secondary) })
|
||||
.strict();
|
||||
const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) }).strict();
|
||||
const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) }).strict();
|
||||
|
||||
export const placeholderValidator = s.string().lengthLessThanOrEqual(150).setValidationEnabled(isValidationEnabled);
|
||||
export const minMaxValidator = s
|
||||
.number()
|
||||
.int()
|
||||
.greaterThanOrEqual(0)
|
||||
.lessThanOrEqual(25)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const labelValueDescriptionValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(100)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const jsonOptionValidator = s
|
||||
.object({
|
||||
label: labelValueDescriptionValidator,
|
||||
value: labelValueDescriptionValidator,
|
||||
description: labelValueDescriptionValidator.optional(),
|
||||
emoji: emojiValidator.optional(),
|
||||
default: s.boolean().optional(),
|
||||
const buttonLinkPredicate = buttonPredicateBase
|
||||
.extend({
|
||||
style: z.literal(ButtonStyle.Link),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.refine(refineURLPredicate(['http:', 'https:', 'discord:'])),
|
||||
emoji: emojiPredicate.optional(),
|
||||
label: labelPredicate,
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
.strict();
|
||||
|
||||
export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const optionsValidator = optionValidator
|
||||
.array()
|
||||
.lengthGreaterThanOrEqual(0)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const optionsLengthValidator = s
|
||||
.number()
|
||||
.int()
|
||||
.greaterThanOrEqual(0)
|
||||
.lessThanOrEqual(25)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
optionsValidator.parse(options);
|
||||
}
|
||||
|
||||
export const defaultValidator = s.boolean();
|
||||
|
||||
export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {
|
||||
labelValueDescriptionValidator.parse(label);
|
||||
labelValueDescriptionValidator.parse(value);
|
||||
}
|
||||
|
||||
export const channelTypesValidator = s.nativeEnum(ChannelType).array().setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const urlValidator = s
|
||||
.string()
|
||||
.url({
|
||||
allowedProtocols: ['http:', 'https:', 'discord:'],
|
||||
const buttonPremiumPredicate = buttonPredicateBase
|
||||
.extend({
|
||||
style: z.literal(ButtonStyle.Premium),
|
||||
sku_id: z.string(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
.strict();
|
||||
|
||||
export function validateRequiredButtonParameters(
|
||||
style?: ButtonStyle,
|
||||
label?: string,
|
||||
emoji?: APIMessageComponentEmoji,
|
||||
customId?: string,
|
||||
skuId?: string,
|
||||
url?: string,
|
||||
) {
|
||||
if (style === ButtonStyle.Premium) {
|
||||
if (!skuId) {
|
||||
throw new RangeError('Premium buttons must have an SKU id.');
|
||||
export const buttonPredicate = z.discriminatedUnion('style', [
|
||||
buttonLinkPredicate,
|
||||
buttonPrimaryPredicate,
|
||||
buttonSecondaryPredicate,
|
||||
buttonSuccessPredicate,
|
||||
buttonDangerPredicate,
|
||||
buttonPremiumPredicate,
|
||||
]);
|
||||
|
||||
const selectMenuBasePredicate = z.object({
|
||||
placeholder: z.string().max(150).optional(),
|
||||
min_values: z.number().min(0).max(25).optional(),
|
||||
max_values: z.number().min(0).max(25).optional(),
|
||||
custom_id: customIdPredicate,
|
||||
disabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.ChannelSelect),
|
||||
channel_types: z.nativeEnum(ChannelType).array().optional(),
|
||||
default_values: z
|
||||
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) })
|
||||
.array()
|
||||
.max(25)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.MentionableSelect),
|
||||
default_values: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
type: z.union([z.literal(SelectMenuDefaultValueType.Role), z.literal(SelectMenuDefaultValueType.User)]),
|
||||
})
|
||||
.array()
|
||||
.max(25)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const selectMenuRolePredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.RoleSelect),
|
||||
default_values: z
|
||||
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Role) })
|
||||
.array()
|
||||
.max(25)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const selectMenuStringOptionPredicate = z.object({
|
||||
label: labelPredicate,
|
||||
value: z.string().min(1).max(100),
|
||||
description: z.string().min(1).max(100).optional(),
|
||||
emoji: emojiPredicate.optional(),
|
||||
default: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const selectMenuStringPredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: z.literal(ComponentType.StringSelect),
|
||||
options: selectMenuStringOptionPredicate.array().min(1).max(25),
|
||||
})
|
||||
.superRefine((menu, ctx) => {
|
||||
const addIssue = (name: string, minimum: number) =>
|
||||
ctx.addIssue({
|
||||
code: 'too_small',
|
||||
message: `The number of options must be greater than or equal to ${name}`,
|
||||
inclusive: true,
|
||||
minimum,
|
||||
type: 'number',
|
||||
path: ['options'],
|
||||
});
|
||||
|
||||
if (menu.max_values !== undefined && menu.options.length < menu.max_values) {
|
||||
addIssue('max_values', menu.max_values);
|
||||
}
|
||||
|
||||
if (customId || label || url || emoji) {
|
||||
throw new RangeError('Premium buttons cannot have a custom id, label, URL, or emoji.');
|
||||
}
|
||||
} else {
|
||||
if (skuId) {
|
||||
throw new RangeError('Non-premium buttons must not have an SKU id.');
|
||||
if (menu.min_values !== undefined && menu.options.length < menu.min_values) {
|
||||
addIssue('min_values', menu.min_values);
|
||||
}
|
||||
});
|
||||
|
||||
if (url && customId) {
|
||||
throw new RangeError('URL and custom id are mutually exclusive.');
|
||||
}
|
||||
export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.UserSelect),
|
||||
default_values: z
|
||||
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.User) })
|
||||
.array()
|
||||
.max(25)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
if (!label && !emoji) {
|
||||
throw new RangeError('Non-premium buttons must have a label and/or an emoji.');
|
||||
}
|
||||
|
||||
if (style === ButtonStyle.Link) {
|
||||
if (!url) {
|
||||
throw new RangeError('Link buttons must have a URL.');
|
||||
}
|
||||
} else if (url) {
|
||||
throw new RangeError('Non-premium and non-link buttons cannot have a URL.');
|
||||
}
|
||||
}
|
||||
}
|
||||
export const actionRowPredicate = z.object({
|
||||
type: z.literal(ComponentType.ActionRow),
|
||||
components: z.union([
|
||||
z
|
||||
.object({ type: z.literal(ComponentType.Button) })
|
||||
.array()
|
||||
.min(1)
|
||||
.max(5),
|
||||
z
|
||||
.object({
|
||||
type: z.union([
|
||||
z.literal(ComponentType.ChannelSelect),
|
||||
z.literal(ComponentType.MentionableSelect),
|
||||
z.literal(ComponentType.RoleSelect),
|
||||
z.literal(ComponentType.StringSelect),
|
||||
z.literal(ComponentType.UserSelect),
|
||||
// And this!
|
||||
z.literal(ComponentType.TextInput),
|
||||
]),
|
||||
})
|
||||
.array()
|
||||
.length(1),
|
||||
]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user