Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/builders/__tests__/messages/fileBody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { AttachmentBuilder, MessageBuilder } from '../../src/index.js';
test('AttachmentBuilder stores and exposes file data', () => {
const data = Buffer.from('hello world');
const attachment = new AttachmentBuilder()
.setId('0')
.setId(1)
.setFilename('greeting.txt')
.setFileData(data)
.setFileContentType('text/plain');

expect(attachment.getRawFile()).toStrictEqual({
contentType: 'text/plain',
data,
key: 'files[0]',
key: 'files[1]',
name: 'greeting.txt',
});

Expand All @@ -27,7 +27,7 @@ test('AttachmentBuilder stores and exposes file data', () => {
test('MessageBuilder.toFileBody returns JSON body and files', () => {
const msg = new MessageBuilder().setContent('here is a file').addAttachments(
new AttachmentBuilder()
.setId('0')
.setId(0)
.setFilename('file.bin')
.setFileData(Buffer.from([1, 2, 3]))
.setFileContentType('application/octet-stream'),
Expand All @@ -47,7 +47,9 @@ test('MessageBuilder.toFileBody returns JSON body and files', () => {
});

test('MessageBuilder.toFileBody returns empty files when attachments reference existing uploads', () => {
const msg = new MessageBuilder().addAttachments(new AttachmentBuilder().setId('123').setFilename('existing.png'));
const msg = new MessageBuilder().addAttachments(
new AttachmentBuilder().setId('1234567890123456789').setFilename('existing.png'),
);

const { body, files } = msg.toFileBody();
expect(body).toEqual(msg.toJSON());
Expand Down
4 changes: 2 additions & 2 deletions packages/builders/__tests__/messages/message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('Message', () => {
row.addPrimaryButtonComponents((button) => button.setCustomId('abc').setLabel('def')),
)
.setStickerIds('123', '456')
.addAttachments((attachment) => attachment.setId('hi!').setFilename('abc'))
.addAttachments((attachment) => attachment.setId(0).setFilename('abc'))
.setFlags(MessageFlags.Ephemeral)
.setEnforceNonce(false)
.updatePoll((poll) => poll.addAnswers({ poll_media: { text: 'foo' } }).setQuestion({ text: 'foo' }));
Expand All @@ -83,7 +83,7 @@ describe('Message', () => {
},
],
sticker_ids: ['123', '456'],
attachments: [{ id: 'hi!', filename: 'abc' }],
attachments: [{ id: 0, filename: 'abc' }],
flags: 64,
enforce_nonce: false,
poll: {
Expand Down
1 change: 1 addition & 0 deletions packages/builders/src/Assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod';

export const idPredicate = z.int().min(0).max(2_147_483_647).optional();
export const customIdPredicate = z.string().min(1).max(100);
export const snowflakePredicate = z.string().regex(/^[1-9]\d{16,18}$/);
Copy link
Member

@vladfrangu vladfrangu Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const snowflakePredicate = z.string().regex(/^[1-9]\d{16,18}$/);
export const snowflakePredicate = z.string().regex(/^[0-9]\d{16,19}$/);

Copy link
Member

@Jiralite Jiralite Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 is perfectly valid at the start?

Also, disagree on increasing the limit. The API doesn't even accept snowflakes that large...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 is perfectly valid at the start?

I do not like that we have that at the start, I completely missed that its two parts... But also why not? technically even 0 is a valid snowflake (and can be used in pagination to use as a starting point)

Copy link
Member

@Jiralite Jiralite Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is builders though. Snowflakes here are not used for pagination. They're used for emoji ids, select menus (role ids, channel ids, and user ids)... I'd argue someone is building something not at the start of Discord's time, so realistically, 0 is invalid.

If in the future, we ever make builders for methods that use pagination, we can revisit this?

Copy link
Member

@vladfrangu vladfrangu Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷, my two cents is that this should've been \d{17,20} and not overly complex but w/e (plus for attachments it can literally be just \d+ when creating)...

And before you argue for attachments that it cannot be \d+ bc the spec says so -> API cares about the limit, not the ID last I checked

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR alters that too, soo maybe that needs to be undone? Also you miss the point, the string "0" will work too

Copy link
Member

@Jiralite Jiralite Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It alters it because we're being strict on input: snowflakes for editing and numbers for uploading, which is the documented way.

Also you miss the point, the string "0" will work too

0 actually gets set to undefined:

key: this.data.id ? `files[${this.data.id}]` : undefined,

Is... is that an issue?

Someone shouldn't be passing "0" though, right? That seems like a mistake...

Copy link
Member

@didinele didinele Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone shouldn't be passing "0" though, right? That seems like a mistake...

yup. it should be 0 (number)

IMO the meaning of the ID is either snowflake (as in, literal, actual snowflake when editing, already uploaded attachment) or index, as in "im uploading this now, its the nth file"

re the code, yes. that's a bad falsy check

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #11314 to track that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


export const memberPermissionsPredicate = z.coerce.bigint();

Expand Down
14 changes: 7 additions & 7 deletions packages/builders/src/components/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
import { z } from 'zod';
import { idPredicate, customIdPredicate } from '../Assertions.js';
import { idPredicate, customIdPredicate, snowflakePredicate } from '../Assertions.js';

const labelPredicate = z.string().min(1).max(80);

export const emojiPredicate = z
.strictObject({
id: z.string().optional(),
id: snowflakePredicate.optional(),
name: z.string().min(2).max(32).optional(),
animated: z.boolean().optional(),
})
Expand Down Expand Up @@ -39,7 +39,7 @@ const buttonLinkPredicate = buttonPredicateBase.extend({

const buttonPremiumPredicate = buttonPredicateBase.extend({
style: z.literal(ButtonStyle.Premium),
sku_id: z.string(),
sku_id: snowflakePredicate,
});

export const buttonPredicate = z.discriminatedUnion('style', [
Expand All @@ -64,7 +64,7 @@ export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.ChannelSelect),
channel_types: z.enum(ChannelType).array().optional(),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) })
.object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.Channel) })
.array()
.max(25)
.optional(),
Expand All @@ -74,7 +74,7 @@ export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.MentionableSelect),
default_values: z
.object({
id: z.string(),
id: snowflakePredicate,
type: z.literal([SelectMenuDefaultValueType.Role, SelectMenuDefaultValueType.User]),
})
.array()
Expand All @@ -85,7 +85,7 @@ export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
export const selectMenuRolePredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.RoleSelect),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Role) })
.object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.Role) })
.array()
.max(25)
.optional(),
Expand Down Expand Up @@ -142,7 +142,7 @@ export const selectMenuStringPredicate = selectMenuBasePredicate
export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.UserSelect),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.User) })
.object({ id: snowflakePredicate, type: z.literal(SelectMenuDefaultValueType.User) })
.array()
.max(25)
.optional(),
Expand Down
3 changes: 2 additions & 1 deletion packages/builders/src/messages/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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';

Expand All @@ -15,7 +16,7 @@ export const rawFilePredicate = z.object({

export const attachmentPredicate = z.object({
// As a string it only makes sense for edits when we do have an attachment snowflake
id: z.union([z.string(), z.number()]),
id: z.union([snowflakePredicate, z.number()]),
description: z.string().max(1_024).optional(),
duration_secs: z
.number()
Expand Down