mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
feat: message builder (#10793)
* feat: attachment builder * feat: message builder * chore: nits Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * fix: nonce assertion * chore: strip bad method * chore: nit Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * chore: nits * chore: address final review --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ab6a69401e
commit
09beb8a6a0
65
packages/builders/__tests__/messages/message.test.ts
Normal file
65
packages/builders/__tests__/messages/message.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { AllowedMentionsTypes, MessageFlags } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { EmbedBuilder, MessageBuilder } from '../../src/index.js';
|
||||
|
||||
const base = {
|
||||
allowed_mentions: undefined,
|
||||
attachments: [],
|
||||
components: [],
|
||||
embeds: [],
|
||||
message_reference: undefined,
|
||||
poll: undefined,
|
||||
};
|
||||
|
||||
describe('Message', () => {
|
||||
test('GIVEN a message with pre-defined content THEN return valid toJSON data', () => {
|
||||
const message = new MessageBuilder({ content: 'foo' });
|
||||
expect(message.toJSON()).toStrictEqual({ ...base, content: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN bad action row THEN it throws', () => {
|
||||
const message = new MessageBuilder().setComponents((row) =>
|
||||
row.addTextInputComponent((input) => input.setCustomId('abc').setLabel('def')),
|
||||
);
|
||||
expect(() => message.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'] })
|
||||
.setMessageReference({ channel_id: '123', message_id: '123' })
|
||||
.setComponents((row) => row.addPrimaryButtonComponents((button) => button.setCustomId('abc').setLabel('def')))
|
||||
.setStickerIds('123', '456')
|
||||
.addAttachments((attachment) => attachment.setId('hi!').setFilename('abc'))
|
||||
.setFlags(MessageFlags.Ephemeral)
|
||||
.setEnforceNonce(false)
|
||||
.updatePoll((poll) => poll.addAnswers({ poll_media: { text: 'foo' } }).setQuestion({ text: 'foo' }));
|
||||
|
||||
expect(message.toJSON()).toStrictEqual({
|
||||
content: 'foo',
|
||||
nonce: 123,
|
||||
tts: true,
|
||||
embeds: [{ title: 'foo', description: 'bar', author: undefined, fields: [], footer: undefined }],
|
||||
allowed_mentions: { parse: ['roles'], roles: ['123'] },
|
||||
message_reference: { channel_id: '123', message_id: '123' },
|
||||
components: [
|
||||
{
|
||||
type: 1,
|
||||
components: [{ type: 2, custom_id: 'abc', label: 'def', style: 1 }],
|
||||
},
|
||||
],
|
||||
sticker_ids: ['123', '456'],
|
||||
attachments: [{ id: 'hi!', filename: 'abc' }],
|
||||
flags: 64,
|
||||
enforce_nonce: false,
|
||||
poll: {
|
||||
question: { text: 'foo' },
|
||||
answers: [{ poll_media: { text: 'foo' } }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -67,6 +67,12 @@ export * from './messages/poll/PollAnswerMedia.js';
|
||||
export * from './messages/poll/PollMedia.js';
|
||||
export * from './messages/poll/PollQuestion.js';
|
||||
|
||||
export * from './messages/AllowedMentions.js';
|
||||
export * from './messages/Assertions.js';
|
||||
export * from './messages/Attachment.js';
|
||||
export * from './messages/Message.js';
|
||||
export * from './messages/MessageReference.js';
|
||||
|
||||
export * from './util/componentUtil.js';
|
||||
export * from './util/normalizeArray.js';
|
||||
export * from './util/resolveBuilder.js';
|
||||
|
||||
164
packages/builders/src/messages/AllowedMentions.ts
Normal file
164
packages/builders/src/messages/AllowedMentions.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { AllowedMentionsTypes, APIAllowedMentions, Snowflake } from 'discord-api-types/v10';
|
||||
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
||||
import { validate } from '../util/validation.js';
|
||||
import { allowedMentionPredicate } from './Assertions.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for allowed mentions.
|
||||
*/
|
||||
export class AllowedMentionsBuilder implements JSONEncodable<APIAllowedMentions> {
|
||||
private readonly data: Partial<APIAllowedMentions>;
|
||||
|
||||
/**
|
||||
* Creates new allowed mention builder from API data.
|
||||
*
|
||||
* @param data - The API data to create this attachment with
|
||||
*/
|
||||
public constructor(data: Partial<APIAllowedMentions> = {}) {
|
||||
this.data = structuredClone(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the types of mentions to parse from the content.
|
||||
*
|
||||
* @param parse - The types of mentions to parse from the content
|
||||
*/
|
||||
public setParse(...parse: RestOrArray<AllowedMentionsTypes>): this {
|
||||
this.data.parse = normalizeArray(parse);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the roles to mention.
|
||||
*
|
||||
* @param roles - The roles to mention
|
||||
*/
|
||||
public setRoles(...roles: RestOrArray<Snowflake>): this {
|
||||
this.data.roles = normalizeArray(roles);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds roles to mention.
|
||||
*
|
||||
* @param roles - The roles to mention
|
||||
*/
|
||||
public addRoles(...roles: RestOrArray<Snowflake>): this {
|
||||
this.data.roles ??= [];
|
||||
this.data.roles.push(...normalizeArray(roles));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts roles.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing roles.
|
||||
* @example
|
||||
* Remove the first role:
|
||||
* ```ts
|
||||
* allowedMentions.spliceRoles(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n role:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* allowedMentions.spliceRoles(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last role:
|
||||
* ```ts
|
||||
* allowedMentions.spliceRoles(-1, 1);
|
||||
* ```
|
||||
* @param index - The index to start at
|
||||
* @param deleteCount - The number of roles to remove
|
||||
* @param roles - The replacing role ids
|
||||
*/
|
||||
public spliceRoles(index: number, deleteCount: number, ...roles: RestOrArray<Snowflake>): this {
|
||||
this.data.roles ??= [];
|
||||
this.data.roles.splice(index, deleteCount, ...normalizeArray(roles));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the users to mention.
|
||||
*
|
||||
* @param users - The users to mention
|
||||
*/
|
||||
public setUsers(...users: RestOrArray<Snowflake>): this {
|
||||
this.data.users = normalizeArray(users);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds users to mention.
|
||||
*
|
||||
* @param users - The users to mention
|
||||
*/
|
||||
public addUsers(...users: RestOrArray<Snowflake>): this {
|
||||
this.data.users ??= [];
|
||||
this.data.users.push(...normalizeArray(users));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts users.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing users.
|
||||
* @example
|
||||
* Remove the first user:
|
||||
* ```ts
|
||||
* allowedMentions.spliceUsers(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n user:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* allowedMentions.spliceUsers(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last user:
|
||||
* ```ts
|
||||
* allowedMentions.spliceUsers(-1, 1);
|
||||
* ```
|
||||
* @param index - The index to start at
|
||||
* @param deleteCount - The number of users to remove
|
||||
* @param users - The replacing user ids
|
||||
*/
|
||||
public spliceUsers(index: number, deleteCount: number, ...users: RestOrArray<Snowflake>): this {
|
||||
this.data.users ??= [];
|
||||
this.data.users.splice(index, deleteCount, ...normalizeArray(users));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* For replies, sets whether to mention the author of the message being replied to
|
||||
*/
|
||||
public setRepliedUser(repliedUser = true): this {
|
||||
this.data.replied_user = repliedUser;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||
*
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public toJSON(validationOverride?: boolean): APIAllowedMentions {
|
||||
const clone = structuredClone(this.data);
|
||||
validate(allowedMentionPredicate, clone, validationOverride);
|
||||
|
||||
return clone as APIAllowedMentions;
|
||||
}
|
||||
}
|
||||
77
packages/builders/src/messages/Assertions.ts
Normal file
77
packages/builders/src/messages/Assertions.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { AllowedMentionsTypes, ComponentType, MessageReferenceType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { embedPredicate } from './embed/Assertions.js';
|
||||
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(),
|
||||
});
|
||||
|
||||
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 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.nativeEnum(MessageReferenceType).optional(),
|
||||
});
|
||||
|
||||
export const messagePredicate = z
|
||||
.object({
|
||||
content: z.string().optional(),
|
||||
nonce: z.union([z.string().max(25), z.number()]).optional(),
|
||||
tts: z.boolean().optional(),
|
||||
embeds: embedPredicate.array().max(10).optional(),
|
||||
allowed_mentions: allowedMentionPredicate.optional(),
|
||||
message_reference: messageReferencePredicate.optional(),
|
||||
// Partial validation here to ensure the components are valid,
|
||||
// rest of the validation is done in the action row predicate
|
||||
components: z
|
||||
.object({
|
||||
type: z.literal(ComponentType.ActionRow),
|
||||
components: z
|
||||
.object({
|
||||
type: z.union([
|
||||
z.literal(ComponentType.Button),
|
||||
z.literal(ComponentType.ChannelSelect),
|
||||
z.literal(ComponentType.MentionableSelect),
|
||||
z.literal(ComponentType.RoleSelect),
|
||||
z.literal(ComponentType.StringSelect),
|
||||
z.literal(ComponentType.UserSelect),
|
||||
]),
|
||||
})
|
||||
.array(),
|
||||
})
|
||||
.array()
|
||||
.max(5)
|
||||
.optional(),
|
||||
sticker_ids: z.array(z.string()).min(0).max(3).optional(),
|
||||
attachments: attachmentPredicate.array().max(10).optional(),
|
||||
flags: z.number().optional(),
|
||||
enforce_nonce: z.boolean().optional(),
|
||||
poll: pollPredicate.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
return (
|
||||
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)
|
||||
);
|
||||
},
|
||||
{ message: 'Messages must have content, embeds, a poll, attachments, components, or stickers' },
|
||||
);
|
||||
138
packages/builders/src/messages/Attachment.ts
Normal file
138
packages/builders/src/messages/Attachment.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { RESTAPIAttachment, Snowflake } from 'discord-api-types/v10';
|
||||
import { validate } from '../util/validation.js';
|
||||
import { attachmentPredicate } from './Assertions.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for attachments.
|
||||
*/
|
||||
export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
|
||||
private readonly data: Partial<RESTAPIAttachment>;
|
||||
|
||||
/**
|
||||
* Creates new attachment builder from API data.
|
||||
*
|
||||
* @param data - The API data to create this attachment with
|
||||
*/
|
||||
public constructor(data: Partial<RESTAPIAttachment> = {}) {
|
||||
this.data = structuredClone(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
public setDescription(description: string): this {
|
||||
this.data.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the description of this attachment.
|
||||
*/
|
||||
public clearDescription(): this {
|
||||
this.data.description = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration of this attachment (audio clips).
|
||||
*
|
||||
* @param duration - The duration of the attachment in seconds
|
||||
*/
|
||||
public setDuration(duration: number): this {
|
||||
this.data.duration_secs = duration;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the duration of this attachment.
|
||||
*/
|
||||
public clearDuration(): this {
|
||||
this.data.duration_secs = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the filename of this attachment.
|
||||
*
|
||||
* @param filename - The filename of the attachment
|
||||
*/
|
||||
public setFilename(filename: string): this {
|
||||
this.data.filename = filename;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the filename of this attachment.
|
||||
*/
|
||||
public clearFilename(): this {
|
||||
this.data.filename = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of this attachment.
|
||||
*
|
||||
* @param title - The title of the attachment
|
||||
*/
|
||||
public setTitle(title: string): this {
|
||||
this.data.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the title of this attachment.
|
||||
*/
|
||||
public clearTitle(): this {
|
||||
this.data.title = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the waveform of this attachment.
|
||||
*
|
||||
* @param waveform - The waveform of the attachment
|
||||
*/
|
||||
public setWaveform(waveform: string): this {
|
||||
this.data.waveform = waveform;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the waveform of this attachment.
|
||||
*/
|
||||
public clearWaveform(): this {
|
||||
this.data.waveform = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||
*
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public toJSON(validationOverride?: boolean): RESTAPIAttachment {
|
||||
const clone = structuredClone(this.data);
|
||||
validate(attachmentPredicate, clone, validationOverride);
|
||||
|
||||
return clone as RESTAPIAttachment;
|
||||
}
|
||||
}
|
||||
546
packages/builders/src/messages/Message.ts
Normal file
546
packages/builders/src/messages/Message.ts
Normal file
@@ -0,0 +1,546 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIAllowedMentions,
|
||||
APIAttachment,
|
||||
APIEmbed,
|
||||
APIMessageActionRowComponent,
|
||||
APIMessageReference,
|
||||
APIPoll,
|
||||
RESTPostAPIChannelMessageJSONBody,
|
||||
Snowflake,
|
||||
MessageFlags,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ActionRowBuilder } from '../components/ActionRow.js';
|
||||
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
||||
import { resolveBuilder } from '../util/resolveBuilder.js';
|
||||
import { validate } from '../util/validation.js';
|
||||
import { AllowedMentionsBuilder } from './AllowedMentions.js';
|
||||
import { messagePredicate } from './Assertions.js';
|
||||
import { AttachmentBuilder } from './Attachment.js';
|
||||
import { MessageReferenceBuilder } from './MessageReference.js';
|
||||
import { EmbedBuilder } from './embed/Embed.js';
|
||||
import { PollBuilder } from './poll/Poll.js';
|
||||
|
||||
export interface MessageBuilderData
|
||||
extends Partial<
|
||||
Omit<
|
||||
RESTPostAPIChannelMessageJSONBody,
|
||||
'allowed_mentions' | 'attachments' | 'components' | 'embeds' | 'message_reference' | 'poll'
|
||||
>
|
||||
> {
|
||||
allowed_mentions?: AllowedMentionsBuilder;
|
||||
attachments: AttachmentBuilder[];
|
||||
components: ActionRowBuilder[];
|
||||
embeds: EmbedBuilder[];
|
||||
message_reference?: MessageReferenceBuilder;
|
||||
poll?: PollBuilder;
|
||||
}
|
||||
|
||||
export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJSONBody> {
|
||||
/**
|
||||
* The API data associated with this message.
|
||||
*/
|
||||
private readonly data: MessageBuilderData;
|
||||
|
||||
/**
|
||||
* Gets the attachments of this message.
|
||||
*/
|
||||
public get attachments(): readonly AttachmentBuilder[] {
|
||||
return this.data.attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the components of this message.
|
||||
*/
|
||||
public get components(): readonly ActionRowBuilder[] {
|
||||
return this.data.components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the embeds of this message.
|
||||
*/
|
||||
public get embeds(): readonly EmbedBuilder[] {
|
||||
return this.data.embeds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new attachment builder from API data.
|
||||
*
|
||||
* @param data - The API data to create this attachment with
|
||||
*/
|
||||
public constructor(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) => new ActionRowBuilder(component)) ?? [],
|
||||
message_reference: data.message_reference ? new MessageReferenceBuilder(data.message_reference) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content of the message.
|
||||
*
|
||||
* @param content - The content to set
|
||||
*/
|
||||
public setContent(content: string): this {
|
||||
this.data.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the content of the message.
|
||||
*/
|
||||
public clearContent(): this {
|
||||
this.data.content = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the nonce of the message.
|
||||
*
|
||||
* @param nonce - The nonce to set
|
||||
*/
|
||||
public setNonce(nonce: number | string): this {
|
||||
this.data.nonce = nonce;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the nonce of the message.
|
||||
*/
|
||||
public clearNonce(): this {
|
||||
this.data.nonce = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the message is TTS.
|
||||
*/
|
||||
public setTTS(tts = true): this {
|
||||
this.data.tts = tts;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends embeds to this message.
|
||||
*
|
||||
* @remarks
|
||||
* The maximum amount of embeds that can be added is 10.
|
||||
* @example
|
||||
* Using an array:
|
||||
* ```ts
|
||||
* const embeds: APIEmbed[] = ...;
|
||||
* const message = new MessageBuilder()
|
||||
* .addEmbeds(embeds);
|
||||
* ```
|
||||
* @example
|
||||
* Using rest parameters (variadic):
|
||||
* ```ts
|
||||
* const message = new MessageBuilder()
|
||||
* .addEmbeds(
|
||||
* { title: 'Embed 1' },
|
||||
* { title: 'Embed 2' },
|
||||
* );
|
||||
* ```
|
||||
* @param embeds - The embeds to add
|
||||
*/
|
||||
public addEmbeds(...embeds: RestOrArray<APIEmbed | EmbedBuilder | ((builder: EmbedBuilder) => EmbedBuilder)>): this {
|
||||
this.data.embeds ??= [];
|
||||
|
||||
const resolved = normalizeArray(embeds).map((embed) => resolveBuilder(embed, EmbedBuilder));
|
||||
this.data.embeds.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts embeds for this message.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing embeds of a message.
|
||||
* @example
|
||||
* Remove the first embed:
|
||||
* ```ts
|
||||
* message.spliceEmbeds(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n embeds:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* message.spliceEmbeds(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last embed:
|
||||
* ```ts
|
||||
* message.spliceEmbeds(-1, 1);
|
||||
* ```
|
||||
* @param start - The index to start at
|
||||
* @param deleteCount - The amount of embeds to remove
|
||||
* @param embeds - The embeds to insert
|
||||
*/
|
||||
public spliceEmbeds(
|
||||
start: number,
|
||||
deleteCount: number,
|
||||
...embeds: RestOrArray<APIEmbed | EmbedBuilder | ((builder: EmbedBuilder) => EmbedBuilder)>
|
||||
): this {
|
||||
this.data.embeds ??= [];
|
||||
const resolved = normalizeArray(embeds).map((embed) => resolveBuilder(embed, EmbedBuilder));
|
||||
|
||||
this.data.embeds.splice(start, deleteCount, ...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the allowed mentions for this message.
|
||||
*
|
||||
* @param allowedMentions - The allowed mentions to set
|
||||
*/
|
||||
public setAllowedMentions(
|
||||
allowedMentions:
|
||||
| AllowedMentionsBuilder
|
||||
| APIAllowedMentions
|
||||
| ((builder: AllowedMentionsBuilder) => AllowedMentionsBuilder),
|
||||
): this {
|
||||
this.data.allowed_mentions = resolveBuilder(allowedMentions, AllowedMentionsBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the allowed mentions for this message (and creates it if it doesn't exist)
|
||||
*
|
||||
* @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());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the allowed mentions for this message.
|
||||
*/
|
||||
public clearAllowedMentions(): this {
|
||||
this.data.allowed_mentions = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the message reference for this message.
|
||||
*
|
||||
* @param reference - The reference to set
|
||||
*/
|
||||
public setMessageReference(
|
||||
reference:
|
||||
| APIMessageReference
|
||||
| MessageReferenceBuilder
|
||||
| ((builder: MessageReferenceBuilder) => MessageReferenceBuilder),
|
||||
): this {
|
||||
this.data.message_reference = resolveBuilder(reference, MessageReferenceBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the message reference for this message (and creates it if it doesn't exist)
|
||||
*
|
||||
* @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());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the message reference for this message.
|
||||
*/
|
||||
public clearMessageReference(): this {
|
||||
this.data.message_reference = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds components to this message.
|
||||
*
|
||||
* @param components - The components to add
|
||||
*/
|
||||
public addComponents(
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder
|
||||
| APIActionRowComponent<APIMessageActionRowComponent>
|
||||
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||
>
|
||||
): this {
|
||||
this.data.components ??= [];
|
||||
|
||||
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, ActionRowBuilder));
|
||||
this.data.components.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts components for this message.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing components of a message.
|
||||
* @example
|
||||
* Remove the first component:
|
||||
* ```ts
|
||||
* message.spliceComponents(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n components:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* message.spliceComponents(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last component:
|
||||
* ```ts
|
||||
* message.spliceComponents(-1, 1);
|
||||
* ```
|
||||
* @param start - The index to start at
|
||||
* @param deleteCount - The amount of components to remove
|
||||
* @param components - The components to insert
|
||||
*/
|
||||
public spliceComponents(
|
||||
start: number,
|
||||
deleteCount: number,
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder
|
||||
| APIActionRowComponent<APIMessageActionRowComponent>
|
||||
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||
>
|
||||
): this {
|
||||
this.data.components ??= [];
|
||||
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, ActionRowBuilder));
|
||||
|
||||
this.data.components.splice(start, deleteCount, ...resolved);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the components of this message.
|
||||
*
|
||||
* @param components - The components to set
|
||||
*/
|
||||
public setComponents(
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder
|
||||
| APIActionRowComponent<APIMessageActionRowComponent>
|
||||
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||
>
|
||||
): this {
|
||||
this.data.components = normalizeArray(components).map((component) => resolveBuilder(component, ActionRowBuilder));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sticker ids of this message.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds sticker ids to this message.
|
||||
*
|
||||
* @param stickerIds - The ids of the stickers to add
|
||||
*/
|
||||
public addStickerIds(...stickerIds: RestOrArray<Snowflake>): this {
|
||||
this.data.sticker_ids ??= [] as unknown as MessageBuilderData['sticker_ids'];
|
||||
this.data.sticker_ids!.push(...normalizeArray(stickerIds));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts sticker ids for this message.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing sticker ids of a message.
|
||||
* @example
|
||||
* Remove the first sticker id:
|
||||
* ```ts
|
||||
* message.spliceStickerIds(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n sticker ids:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* message.spliceStickerIds(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last sticker id:
|
||||
* ```ts
|
||||
* message.spliceStickerIds(-1, 1);
|
||||
* ```
|
||||
* @param index - The index to start at
|
||||
* @param deleteCount - The amount of sticker ids to remove
|
||||
* @param stickerIds - The sticker ids to insert
|
||||
*/
|
||||
public spliceStickerIds(index: number, deleteCount: number, ...stickerIds: RestOrArray<Snowflake>): this {
|
||||
this.data.sticker_ids ??= [] as unknown as MessageBuilderData['sticker_ids'];
|
||||
this.data.sticker_ids!.splice(index, deleteCount, ...normalizeArray(stickerIds));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets attachments for this message.
|
||||
*
|
||||
* @param attachments - The attachments to set
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds attachments to this message.
|
||||
*
|
||||
* @param attachments - The attachments to add
|
||||
*/
|
||||
public addAttachments(
|
||||
...attachments: RestOrArray<APIAttachment | AttachmentBuilder | ((builder: AttachmentBuilder) => AttachmentBuilder)>
|
||||
): this {
|
||||
const resolved = normalizeArray(attachments).map((attachment) => resolveBuilder(attachment, AttachmentBuilder));
|
||||
this.data.attachments.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts attachments for this message.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing attachments of a message.
|
||||
* @example
|
||||
* Remove the first attachment:
|
||||
* ```ts
|
||||
* message.spliceAttachments(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n attachments:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* message.spliceAttachments(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last attachment:
|
||||
* ```ts
|
||||
* message.spliceAttachments(-1, 1);
|
||||
* ```
|
||||
* @param start - The index to start at
|
||||
* @param deleteCount - The amount of attachments to remove
|
||||
* @param attachments - The attachments to insert
|
||||
*/
|
||||
public spliceAttachments(
|
||||
start: number,
|
||||
deleteCount: number,
|
||||
...attachments: RestOrArray<APIAttachment | AttachmentBuilder | ((builder: AttachmentBuilder) => AttachmentBuilder)>
|
||||
): this {
|
||||
const resolved = normalizeArray(attachments).map((attachment) => resolveBuilder(attachment, AttachmentBuilder));
|
||||
this.data.attachments.splice(start, deleteCount, ...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flags for this message.
|
||||
*/
|
||||
public setFlags(flags: MessageFlags): this {
|
||||
this.data.flags = flags;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the flags for this message.
|
||||
*/
|
||||
public clearFlags(): this {
|
||||
this.data.flags = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `enforce_nonce` for this message.
|
||||
*/
|
||||
public setEnforceNonce(enforceNonce = true): this {
|
||||
this.data.enforce_nonce = enforceNonce;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the poll for this message.
|
||||
*
|
||||
* @param poll - The poll to set
|
||||
*/
|
||||
public setPoll(poll: APIPoll | PollBuilder | ((builder: PollBuilder) => PollBuilder)): this {
|
||||
this.data.poll = resolveBuilder(poll, PollBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the poll for this message (and creates it if it doesn't exist)
|
||||
*
|
||||
* @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());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the poll for this message.
|
||||
*/
|
||||
public clearPoll(): this {
|
||||
this.data.poll = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||
*
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public toJSON(validationOverride?: boolean): RESTPostAPIChannelMessageJSONBody {
|
||||
const { poll, allowed_mentions, attachments, embeds, components, message_reference, ...rest } = this.data;
|
||||
|
||||
const data = {
|
||||
...structuredClone(rest),
|
||||
// Wherever we pass false, it's covered by the messagePredicate already
|
||||
poll: this.data.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)),
|
||||
// Here, the messagePredicate does specific constraints rather than using the componentPredicate
|
||||
components: this.data.components?.map((component) => component.toJSON(validationOverride)),
|
||||
message_reference: message_reference?.toJSON(false),
|
||||
};
|
||||
|
||||
validate(messagePredicate, data, validationOverride);
|
||||
|
||||
return data as RESTPostAPIChannelMessageJSONBody;
|
||||
}
|
||||
}
|
||||
102
packages/builders/src/messages/MessageReference.ts
Normal file
102
packages/builders/src/messages/MessageReference.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { APIMessageReference, MessageReferenceType, 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>;
|
||||
|
||||
/**
|
||||
* Creates new allowed mention builder from API data.
|
||||
*
|
||||
* @param data - The API data to create this attachment with
|
||||
*/
|
||||
public constructor(data: Partial<MessageReferenceBuilderData> = {}) {
|
||||
this.data = structuredClone(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the types of message reference this represents
|
||||
*
|
||||
* @param type - The type of message reference
|
||||
*/
|
||||
public setType(type: MessageReferenceType): this {
|
||||
this.data.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the type of message reference this represents
|
||||
*/
|
||||
public clearType(): this {
|
||||
this.data.type = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id of the message being referenced
|
||||
*
|
||||
* @param messageId - The id of the message being referenced
|
||||
*/
|
||||
public setMessageId(messageId: Snowflake): this {
|
||||
this.data.message_id = messageId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id of the channel being referenced
|
||||
*
|
||||
* @param channelId - The id of the channel being referenced
|
||||
*/
|
||||
public setChannelId(channelId: Snowflake): this {
|
||||
this.data.channel_id = channelId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the id of the channel being referenced
|
||||
*/
|
||||
public clearChannelId(): this {
|
||||
this.data.channel_id = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id of the guild being referenced
|
||||
*
|
||||
* @param guildId - The id of the guild being referenced
|
||||
*/
|
||||
public setGuildId(guildId: Snowflake): this {
|
||||
this.data.guild_id = guildId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the id of the guild being referenced
|
||||
*/
|
||||
public clearGuildId(): this {
|
||||
this.data.guild_id = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||
*
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
*/
|
||||
public toJSON(validationOverride?: boolean): MessageReferenceBuilderData {
|
||||
const clone = structuredClone(this.data);
|
||||
validate(messageReferencePredicate, clone, validationOverride);
|
||||
|
||||
return clone as MessageReferenceBuilderData;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user