feat: components v2 in builders (#10788)

* feat: thumbnail component

* chore: just a temp file to track remaining components

* feat: file component

* feat: section component

* feat: text display component

* chore: bump alpha version of dtypes

* chore: simplify ComponentBuilder base type

* feat: MediaGallery

* feat: Section builder

* chore: tests for sections

* chore: forgot you

* chore: docs

* fix: missing comma

* fix: my bad

* feat: container builder

* chore: requested changes

* chore: missed u

* chore: type tests

* chore: setId/clearId

* chore: apply suggestions from code review

* chore: unify pick

* chore: some requested changes

* chore: tests and small fixes

* chore: added tests that need fixing

* fix: tests

* chore: cleanup on isle protected

* docs: remove locale

* chore: types for new message builder

* chore: fix tests

* chore: attempt 1 at message builder assertions

* chore: apply suggestions

* Update packages/builders/src/messages/Assertions.ts

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* Update packages/builders/src/components/v2/Thumbnail.ts

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* fix: tests

* chore: fmt

* Apply suggestions from code review

Co-authored-by: Denis-Adrian Cristea <didinele.dev@gmail.com>

* chore: fix pnpm lockfile revert

---------

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Denis-Adrian Cristea <didinele.dev@gmail.com>
This commit is contained in:
Vlad Frangu
2025-04-23 20:29:15 +03:00
committed by GitHub
parent 42ce116226
commit abc5d99ce8
31 changed files with 2176 additions and 116 deletions

View File

@@ -47,7 +47,7 @@ export interface ActionRowBuilderData
* @typeParam ComponentType - The types of components this action row holds
*/
export class ActionRowBuilder extends ComponentBuilder<APIActionRowComponent<APIComponentInActionRow>> {
private readonly data: ActionRowBuilderData;
protected readonly data: ActionRowBuilderData;
/**
* The components within this action row.

View File

@@ -1,17 +1,38 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIActionRowComponent, APIComponentInActionRow } from 'discord-api-types/v10';
import type { APIBaseComponent, ComponentType } from 'discord-api-types/v10';
/**
* Any action row component data represented as an object.
*/
export type AnyAPIActionRowComponent = APIActionRowComponent<APIComponentInActionRow> | APIComponentInActionRow;
export interface ComponentBuilderBaseData {
id?: number | undefined;
}
/**
* The base component builder that contains common symbols for all sorts of components.
*
* @typeParam Component - The type of API data that is stored within the builder
*/
export abstract class ComponentBuilder<Component extends AnyAPIActionRowComponent> implements JSONEncodable<Component> {
export abstract class ComponentBuilder<Component extends APIBaseComponent<ComponentType>>
implements JSONEncodable<Component>
{
protected abstract readonly data: ComponentBuilderBaseData;
/**
* Sets the id of this component.
*
* @param id - The id to use
*/
public setId(id: number) {
this.data.id = id;
return this;
}
/**
* Clears the id of this component, defaulting to a default incremented id.
*/
public clearId() {
this.data.id = undefined;
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*

View File

@@ -1,7 +1,12 @@
import type { APIButtonComponent, APIMessageComponent, APIModalComponent } from 'discord-api-types/v10';
import type {
APIBaseComponent,
APIButtonComponent,
APIMessageComponent,
APIModalComponent,
APISectionAccessoryComponent,
} from 'discord-api-types/v10';
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
import { ActionRowBuilder } from './ActionRow.js';
import type { AnyAPIActionRowComponent } from './Component.js';
import { ComponentBuilder } from './Component.js';
import type { BaseButtonBuilder } from './button/Button.js';
import {
@@ -18,11 +23,33 @@ import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
import { TextInputBuilder } from './textInput/TextInput.js';
import { ContainerBuilder } from './v2/Container.js';
import { FileBuilder } from './v2/File.js';
import { MediaGalleryBuilder } from './v2/MediaGallery.js';
import { SectionBuilder } from './v2/Section.js';
import { SeparatorBuilder } from './v2/Separator.js';
import { TextDisplayBuilder } from './v2/TextDisplay.js';
import { ThumbnailBuilder } from './v2/Thumbnail.js';
/**
* The builders that may be used as top-level components on messages
*/
export type MessageTopLevelComponentBuilder =
| ActionRowBuilder
| ContainerBuilder
| FileBuilder
| MediaGalleryBuilder
| SectionBuilder
| SeparatorBuilder
| TextDisplayBuilder;
/**
* The builders that may be used for messages.
*/
export type MessageComponentBuilder = ActionRowBuilder | MessageActionRowComponentBuilder;
export type MessageComponentBuilder =
| MessageActionRowComponentBuilder
| MessageTopLevelComponentBuilder
| ThumbnailBuilder;
/**
* The builders that may be used for modals.
@@ -97,6 +124,34 @@ export interface MappedComponentTypes {
* The channel select component type is associated with a {@link ChannelSelectMenuBuilder}.
*/
[ComponentType.ChannelSelect]: ChannelSelectMenuBuilder;
/**
* The thumbnail component type is associated with a {@link ThumbnailBuilder}.
*/
[ComponentType.Thumbnail]: ThumbnailBuilder;
/**
* The file component type is associated with a {@link FileBuilder}.
*/
[ComponentType.File]: FileBuilder;
/**
* The separator component type is associated with a {@link SeparatorBuilder}.
*/
[ComponentType.Separator]: SeparatorBuilder;
/**
* The text display component type is associated with a {@link TextDisplayBuilder}.
*/
[ComponentType.TextDisplay]: TextDisplayBuilder;
/**
* The media gallery component type is associated with a {@link MediaGalleryBuilder}.
*/
[ComponentType.MediaGallery]: MediaGalleryBuilder;
/**
* The section component type is associated with a {@link SectionBuilder}.
*/
[ComponentType.Section]: SectionBuilder;
/**
* The container component type is associated with a {@link ContainerBuilder}.
*/
[ComponentType.Container]: ContainerBuilder;
}
/**
@@ -122,7 +177,7 @@ export function createComponentBuilder<ComponentBuilder extends MessageComponent
export function createComponentBuilder(
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
): ComponentBuilder<AnyAPIActionRowComponent> {
): ComponentBuilder<APIBaseComponent<ComponentType>> {
if (data instanceof ComponentBuilder) {
return data;
}
@@ -144,36 +199,20 @@ export function createComponentBuilder(
return new MentionableSelectMenuBuilder(data);
case ComponentType.ChannelSelect:
return new ChannelSelectMenuBuilder(data);
// Will be handled later
case ComponentType.Section: {
throw new Error('Not implemented yet: ComponentType.Section case');
}
case ComponentType.TextDisplay: {
throw new Error('Not implemented yet: ComponentType.TextDisplay case');
}
case ComponentType.Thumbnail: {
throw new Error('Not implemented yet: ComponentType.Thumbnail case');
}
case ComponentType.MediaGallery: {
throw new Error('Not implemented yet: ComponentType.MediaGallery case');
}
case ComponentType.File: {
throw new Error('Not implemented yet: ComponentType.File case');
}
case ComponentType.Separator: {
throw new Error('Not implemented yet: ComponentType.Separator case');
}
case ComponentType.Container: {
throw new Error('Not implemented yet: ComponentType.Container case');
}
case ComponentType.Thumbnail:
return new ThumbnailBuilder(data);
case ComponentType.File:
return new FileBuilder(data);
case ComponentType.Separator:
return new SeparatorBuilder(data);
case ComponentType.TextDisplay:
return new TextDisplayBuilder(data);
case ComponentType.MediaGallery:
return new MediaGalleryBuilder(data);
case ComponentType.Section:
return new SectionBuilder(data);
case ComponentType.Container:
return new ContainerBuilder(data);
default:
// @ts-expect-error This case can still occur if we get a newer unsupported component type
throw new Error(`Cannot properly serialize component type: ${data.type}`);
@@ -199,3 +238,15 @@ function createButtonBuilder(data: APIButtonComponent): ButtonBuilder {
throw new Error(`Cannot properly serialize button with style: ${data.style}`);
}
}
export function resolveAccessoryComponent(component: APISectionAccessoryComponent) {
switch (component.type) {
case ComponentType.Button:
return createButtonBuilder(component);
case ComponentType.Thumbnail:
return new ThumbnailBuilder(component);
default:
// @ts-expect-error This case can still occur if we get a newer unsupported component type
throw new Error(`Cannot properly serialize section accessory component: ${component.type}`);
}
}

View File

@@ -11,8 +11,8 @@ export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
extends ComponentBuilder<Data>
implements JSONEncodable<APISelectMenuComponent>
{
protected abstract readonly data: Partial<
Pick<Data, 'custom_id' | 'disabled' | 'max_values' | 'min_values' | 'placeholder'>
protected abstract override readonly data: Partial<
Pick<Data, 'custom_id' | 'disabled' | 'id' | 'max_values' | 'min_values' | 'placeholder'>
>;
/**

View File

@@ -7,7 +7,7 @@ import { textInputPredicate } from './Assertions.js';
* A builder that creates API-compatible JSON data for text inputs.
*/
export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
private readonly data: Partial<APITextInputComponent>;
protected readonly data: Partial<APITextInputComponent>;
/**
* Creates a new text input from API data.

View File

@@ -0,0 +1,78 @@
import { ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
import { z } from 'zod';
import { refineURLPredicate } from '../../Assertions.js';
import { actionRowPredicate } from '../Assertions.js';
const unfurledMediaItemPredicate = z.object({
url: z
.string()
.url()
.refine(refineURLPredicate(['http:', 'https:', 'attachment:']), {
message: 'Invalid protocol for media URL. Must be http:, https:, or attachment:',
}),
});
export const thumbnailPredicate = z.object({
media: unfurledMediaItemPredicate,
description: z.string().min(1).max(1_024).nullish(),
spoiler: z.boolean().optional(),
});
const unfurledMediaItemAttachmentOnlyPredicate = z.object({
url: z
.string()
.url()
.refine(refineURLPredicate(['attachment:']), {
message: 'Invalid protocol for file URL. Must be attachment:',
}),
});
export const filePredicate = z.object({
file: unfurledMediaItemAttachmentOnlyPredicate,
spoiler: z.boolean().optional(),
});
export const separatorPredicate = z.object({
divider: z.boolean().optional(),
spacing: z.nativeEnum(SeparatorSpacingSize).optional(),
});
export const textDisplayPredicate = z.object({
content: z.string().min(1).max(4_000),
});
export const mediaGalleryItemPredicate = z.object({
media: unfurledMediaItemPredicate,
description: z.string().min(1).max(1_024).nullish(),
spoiler: z.boolean().optional(),
});
export const mediaGalleryPredicate = z.object({
items: z.array(mediaGalleryItemPredicate).min(1).max(10),
});
export const sectionPredicate = z.object({
components: z.array(textDisplayPredicate).min(1).max(3),
accessory: z.union([
z.object({ type: z.literal(ComponentType.Button) }),
z.object({ type: z.literal(ComponentType.Thumbnail) }),
]),
});
export const containerPredicate = z.object({
components: z
.array(
z.union([
actionRowPredicate,
filePredicate,
mediaGalleryPredicate,
sectionPredicate,
separatorPredicate,
textDisplayPredicate,
]),
)
.min(1)
.max(10),
spoiler: z.boolean().optional(),
accent_color: z.number().int().min(0).max(0xffffff).nullish(),
});

View File

@@ -0,0 +1,232 @@
import type {
APIActionRowComponent,
APIFileComponent,
APITextDisplayComponent,
APIContainerComponent,
APIComponentInContainer,
APIMediaGalleryComponent,
APISectionComponent,
} from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import type { APIComponentInMessageActionRow, APISeparatorComponent } from 'discord-api-types/v9';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray';
import { resolveBuilder } from '../../util/resolveBuilder';
import { validate } from '../../util/validation';
import { ActionRowBuilder } from '../ActionRow.js';
import { ComponentBuilder } from '../Component.js';
import { createComponentBuilder } from '../Components';
import { containerPredicate } from './Assertions';
import { FileBuilder } from './File.js';
import { MediaGalleryBuilder } from './MediaGallery';
import { SectionBuilder } from './Section';
import { SeparatorBuilder } from './Separator.js';
import { TextDisplayBuilder } from './TextDisplay';
export type ContainerComponentBuilders =
| ActionRowBuilder
| FileBuilder
| MediaGalleryBuilder
| SectionBuilder
| SeparatorBuilder
| TextDisplayBuilder;
export interface ContainerBuilderData extends Partial<Omit<APIContainerComponent, 'components'>> {
components: ContainerComponentBuilders[];
}
export class ContainerBuilder extends ComponentBuilder<APIContainerComponent> {
protected readonly data: ContainerBuilderData;
public constructor({ components = [], ...rest }: Partial<APIContainerComponent> = {}) {
super();
this.data = {
...structuredClone(rest),
components: components.map((component) => createComponentBuilder(component)),
type: ComponentType.Container,
};
}
/**
* Sets the accent color of this container.
*
* @param color - The color to use
*/
public setAccentColor(color: number) {
this.data.accent_color = color;
return this;
}
/**
* Clears the accent color of this container.
*/
public clearAccentColor() {
this.data.accent_color = undefined;
return this;
}
/**
* Sets the spoiler status of this container.
*
* @param spoiler - The spoiler status to use
*/
public setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Adds action row components to this container.
*
* @param input - The action row to add
*/
public addActionRowComponents(
...input: RestOrArray<
| ActionRowBuilder
| APIActionRowComponent<APIComponentInMessageActionRow>
| ((builder: ActionRowBuilder) => ActionRowBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, ActionRowBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds file components to this container.
*
* @param input - The file components to add
*/
public addFileComponents(
...input: RestOrArray<APIFileComponent | FileBuilder | ((builder: FileBuilder) => FileBuilder)>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, FileBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds media gallery components to this container.
*
* @param input - The media gallery components to add
*/
public addMediaGalleryComponents(
...input: RestOrArray<
APIMediaGalleryComponent | MediaGalleryBuilder | ((builder: MediaGalleryBuilder) => MediaGalleryBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, MediaGalleryBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds section components to this container.
*
* @param input - The section components to add
*/
public addSectionComponents(
...input: RestOrArray<APISectionComponent | SectionBuilder | ((builder: SectionBuilder) => SectionBuilder)>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, SectionBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds separator components to this container.
*
* @param input - The separator components to add
*/
public addSeparatorComponents(
...input: RestOrArray<APISeparatorComponent | SeparatorBuilder | ((builder: SeparatorBuilder) => SeparatorBuilder)>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, SeparatorBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds text display components to this container.
*
* @param input - The text display components to add
*/
public addTextDisplayComponents(
...input: RestOrArray<
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, TextDisplayBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Removes, replaces, or inserts components for this container
*
* @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 container.
* @example
* Remove the first component:
* ```ts
* container.spliceComponents(0, 1);
* ```
* @example
* Remove the first n components:
* ```ts
* const n = 4;
* container.spliceComponents(0, n);
* ```
* @example
* Remove the last component:
* ```ts
* container.spliceComponents(-1, 1);
* ```
* @param index - The index to start at
* @param deleteCount - The number of components to remove
* @param components - The replacing component objects
*/
public spliceComponents(
index: number,
deleteCount: number,
...components: RestOrArray<APIComponentInContainer | ContainerComponentBuilders>
): this {
const normalized = normalizeArray(components);
const resolved = normalized.map((component) =>
component instanceof ComponentBuilder ? component : createComponentBuilder(component),
);
this.data.components.splice(index, deleteCount, ...resolved);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIContainerComponent {
const { components, ...rest } = this.data;
const data = {
...structuredClone(rest),
components: components.map((component) => component.toJSON(false)),
};
validate(containerPredicate, data, validationOverride);
return data as APIContainerComponent;
}
}

View File

@@ -0,0 +1,72 @@
import { ComponentType, type APIFileComponent } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { ComponentBuilder } from '../Component.js';
import { filePredicate } from './Assertions.js';
export class FileBuilder extends ComponentBuilder<APIFileComponent> {
protected readonly data: Partial<APIFileComponent>;
/**
* Creates a new file from API data.
*
* @param data - The API data to create this file with
* @example
* Creating a file from an API data object:
* ```ts
* const file = new FileBuilder({
* spoiler: true,
* file: {
* url: 'attachment://file.png',
* },
* });
* ```
* @example
* Creating a file using setters and API data:
* ```ts
* const file = new FileBuilder({
* file: {
* url: 'attachment://image.jpg',
* },
* })
* .setSpoiler(false);
* ```
*/
public constructor(data: Partial<APIFileComponent> = {}) {
super();
this.data = {
...structuredClone(data),
file: data.file ? { url: data.file.url } : undefined,
type: ComponentType.File,
};
}
/**
* Sets the spoiler status of this file.
*
* @param spoiler - The spoiler status to use
*/
public setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Sets the media URL of this file.
*
* @param url - The URL to use
*/
public setURL(url: string) {
this.data.file = { url };
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIFileComponent {
const clone = structuredClone(this.data);
validate(filePredicate, clone, validationOverride);
return clone as APIFileComponent;
}
}

View File

@@ -0,0 +1,118 @@
import { type APIMediaGalleryItem, type APIMediaGalleryComponent, ComponentType } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
import { resolveBuilder } from '../../util/resolveBuilder.js';
import { validate } from '../../util/validation.js';
import { ComponentBuilder } from '../Component.js';
import { mediaGalleryPredicate } from './Assertions.js';
import { MediaGalleryItemBuilder } from './MediaGalleryItem.js';
export interface MediaGalleryBuilderData extends Partial<Omit<APIMediaGalleryComponent, 'items'>> {
items: MediaGalleryItemBuilder[];
}
export class MediaGalleryBuilder extends ComponentBuilder<APIMediaGalleryComponent> {
protected readonly data: MediaGalleryBuilderData;
/**
* Creates a new media gallery from API data.
*
* @param data - The API data to create this container with
* @example
* Creating a media gallery from an API data object:
* ```ts
* const mediaGallery = new MediaGalleryBuilder({
* items: [
* {
* description: "Some text here",
* media: {
* url: 'https://cdn.discordapp.com/embed/avatars/2.png',
* },
* },
* ],
* });
* ```
* @example
* Creating a media gallery using setters and API data:
* ```ts
* const mediaGallery = new MediaGalleryBuilder({
* items: [
* {
* description: "alt text",
* media: {
* url: 'https://cdn.discordapp.com/embed/avatars/5.png',
* },
* },
* ],
* })
* .addItems(item2, item3);
* ```
*/
public constructor(data: Partial<APIMediaGalleryComponent> = {}) {
super();
this.data = {
items: data?.items?.map((item) => new MediaGalleryItemBuilder(item)) ?? [],
type: ComponentType.MediaGallery,
};
}
/**
* The items in this media gallery.
*/
public get items(): readonly MediaGalleryItemBuilder[] {
return this.data.items;
}
/**
* Adds a media gallery item to this media gallery.
*
* @param input - The items to add
*/
public addItems(
...input: RestOrArray<
APIMediaGalleryItem | MediaGalleryItemBuilder | ((builder: MediaGalleryItemBuilder) => MediaGalleryItemBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((item) => resolveBuilder(item, MediaGalleryItemBuilder));
this.data.items.push(...resolved);
return this;
}
/**
* Removes, replaces, or inserts media gallery items for this media gallery.
*
* @param index - The index to start removing, replacing or inserting items
* @param deleteCount - The amount of items to remove
* @param items - The items to insert
*/
public spliceItems(
index: number,
deleteCount: number,
...items: RestOrArray<
APIMediaGalleryItem | MediaGalleryItemBuilder | ((builder: MediaGalleryItemBuilder) => MediaGalleryItemBuilder)
>
) {
const normalized = normalizeArray(items);
const resolved = normalized.map((item) => resolveBuilder(item, MediaGalleryItemBuilder));
this.data.items.splice(index, deleteCount, ...resolved);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIMediaGalleryComponent {
const { items, ...rest } = this.data;
const data = {
...structuredClone(rest),
items: items.map((item) => item.toJSON(false)),
};
validate(mediaGalleryPredicate, data, validationOverride);
return data as APIMediaGalleryComponent;
}
}

View File

@@ -0,0 +1,87 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIMediaGalleryItem } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { mediaGalleryItemPredicate } from './Assertions.js';
export class MediaGalleryItemBuilder implements JSONEncodable<APIMediaGalleryItem> {
private readonly data: Partial<APIMediaGalleryItem>;
/**
* Creates a new media gallery item from API data.
*
* @param data - The API data to create this media gallery item with
* @example
* Creating a media gallery item from an API data object:
* ```ts
* const item = new MediaGalleryItemBuilder({
* description: "Some text here",
* media: {
* url: 'https://cdn.discordapp.com/embed/avatars/2.png',
* },
* });
* ```
* @example
* Creating a media gallery item using setters and API data:
* ```ts
* const item = new MediaGalleryItemBuilder({
* media: {
* url: 'https://cdn.discordapp.com/embed/avatars/5.png',
* },
* })
* .setDescription("alt text");
* ```
*/
public constructor(data: Partial<APIMediaGalleryItem> = {}) {
this.data = {
...structuredClone(data),
};
}
/**
* Sets the source URL of this media gallery item.
*
* @param url - The URL to use
*/
public setURL(url: string) {
this.data.media = { url };
return this;
}
/**
* Sets the description of this thumbnail.
*
* @param description - The description to use
*/
public setDescription(description: string) {
this.data.description = description;
return this;
}
/**
* Clears the description of this thumbnail.
*/
public clearDescription() {
this.data.description = undefined;
return this;
}
/**
* Sets the spoiler status of this thumbnail.
*
* @param spoiler - The spoiler status to use
*/
public setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Transforms this object to its JSON format
*/
public toJSON(validationOverride?: boolean): APIMediaGalleryItem {
const clone = structuredClone(this.data);
validate(mediaGalleryItemPredicate, clone, validationOverride);
return clone as APIMediaGalleryItem;
}
}

View File

@@ -0,0 +1,257 @@
import type {
APITextDisplayComponent,
APISectionComponent,
APIButtonComponentWithCustomId,
APIThumbnailComponent,
APIButtonComponentWithSKUId,
APIButtonComponentWithURL,
ButtonStyle,
} from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
import { resolveBuilder } from '../../util/resolveBuilder.js';
import { validate } from '../../util/validation.js';
import { ComponentBuilder } from '../Component.js';
import { resolveAccessoryComponent, type ButtonBuilder } from '../Components.js';
import {
DangerButtonBuilder,
PrimaryButtonBuilder,
SecondaryButtonBuilder,
SuccessButtonBuilder,
} from '../button/CustomIdButton.js';
import { LinkButtonBuilder } from '../button/LinkButton.js';
import { PremiumButtonBuilder } from '../button/PremiumButton.js';
import { sectionPredicate } from './Assertions.js';
import { TextDisplayBuilder } from './TextDisplay.js';
import { ThumbnailBuilder } from './Thumbnail.js';
export type SectionBuilderAccessory = ButtonBuilder | ThumbnailBuilder;
export interface SectionBuilderData extends Partial<Omit<APISectionComponent, 'accessory' | 'components'>> {
accessory?: SectionBuilderAccessory;
components: TextDisplayBuilder[];
}
export class SectionBuilder extends ComponentBuilder<APISectionComponent> {
protected readonly data: SectionBuilderData;
public get components(): readonly TextDisplayBuilder[] {
return this.data.components;
}
/**
* Creates a new section from API data.
*
* @param data - The API data to create this section with
* @example
* Creating a section from an API data object:
* ```ts
* const section = new SectionBuilder({
* components: [
* {
* content: "Some text here",
* type: ComponentType.TextDisplay,
* },
* ],
* accessory: {
* media: {
* url: 'https://cdn.discordapp.com/embed/avatars/3.png',
* },
* }
* });
* ```
* @example
* Creating a section using setters and API data:
* ```ts
* const section = new SectionBuilder({
* components: [
* {
* content: "# Heading",
* type: ComponentType.TextDisplay,
* },
* ],
* })
* .setPrimaryButtonAccessory(button);
* ```
*/
public constructor(data: Partial<APISectionComponent> = {}) {
super();
const { components = [], accessory, ...rest } = data;
this.data = {
...structuredClone(rest),
accessory: accessory ? resolveAccessoryComponent(accessory) : undefined,
components: components.map((component) => new TextDisplayBuilder(component)),
type: ComponentType.Section,
};
}
/**
* Adds text display components to this section.
*
* @param input - The text display components to add
*/
public addTextDisplayComponents(
...input: RestOrArray<
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((component) => resolveBuilder(component, TextDisplayBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Sets a primary button component to be the accessory of this section.
*
* @param input - The button to set as the accessory
*/
public setPrimaryButtonAccessory(
input:
| PrimaryButtonBuilder
| ((builder: PrimaryButtonBuilder) => PrimaryButtonBuilder)
| (APIButtonComponentWithCustomId & { style: ButtonStyle.Primary }),
): this {
const builder = resolveBuilder(input, PrimaryButtonBuilder);
this.data.accessory = builder;
return this;
}
/**
* Sets a secondary button component to be the accessory of this section.
*
* @param input - The button to set as the accessory
*/
public setSecondaryButtonAccessory(
input:
| SecondaryButtonBuilder
| ((builder: SecondaryButtonBuilder) => SecondaryButtonBuilder)
| (APIButtonComponentWithCustomId & { style: ButtonStyle.Secondary }),
): this {
const builder = resolveBuilder(input, SecondaryButtonBuilder);
this.data.accessory = builder;
return this;
}
/**
* Sets a success button component to be the accessory of this section.
*
* @param input - The button to set as the accessory
*/
public setSuccessButtonAccessory(
input:
| SuccessButtonBuilder
| ((builder: SuccessButtonBuilder) => SuccessButtonBuilder)
| (APIButtonComponentWithCustomId & { style: ButtonStyle.Success }),
): this {
const builder = resolveBuilder(input, SuccessButtonBuilder);
this.data.accessory = builder;
return this;
}
/**
* Sets a danger button component to be the accessory of this section.
*
* @param input - The button to set as the accessory
*/
public setDangerButtonAccessory(
input:
| DangerButtonBuilder
| ((builder: DangerButtonBuilder) => DangerButtonBuilder)
| (APIButtonComponentWithCustomId & { style: ButtonStyle.Danger }),
): this {
const builder = resolveBuilder(input, DangerButtonBuilder);
this.data.accessory = builder;
return this;
}
/**
* Sets a SKU id button component to be the accessory of this section.
*
* @param input - The button to set as the accessory
*/
public setPremiumButtonAccessory(
input:
| APIButtonComponentWithSKUId
| PremiumButtonBuilder
| ((builder: PremiumButtonBuilder) => PremiumButtonBuilder),
): this {
const builder = resolveBuilder(input, PremiumButtonBuilder);
this.data.accessory = builder;
return this;
}
/**
* Sets a URL button component to be the accessory of this section.
*
* @param input - The button to set as the accessory
*/
public setLinkButtonAccessory(
input: APIButtonComponentWithURL | LinkButtonBuilder | ((builder: LinkButtonBuilder) => LinkButtonBuilder),
): this {
const builder = resolveBuilder(input, LinkButtonBuilder);
this.data.accessory = builder;
return this;
}
/**
* Sets a thumbnail component to be the accessory of this section.
*
* @param input - The thumbnail to set as the accessory
*/
public setThumbnailAccessory(
input: APIThumbnailComponent | ThumbnailBuilder | ((builder: ThumbnailBuilder) => ThumbnailBuilder),
): this {
const builder = resolveBuilder(input, ThumbnailBuilder);
this.data.accessory = builder;
return this;
}
/**
* Removes, replaces, or inserts text display components for this section.
*
* @param index - The index to start removing, replacing or inserting text display components
* @param deleteCount - The amount of text display components to remove
* @param components - The text display components to insert
*/
public spliceTextDisplayComponents(
index: number,
deleteCount: number,
...components: RestOrArray<
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
>
): this {
const normalized = normalizeArray(components);
const resolved = normalized.map((component) => resolveBuilder(component, TextDisplayBuilder));
this.data.components.splice(index, deleteCount, ...resolved);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APISectionComponent {
const { components, accessory, ...rest } = this.data;
const data = {
...structuredClone(rest),
components: components.map((component) => component.toJSON(false)),
accessory: accessory?.toJSON(validationOverride),
};
validate(sectionPredicate, data, validationOverride);
return data as APISectionComponent;
}
}

View File

@@ -0,0 +1,76 @@
import type { SeparatorSpacingSize, APISeparatorComponent } from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { ComponentBuilder } from '../Component.js';
import { separatorPredicate } from './Assertions.js';
export class SeparatorBuilder extends ComponentBuilder<APISeparatorComponent> {
protected readonly data: Partial<APISeparatorComponent>;
/**
* Creates a new separator from API data.
*
* @param data - The API data to create this separator with
* @example
* Creating a separator from an API data object:
* ```ts
* const separator = new SeparatorBuilder({
* spacing: SeparatorSpacingSize.Small,
* divider: true,
* });
* ```
* @example
* Creating a separator using setters and API data:
* ```ts
* const separator = new SeparatorBuilder({
* spacing: SeparatorSpacingSize.Large,
* })
* .setDivider(false);
* ```
*/
public constructor(data: Partial<APISeparatorComponent> = {}) {
super();
this.data = {
...structuredClone(data),
type: ComponentType.Separator,
};
}
/**
* Sets whether this separator should show a divider line.
*
* @param divider - Whether to show a divider line
*/
public setDivider(divider = true) {
this.data.divider = divider;
return this;
}
/**
* Sets the spacing of this separator.
*
* @param spacing - The spacing to use
*/
public setSpacing(spacing: SeparatorSpacingSize) {
this.data.spacing = spacing;
return this;
}
/**
* Clears the spacing of this separator.
*/
public clearSpacing() {
this.data.spacing = undefined;
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APISeparatorComponent {
const clone = structuredClone(this.data);
validate(separatorPredicate, clone, validationOverride);
return clone as APISeparatorComponent;
}
}

View File

@@ -0,0 +1,57 @@
import type { APITextDisplayComponent } from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { ComponentBuilder } from '../Component.js';
import { textDisplayPredicate } from './Assertions.js';
export class TextDisplayBuilder extends ComponentBuilder<APITextDisplayComponent> {
protected readonly data: Partial<APITextDisplayComponent>;
/**
* Creates a new text display from API data.
*
* @param data - The API data to create this text display with
* @example
* Creating a text display from an API data object:
* ```ts
* const textDisplay = new TextDisplayBuilder({
* content: 'some text',
* });
* ```
* @example
* Creating a text display using setters and API data:
* ```ts
* const textDisplay = new TextDisplayBuilder({
* content: 'old text',
* })
* .setContent('new text');
* ```
*/
public constructor(data: Partial<APITextDisplayComponent> = {}) {
super();
this.data = {
...structuredClone(data),
type: ComponentType.TextDisplay,
};
}
/**
* Sets the text of this text display.
*
* @param content - The text to use
*/
public setContent(content: string) {
this.data.content = content;
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APITextDisplayComponent {
const clone = structuredClone(this.data);
validate(textDisplayPredicate, clone, validationOverride);
return clone as APITextDisplayComponent;
}
}

View File

@@ -0,0 +1,91 @@
import type { APIThumbnailComponent } from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { ComponentBuilder } from '../Component.js';
import { thumbnailPredicate } from './Assertions.js';
export class ThumbnailBuilder extends ComponentBuilder<APIThumbnailComponent> {
protected readonly data: Partial<APIThumbnailComponent>;
/**
* Creates a new thumbnail from API data.
*
* @param data - The API data to create this thumbnail with
* @example
* Creating a thumbnail from an API data object:
* ```ts
* const thumbnail = new ThumbnailBuilder({
* description: 'some text',
* media: {
* url: 'https://cdn.discordapp.com/embed/avatars/4.png',
* },
* });
* ```
* @example
* Creating a thumbnail using setters and API data:
* ```ts
* const thumbnail = new ThumbnailBuilder({
* media: {
* url: 'attachment://image.png',
* },
* })
* .setDescription('alt text');
* ```
*/
public constructor(data: Partial<APIThumbnailComponent> = {}) {
super();
this.data = {
...structuredClone(data),
media: data.media ? { url: data.media.url } : undefined,
type: ComponentType.Thumbnail,
};
}
/**
* Sets the description of this thumbnail.
*
* @param description - The description to use
*/
public setDescription(description: string) {
this.data.description = description;
return this;
}
/**
* Clears the description of this thumbnail.
*/
public clearDescription() {
this.data.description = undefined;
return this;
}
/**
* Sets the spoiler status of this thumbnail.
*
* @param spoiler - The spoiler status to use
*/
public setSpoiler(spoiler = true) {
this.data.spoiler = spoiler;
return this;
}
/**
* Sets the media URL of this thumbnail.
*
* @param url - The URL to use
*/
public setURL(url: string) {
this.data.media = { url };
return this;
}
/**
* {@inheritdoc ComponentBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): APIThumbnailComponent {
const clone = structuredClone(this.data);
validate(thumbnailPredicate, clone, validationOverride);
return clone as APIThumbnailComponent;
}
}

View File

@@ -20,6 +20,15 @@ export * from './components/Assertions.js';
export * from './components/Component.js';
export * from './components/Components.js';
export * from './components/v2/Assertions.js';
export * from './components/v2/File.js';
export * from './components/v2/MediaGallery.js';
export * from './components/v2/MediaGalleryItem.js';
export * from './components/v2/Section.js';
export * from './components/v2/Separator.js';
export * from './components/v2/TextDisplay.js';
export * from './components/v2/Thumbnail.js';
export * from './interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.js';
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.js';

View File

@@ -1,4 +1,4 @@
import { AllowedMentionsTypes, ComponentType, MessageReferenceType } from 'discord-api-types/v10';
import { AllowedMentionsTypes, ComponentType, MessageFlags, MessageReferenceType } from 'discord-api-types/v10';
import { z } from 'zod';
import { embedPredicate } from './embed/Assertions.js';
import { pollPredicate } from './poll/Assertions.js';
@@ -27,40 +27,49 @@ export const messageReferencePredicate = z.object({
type: z.nativeEnum(MessageReferenceType).optional(),
});
export const messagePredicate = z
.object({
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.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(),
});
const messageNoComponentsV2Predicate = baseMessagePredicate
.extend({
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(),
components: basicActionRowPredicate.array().max(5).optional(),
flags: z
.number()
.optional()
.refine((flags) => {
// If we have flags, ensure we don't have the ComponentsV2 flag
if (flags) {
return (flags & MessageFlags.IsComponentsV2) === 0;
}
return true;
}),
})
.refine(
(data) =>
@@ -70,5 +79,37 @@ export const messagePredicate = z
(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' },
{ message: 'Messages must have content, embeds, a poll, attachments, components or stickers' },
);
const allTopLevelComponentsPredicate = z
.union([
basicActionRowPredicate,
z.object({
type: z.union([
// Components v2
z.literal(ComponentType.Container),
z.literal(ComponentType.File),
z.literal(ComponentType.MediaGallery),
z.literal(ComponentType.Section),
z.literal(ComponentType.Separator),
z.literal(ComponentType.TextDisplay),
z.literal(ComponentType.Thumbnail),
]),
}),
])
.array()
.min(1)
.max(10);
const messageComponentsV2Predicate = baseMessagePredicate.extend({
components: allTopLevelComponentsPredicate,
flags: z.number().refine((flags) => (flags & MessageFlags.IsComponentsV2) === MessageFlags.IsComponentsV2),
// 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]);

View File

@@ -10,9 +10,24 @@ import type {
RESTPostAPIChannelMessageJSONBody,
Snowflake,
MessageFlags,
APIComponentInActionRow,
APIContainerComponent,
APIFileComponent,
APIMediaGalleryComponent,
APISectionComponent,
APISeparatorComponent,
APITextDisplayComponent,
APIMessageTopLevelComponent,
} from 'discord-api-types/v10';
import { ActionRowBuilder } from '../components/ActionRow.js';
import { ComponentBuilder } from '../components/Component.js';
import type { MessageTopLevelComponentBuilder } from '../components/Components.js';
import { createComponentBuilder } from '../components/Components.js';
import { ContainerBuilder } from '../components/v2/Container.js';
import { FileBuilder } from '../components/v2/File.js';
import { MediaGalleryBuilder } from '../components/v2/MediaGallery.js';
import { SectionBuilder } from '../components/v2/Section.js';
import { SeparatorBuilder } from '../components/v2/Separator.js';
import { TextDisplayBuilder } from '../components/v2/TextDisplay.js';
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
import { resolveBuilder } from '../util/resolveBuilder.js';
import { validate } from '../util/validation.js';
@@ -32,7 +47,7 @@ export interface MessageBuilderData
> {
allowed_mentions?: AllowedMentionsBuilder;
attachments: AttachmentBuilder[];
components: ActionRowBuilder[];
components: MessageTopLevelComponentBuilder[];
embeds: EmbedBuilder[];
message_reference?: MessageReferenceBuilder;
poll?: PollBuilder;
@@ -54,7 +69,7 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
/**
* Gets the components of this message.
*/
public get components(): readonly ActionRowBuilder[] {
public get components(): readonly MessageTopLevelComponentBuilder[] {
return this.data.components;
}
@@ -77,10 +92,7 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
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 as unknown as APIActionRowComponent<APIComponentInActionRow>),
) ?? [],
components: data.components?.map((component) => createComponentBuilder(component)) ?? [],
message_reference: data.message_reference ? new MessageReferenceBuilder(data.message_reference) : undefined,
};
}
@@ -268,11 +280,11 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
}
/**
* Adds components to this message.
* Adds action row components to this message.
*
* @param components - The components to add
* @param components - The action row components to add
*/
public addComponents(
public addActionRowComponents(
...components: RestOrArray<
| ActionRowBuilder
| APIActionRowComponent<APIComponentInMessageActionRow>
@@ -287,6 +299,110 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
return this;
}
/**
* Adds container components to this message.
*
* @param components - The container components to add
*/
public addContainerComponents(
...components: RestOrArray<
APIContainerComponent | ContainerBuilder | ((builder: ContainerBuilder) => ContainerBuilder)
>
): this {
this.data.components ??= [];
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, ContainerBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds file components to this message.
*
* @param components - The file components to add
*/
public addFileComponents(
...components: RestOrArray<APIFileComponent | FileBuilder | ((builder: FileBuilder) => FileBuilder)>
): this {
this.data.components ??= [];
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, FileBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds media gallery components to this message.
*
* @param components - The media gallery components to add
*/
public addMediaGalleryComponents(
...components: RestOrArray<
APIMediaGalleryComponent | MediaGalleryBuilder | ((builder: MediaGalleryBuilder) => MediaGalleryBuilder)
>
): this {
this.data.components ??= [];
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, MediaGalleryBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds section components to this message.
*
* @param components - The section components to add
*/
public addSectionComponents(
...components: RestOrArray<APISectionComponent | SectionBuilder | ((builder: SectionBuilder) => SectionBuilder)>
): this {
this.data.components ??= [];
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, SectionBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds separator components to this message.
*
* @param components - The separator components to add
*/
public addSeparatorComponents(
...components: RestOrArray<
APISeparatorComponent | SeparatorBuilder | ((builder: SeparatorBuilder) => SeparatorBuilder)
>
): this {
this.data.components ??= [];
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, SeparatorBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Adds text display components to this message.
*
* @param components - The text display components to add
*/
public addTextDisplayComponents(
...components: RestOrArray<
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
>
): this {
this.data.components ??= [];
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, TextDisplayBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Removes, replaces, or inserts components for this message.
*
@@ -318,35 +434,17 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
public spliceComponents(
start: number,
deleteCount: number,
...components: RestOrArray<
| ActionRowBuilder
| APIActionRowComponent<APIComponentInMessageActionRow>
| ((builder: ActionRowBuilder) => ActionRowBuilder)
>
...components: RestOrArray<APIMessageTopLevelComponent | MessageTopLevelComponentBuilder>
): this {
this.data.components ??= [];
const resolved = normalizeArray(components).map((component) => resolveBuilder(component, ActionRowBuilder));
const resolved = normalizeArray(components).map((component) =>
component instanceof ComponentBuilder ? component : createComponentBuilder(component),
);
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<APIComponentInMessageActionRow>
| ((builder: ActionRowBuilder) => ActionRowBuilder)
>
): this {
this.data.components = normalizeArray(components).map((component) => resolveBuilder(component, ActionRowBuilder));
return this;
}
/**
* Sets the sticker ids of this message.
*

View File

@@ -1,3 +1,4 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIEmbedAuthor } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { embedAuthorPredicate } from './Assertions.js';
@@ -5,7 +6,7 @@ import { embedAuthorPredicate } from './Assertions.js';
/**
* A builder that creates API-compatible JSON data for the embed author.
*/
export class EmbedAuthorBuilder {
export class EmbedAuthorBuilder implements JSONEncodable<APIEmbedAuthor> {
private readonly data: Partial<APIEmbedAuthor>;
/**

View File

@@ -1,3 +1,4 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIEmbedField } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { embedFieldPredicate } from './Assertions.js';
@@ -5,7 +6,7 @@ import { embedFieldPredicate } from './Assertions.js';
/**
* A builder that creates API-compatible JSON data for embed fields.
*/
export class EmbedFieldBuilder {
export class EmbedFieldBuilder implements JSONEncodable<APIEmbedField> {
private readonly data: Partial<APIEmbedField>;
/**

View File

@@ -1,3 +1,4 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIEmbedFooter } from 'discord-api-types/v10';
import { validate } from '../../util/validation.js';
import { embedFooterPredicate } from './Assertions.js';
@@ -5,7 +6,7 @@ import { embedFooterPredicate } from './Assertions.js';
/**
* A builder that creates API-compatible JSON data for the embed footer.
*/
export class EmbedFooterBuilder {
export class EmbedFooterBuilder implements JSONEncodable<APIEmbedFooter> {
private readonly data: Partial<APIEmbedFooter>;
/**

View File

@@ -1,3 +1,4 @@
import type { JSONEncodable } from '@discordjs/util';
import type { APIPollAnswer, APIPollMedia } from 'discord-api-types/v10';
import { resolveBuilder } from '../../util/resolveBuilder';
import { validate } from '../../util/validation';
@@ -8,11 +9,11 @@ export interface PollAnswerData extends Omit<APIPollAnswer, 'answer_id' | 'poll_
poll_media: PollAnswerMediaBuilder;
}
export class PollAnswerBuilder {
export class PollAnswerBuilder implements JSONEncodable<Omit<APIPollAnswer, 'answer_id'>> {
/**
* The API data associated with this poll answer.
*/
protected readonly data: PollAnswerData;
private readonly data: PollAnswerData;
/**
* Creates a new poll answer from API data.