Skip to content

fix: message builders #10802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 25, 2025
Merged
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
34 changes: 31 additions & 3 deletions packages/builders/__tests__/messages/message.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AllowedMentionsTypes, MessageFlags } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { EmbedBuilder, MessageBuilder } from '../../src/index.js';
import { AllowedMentionsBuilder, EmbedBuilder, MessageBuilder } from '../../src/index.js';

const base = {
allowed_mentions: undefined,
Expand All @@ -24,13 +24,41 @@ describe('Message', () => {
expect(() => message.toJSON()).toThrow();
});

test('GIVEN parse: [users] and empty users THEN return valid toJSON data', () => {
const allowedMentions = new AllowedMentionsBuilder();
allowedMentions.setUsers();
allowedMentions.setParse(AllowedMentionsTypes.User);
expect(allowedMentions.toJSON()).toStrictEqual({ parse: [AllowedMentionsTypes.User], users: [] });
});

test('GIVEN parse: [roles] and empty roles THEN return valid toJSON data', () => {
const allowedMentions = new AllowedMentionsBuilder();
allowedMentions.setRoles();
allowedMentions.setParse(AllowedMentionsTypes.Role);
expect(allowedMentions.toJSON()).toStrictEqual({ parse: [AllowedMentionsTypes.Role], roles: [] });
});

test('GIVEN specific users and parse: [users] THEN it throws', () => {
const allowedMentions = new AllowedMentionsBuilder();
allowedMentions.setUsers('123');
allowedMentions.setParse(AllowedMentionsTypes.User);
expect(() => allowedMentions.toJSON()).toThrow();
});

test('GIVEN specific roles and parse: [roles] THEN it throws', () => {
const allowedMentions = new AllowedMentionsBuilder();
allowedMentions.setRoles('123');
allowedMentions.setParse(AllowedMentionsTypes.Role);
expect(() => allowedMentions.toJSON()).toThrow();
});

test('GIVEN tons of data THEN return valid toJSON data', () => {
const message = new MessageBuilder()
.setContent('foo')
.setNonce(123)
.setTTS()
.addEmbeds(new EmbedBuilder().setTitle('foo').setDescription('bar'))
.setAllowedMentions({ parse: [AllowedMentionsTypes.Role], roles: ['123'] })
.setAllowedMentions({ parse: [AllowedMentionsTypes.Role] })
.setMessageReference({ channel_id: '123', message_id: '123' })
.addActionRowComponents((row) =>
row.addPrimaryButtonComponents((button) => button.setCustomId('abc').setLabel('def')),
Expand All @@ -46,7 +74,7 @@ describe('Message', () => {
nonce: 123,
tts: true,
embeds: [{ title: 'foo', description: 'bar', author: undefined, fields: [], footer: undefined }],
allowed_mentions: { parse: ['roles'], roles: ['123'] },
allowed_mentions: { parse: ['roles'] },
message_reference: { channel_id: '123', message_id: '123' },
components: [
{
Expand Down
36 changes: 31 additions & 5 deletions packages/builders/src/messages/AllowedMentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export class AllowedMentionsBuilder implements JSONEncodable<APIAllowedMentions>
private readonly data: Partial<APIAllowedMentions>;

/**
* Creates new allowed mention builder from API data.
* Creates a new allowed mentions builder from API data.
*
* @param data - The API data to create this attachment with
* @param data - The API data to create this allowed mentions builder with
*/
public constructor(data: Partial<APIAllowedMentions> = {}) {
this.data = structuredClone(data);
Expand All @@ -29,6 +29,14 @@ export class AllowedMentionsBuilder implements JSONEncodable<APIAllowedMentions>
return this;
}

/**
* Clears the parse mention types.
*/
public clearParse(): this {
this.data.parse = undefined;
return this;
}

/**
* Sets the roles to mention.
*
Expand Down Expand Up @@ -65,7 +73,7 @@ export class AllowedMentionsBuilder implements JSONEncodable<APIAllowedMentions>
* allowedMentions.spliceRoles(0, 1);
* ```
* @example
* Remove the first n role:
* Remove the first n roles:
* ```ts
* const n = 4;
* allowedMentions.spliceRoles(0, n);
Expand All @@ -85,6 +93,14 @@ export class AllowedMentionsBuilder implements JSONEncodable<APIAllowedMentions>
return this;
}

/**
* Clears the roles to mention.
*/
public clearRoles(): this {
this.data.roles = undefined;
return this;
}

/**
* Sets the users to mention.
*
Expand Down Expand Up @@ -120,7 +136,7 @@ export class AllowedMentionsBuilder implements JSONEncodable<APIAllowedMentions>
* allowedMentions.spliceUsers(0, 1);
* ```
* @example
* Remove the first n user:
* Remove the first n users:
* ```ts
* const n = 4;
* allowedMentions.spliceUsers(0, n);
Expand All @@ -141,7 +157,17 @@ export class AllowedMentionsBuilder implements JSONEncodable<APIAllowedMentions>
}

/**
* For replies, sets whether to mention the author of the message being replied to
* Clears the users to mention.
*/
public clearUsers(): this {
this.data.users = undefined;
return this;
}

/**
* For replies, sets whether to mention the author of the message being replied to.
*
* @param repliedUser - Whether to mention the author of the message being replied to
*/
public setRepliedUser(repliedUser = true): this {
this.data.replied_user = repliedUser;
Expand Down
41 changes: 28 additions & 13 deletions packages/builders/src/messages/Assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,34 @@ import { pollPredicate } from './poll/Assertions.js';

export const attachmentPredicate = z.object({
id: z.union([z.string(), z.number()]),
description: z.string().optional(),
duration_secs: z.number().optional(),
filename: z.string().optional(),
title: z.string().optional(),
waveform: z.string().optional(),
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.nativeEnum(AllowedMentionsTypes).array().optional(),
roles: z.string().array().optional(),
users: z.string().array().optional(),
replied_user: z.boolean().optional(),
});
export const allowedMentionPredicate = z
.object({
parse: z.nativeEnum(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)
),
{
message:
'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(),
Expand Down Expand Up @@ -54,9 +69,9 @@ const basicActionRowPredicate = z.object({

const messageNoComponentsV2Predicate = baseMessagePredicate
.extend({
content: z.string().optional(),
content: z.string().max(2_000).optional(),
embeds: embedPredicate.array().max(10).optional(),
sticker_ids: z.array(z.string()).min(0).max(3).optional(),
sticker_ids: z.array(z.string()).max(3).optional(),
poll: pollPredicate.optional(),
components: basicActionRowPredicate.array().max(5).optional(),
flags: z
Expand Down
18 changes: 7 additions & 11 deletions packages/builders/src/messages/Attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,28 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
private readonly data: Partial<RESTAPIAttachment>;

/**
* Creates new attachment builder from API data.
* Creates a new attachment builder from API data.
*
* @param data - The API data to create this attachment with
* @param data - The API data to create this attachment builder with
*/
public constructor(data: Partial<RESTAPIAttachment> = {}) {
this.data = structuredClone(data);
}

/**
* Sets the id of the attachment.
*
* @param id - The id of the attachment
*/
public setId(id: Snowflake): this {
this.data.id = id;
return this;
}

/**
* Clears the id of this attachment.
*/
public clearId(): this {
this.data.id = undefined;
return this;
}

/**
* Sets the description of this attachment.
*
* @param description - The description of the attachment
*/
public setDescription(description: string): this {
this.data.description = description;
Expand Down Expand Up @@ -105,7 +101,7 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
}

/**
* Sets the waveform of this attachment.
* Sets the waveform of this attachment (audio clips).
*
* @param waveform - The waveform of the attachment
*/
Expand Down
Loading