mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
fix: message builders (#10802)
* fix: message builders
- Added `clearParse`, `clearRoles`, and `clearUsers` methods to the `AllowedMentionsBuilder`, since passing an empty array and omitting the these fields behave differently
- Strictened assertions
- Removed `AttachmentBuilder#clearId`, as it is a required field
- Added missing `MessageBuilder#setEmbeds`
- Added missing `MessageReferenceBuilder#setFailIfNotExists`
- Improve/fix documentation
- Consistency™️
* fix: updater functions return type
---------
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
@@ -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')),
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
*
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
*
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
@@ -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
|
||||
|
||||
@@ -10,15 +10,17 @@ 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 {
|
||||
@@ -26,16 +28,10 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
|
||||
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;
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
@@ -53,6 +55,9 @@ export interface MessageBuilderData
|
||||
poll?: PollBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for messages.
|
||||
*/
|
||||
export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJSONBody> {
|
||||
/**
|
||||
* The API data associated with this message.
|
||||
@@ -81,19 +86,27 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new attachment builder from API data.
|
||||
* Creates a new message builder from API data.
|
||||
*
|
||||
* @param data - The API data to create this attachment with
|
||||
* @param data - The API data to create this message builder with
|
||||
*/
|
||||
public constructor(data: Partial<RESTPostAPIChannelMessageJSONBody> = {}) {
|
||||
public constructor({
|
||||
attachments = [],
|
||||
embeds = [],
|
||||
components = [],
|
||||
message_reference,
|
||||
poll,
|
||||
allowed_mentions,
|
||||
...data
|
||||
}: Partial<RESTPostAPIChannelMessageJSONBody> = {}) {
|
||||
this.data = {
|
||||
...structuredClone(data),
|
||||
allowed_mentions: data.allowed_mentions ? new AllowedMentionsBuilder(data.allowed_mentions) : undefined,
|
||||
attachments: data.attachments?.map((attachment) => new AttachmentBuilder(attachment)) ?? [],
|
||||
embeds: data.embeds?.map((embed) => new EmbedBuilder(embed)) ?? [],
|
||||
poll: data.poll ? new PollBuilder(data.poll) : undefined,
|
||||
components: data.components?.map((component) => createComponentBuilder(component)) ?? [],
|
||||
message_reference: data.message_reference ? new MessageReferenceBuilder(data.message_reference) : undefined,
|
||||
allowed_mentions: allowed_mentions && new AllowedMentionsBuilder(allowed_mentions),
|
||||
attachments: attachments.map((attachment) => new AttachmentBuilder(attachment)),
|
||||
embeds: embeds.map((embed) => new EmbedBuilder(embed)),
|
||||
poll: poll && new PollBuilder(poll),
|
||||
components: components.map((component) => createComponentBuilder(component)),
|
||||
message_reference: message_reference && new MessageReferenceBuilder(message_reference),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -135,6 +148,8 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
|
||||
/**
|
||||
* Sets whether the message is TTS.
|
||||
*
|
||||
* @param tts - Whether the message is TTS
|
||||
*/
|
||||
public setTTS(tts = true): this {
|
||||
this.data.tts = tts;
|
||||
@@ -213,6 +228,15 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the embeds for this message.
|
||||
*
|
||||
* @param embeds - The embeds to set
|
||||
*/
|
||||
public setEmbeds(...embeds: RestOrArray<APIEmbed | EmbedBuilder | ((builder: EmbedBuilder) => EmbedBuilder)>): this {
|
||||
return this.spliceEmbeds(0, this.embeds.length, ...normalizeArray(embeds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the allowed mentions for this message.
|
||||
*
|
||||
@@ -233,8 +257,8 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
*
|
||||
* @param updater - The function to update the allowed mentions with
|
||||
*/
|
||||
public updateAllowedMentions(updater: (builder: AllowedMentionsBuilder) => AllowedMentionsBuilder): this {
|
||||
this.data.allowed_mentions = updater(this.data.allowed_mentions ?? new AllowedMentionsBuilder());
|
||||
public updateAllowedMentions(updater: (builder: AllowedMentionsBuilder) => void): this {
|
||||
updater((this.data.allowed_mentions ??= new AllowedMentionsBuilder()));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -266,8 +290,8 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
*
|
||||
* @param updater - The function to update the message reference with
|
||||
*/
|
||||
public updateMessageReference(updater: (builder: MessageReferenceBuilder) => MessageReferenceBuilder): this {
|
||||
this.data.message_reference = updater(this.data.message_reference ?? new MessageReferenceBuilder());
|
||||
public updateMessageReference(updater: (builder: MessageReferenceBuilder) => void): this {
|
||||
updater((this.data.message_reference ??= new MessageReferenceBuilder()));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -451,8 +475,7 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
* @param stickerIds - The ids of the stickers to set
|
||||
*/
|
||||
public setStickerIds(...stickerIds: RestOrArray<Snowflake>): this {
|
||||
this.data.sticker_ids = normalizeArray(stickerIds) as MessageBuilderData['sticker_ids'];
|
||||
return this;
|
||||
return this.spliceStickerIds(0, this.data.sticker_ids?.length ?? 0, ...normalizeArray(stickerIds));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -508,10 +531,7 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
public setAttachments(
|
||||
...attachments: RestOrArray<APIAttachment | AttachmentBuilder | ((builder: AttachmentBuilder) => AttachmentBuilder)>
|
||||
): this {
|
||||
const resolved = normalizeArray(attachments).map((attachment) => resolveBuilder(attachment, AttachmentBuilder));
|
||||
this.data.attachments = resolved;
|
||||
|
||||
return this;
|
||||
return this.spliceAttachments(0, this.data.attachments.length, ...normalizeArray(attachments));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,6 +589,8 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
|
||||
/**
|
||||
* Sets the flags for this message.
|
||||
*
|
||||
* @param flags - The flags to set
|
||||
*/
|
||||
public setFlags(flags: MessageFlags): this {
|
||||
this.data.flags = flags;
|
||||
@@ -584,7 +606,9 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `enforce_nonce` for this message.
|
||||
* Sets whether to enforce recent uniqueness of the nonce of this message.
|
||||
*
|
||||
* @param enforceNonce - Whether to enforce recent uniqueness of the nonce of this message
|
||||
*/
|
||||
public setEnforceNonce(enforceNonce = true): this {
|
||||
this.data.enforce_nonce = enforceNonce;
|
||||
@@ -606,8 +630,8 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
*
|
||||
* @param updater - The function to update the poll with
|
||||
*/
|
||||
public updatePoll(updater: (builder: PollBuilder) => PollBuilder): this {
|
||||
this.data.poll = updater(this.data.poll ?? new PollBuilder());
|
||||
public updatePoll(updater: (builder: PollBuilder) => void): this {
|
||||
updater((this.data.poll ??= new PollBuilder()));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -632,12 +656,12 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
||||
const data = {
|
||||
...structuredClone(rest),
|
||||
// Wherever we pass false, it's covered by the messagePredicate already
|
||||
poll: this.data.poll?.toJSON(false),
|
||||
poll: poll?.toJSON(false),
|
||||
allowed_mentions: allowed_mentions?.toJSON(false),
|
||||
attachments: attachments.map((attachment) => attachment.toJSON(false)),
|
||||
embeds: this.data.embeds.map((embed) => embed.toJSON(false)),
|
||||
embeds: embeds.map((embed) => embed.toJSON(false)),
|
||||
// Here, the messagePredicate does specific constraints rather than using the componentPredicate
|
||||
components: this.data.components?.map((component) => component.toJSON(validationOverride)),
|
||||
components: components.map((component) => component.toJSON(validationOverride)),
|
||||
message_reference: message_reference?.toJSON(false),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { APIMessageReference, MessageReferenceType, Snowflake } from 'discord-api-types/v10';
|
||||
import type { MessageReferenceType, RESTAPIMessageReference, Snowflake } from 'discord-api-types/v10';
|
||||
import { validate } from '../util/validation.js';
|
||||
import { messageReferencePredicate } from './Assertions.js';
|
||||
|
||||
export interface MessageReferenceBuilderData extends Omit<APIMessageReference, 'message_id'> {
|
||||
message_id: Snowflake;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for message references.
|
||||
*/
|
||||
export class MessageReferenceBuilder implements JSONEncodable<MessageReferenceBuilderData> {
|
||||
private readonly data: Partial<MessageReferenceBuilderData>;
|
||||
export class MessageReferenceBuilder implements JSONEncodable<RESTAPIMessageReference> {
|
||||
private readonly data: Partial<RESTAPIMessageReference>;
|
||||
|
||||
/**
|
||||
* Creates new allowed mention builder from API data.
|
||||
* Creates a new message reference builder from API data.
|
||||
*
|
||||
* @param data - The API data to create this attachment with
|
||||
* @param data - The API data to create this message reference builder with
|
||||
*/
|
||||
public constructor(data: Partial<MessageReferenceBuilderData> = {}) {
|
||||
public constructor(data: Partial<RESTAPIMessageReference> = {}) {
|
||||
this.data = structuredClone(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the types of message reference this represents
|
||||
* Sets the type of message reference this represents
|
||||
*
|
||||
* @param type - The type of message reference
|
||||
*/
|
||||
@@ -86,6 +82,16 @@ export class MessageReferenceBuilder implements JSONEncodable<MessageReferenceBu
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to fail the message creation if the referenced message does not exist
|
||||
*
|
||||
* @param failIfNotExists - Whether to fail the message creation if the referenced message does not exist
|
||||
*/
|
||||
public setFailIfNotExists(failIfNotExists = true): this {
|
||||
this.data.fail_if_not_exists = failIfNotExists;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
@@ -93,10 +99,10 @@ export class MessageReferenceBuilder implements JSONEncodable<MessageReferenceBu
|
||||
*
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public toJSON(validationOverride?: boolean): MessageReferenceBuilderData {
|
||||
public toJSON(validationOverride?: boolean): RESTAPIMessageReference {
|
||||
const clone = structuredClone(this.data);
|
||||
validate(messageReferencePredicate, clone, validationOverride);
|
||||
|
||||
return clone as MessageReferenceBuilderData;
|
||||
return clone as RESTAPIMessageReference;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user