mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-14 10:33:30 +01:00
* fix(builders): add proper snowflake validation close #11289 * fix(builders): use snowflake validation for attachment id * test(builders): add validation tests for snowflake attachment IDs * fix: better regex Co-authored-by: Almeida <github@almeidx.dev> * test(builders): fix snowflake validation in fileBody test * Update packages/builders/src/Assertions.ts Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * fix: update regex --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: Almeida <github@almeidx.dev> Co-authored-by: Vlad Frangu <me@vladfrangu.dev> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import { Buffer } from 'node:buffer';
|
|
import { AllowedMentionsTypes, ComponentType, MessageFlags, MessageReferenceType } from 'discord-api-types/v10';
|
|
import { z } from 'zod';
|
|
import { snowflakePredicate } from '../Assertions.js';
|
|
import { embedPredicate } from './embed/Assertions.js';
|
|
import { pollPredicate } from './poll/Assertions.js';
|
|
|
|
const fileKeyRegex = /^files\[(?<placeholder>\d+?)]$/;
|
|
|
|
export const rawFilePredicate = z.object({
|
|
data: z.union([z.instanceof(Buffer), z.instanceof(Uint8Array), z.string()]),
|
|
name: z.string().min(1),
|
|
contentType: z.string().optional(),
|
|
key: z.string().regex(fileKeyRegex).optional(),
|
|
});
|
|
|
|
export const attachmentPredicate = z.object({
|
|
// As a string it only makes sense for edits when we do have an attachment snowflake
|
|
id: z.union([snowflakePredicate, z.number()]),
|
|
description: z.string().max(1_024).optional(),
|
|
duration_secs: z
|
|
.number()
|
|
.max(2 ** 31 - 1)
|
|
.optional(),
|
|
filename: z.string().max(1_024).optional(),
|
|
title: z.string().max(1_024).optional(),
|
|
waveform: z.string().max(400).optional(),
|
|
});
|
|
|
|
export const allowedMentionPredicate = z
|
|
.object({
|
|
parse: z.enum(AllowedMentionsTypes).array().optional(),
|
|
roles: z.string().array().max(100).optional(),
|
|
users: z.string().array().max(100).optional(),
|
|
replied_user: z.boolean().optional(),
|
|
})
|
|
.refine(
|
|
(data) =>
|
|
!(
|
|
(data.parse?.includes(AllowedMentionsTypes.User) && data.users?.length) ||
|
|
(data.parse?.includes(AllowedMentionsTypes.Role) && data.roles?.length)
|
|
),
|
|
{
|
|
error:
|
|
'Cannot specify both parse: ["users"] and non-empty users array, or parse: ["roles"] and non-empty roles array. These are mutually exclusive',
|
|
},
|
|
);
|
|
|
|
export const messageReferencePredicate = z.object({
|
|
channel_id: z.string().optional(),
|
|
fail_if_not_exists: z.boolean().optional(),
|
|
guild_id: z.string().optional(),
|
|
message_id: z.string(),
|
|
type: z.enum(MessageReferenceType).optional(),
|
|
});
|
|
|
|
const baseMessagePredicate = z.object({
|
|
nonce: z.union([z.string().max(25), z.number()]).optional(),
|
|
tts: z.boolean().optional(),
|
|
allowed_mentions: allowedMentionPredicate.optional(),
|
|
message_reference: messageReferencePredicate.optional(),
|
|
attachments: attachmentPredicate.array().max(10).optional(),
|
|
enforce_nonce: z.boolean().optional(),
|
|
});
|
|
|
|
const basicActionRowPredicate = z.object({
|
|
type: z.literal(ComponentType.ActionRow),
|
|
components: z
|
|
.object({
|
|
type: z.literal([
|
|
ComponentType.Button,
|
|
ComponentType.ChannelSelect,
|
|
ComponentType.MentionableSelect,
|
|
ComponentType.RoleSelect,
|
|
ComponentType.StringSelect,
|
|
ComponentType.UserSelect,
|
|
]),
|
|
})
|
|
.array(),
|
|
});
|
|
|
|
const messageNoComponentsV2Predicate = baseMessagePredicate
|
|
.extend({
|
|
content: z.string().max(2_000).optional(),
|
|
embeds: embedPredicate.array().max(10).optional(),
|
|
sticker_ids: z.array(z.string()).max(3).optional(),
|
|
poll: pollPredicate.optional(),
|
|
components: basicActionRowPredicate.array().max(5).optional(),
|
|
flags: z
|
|
.int()
|
|
.optional()
|
|
.refine((flags) => !flags || (flags & MessageFlags.IsComponentsV2) === 0, {
|
|
error: 'Cannot set content, embeds, stickers, or poll with IsComponentsV2 flag set',
|
|
}),
|
|
})
|
|
.refine(
|
|
(data) =>
|
|
data.content !== undefined ||
|
|
(data.embeds !== undefined && data.embeds.length > 0) ||
|
|
data.poll !== undefined ||
|
|
(data.attachments !== undefined && data.attachments.length > 0) ||
|
|
(data.components !== undefined && data.components.length > 0) ||
|
|
(data.sticker_ids !== undefined && data.sticker_ids.length > 0),
|
|
{ error: 'Messages must have content, embeds, a poll, attachments, components or stickers' },
|
|
);
|
|
|
|
const allTopLevelComponentsPredicate = z
|
|
.union([
|
|
basicActionRowPredicate,
|
|
z.object({
|
|
type: z.literal([
|
|
// Components v2
|
|
ComponentType.Container,
|
|
ComponentType.File,
|
|
ComponentType.MediaGallery,
|
|
ComponentType.Section,
|
|
ComponentType.Separator,
|
|
ComponentType.TextDisplay,
|
|
ComponentType.Thumbnail,
|
|
]),
|
|
}),
|
|
])
|
|
.array()
|
|
.min(1)
|
|
.max(10);
|
|
|
|
const messageComponentsV2Predicate = baseMessagePredicate.extend({
|
|
components: allTopLevelComponentsPredicate,
|
|
flags: z.int().refine((flags) => (flags & MessageFlags.IsComponentsV2) === MessageFlags.IsComponentsV2, {
|
|
error: 'Must set IsComponentsV2 flag to use Components V2',
|
|
}),
|
|
// These fields cannot be set
|
|
content: z.string().length(0).nullish(),
|
|
embeds: z.array(z.never()).nullish(),
|
|
sticker_ids: z.array(z.never()).nullish(),
|
|
poll: z.null().optional(),
|
|
});
|
|
|
|
export const messagePredicate = z.union([messageNoComponentsV2Predicate, messageComponentsV2Predicate]);
|
|
|
|
// This validator does not assert file.key <-> attachment.id coherence. This is fine, because the builders
|
|
// should effectively guarantee that.
|
|
export const fileBodyMessagePredicate = z.object({
|
|
body: messagePredicate,
|
|
// No min length to support message edits
|
|
files: rawFilePredicate.array().max(10),
|
|
});
|