mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-14 18:43:31 +01:00
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:
@@ -3,10 +3,10 @@ import {
|
|||||||
ComponentType,
|
ComponentType,
|
||||||
TextInputStyle,
|
TextInputStyle,
|
||||||
type APIButtonComponent,
|
type APIButtonComponent,
|
||||||
type APIComponentInMessageActionRow,
|
|
||||||
type APISelectMenuComponent,
|
type APISelectMenuComponent,
|
||||||
type APITextInputComponent,
|
type APITextInputComponent,
|
||||||
type APIActionRowComponent,
|
type APIActionRowComponent,
|
||||||
|
type APIComponentInMessageActionRow,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
|||||||
237
packages/builders/__tests__/components/v2/container.test.ts
Normal file
237
packages/builders/__tests__/components/v2/container.test.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import {
|
||||||
|
type APIActionRowComponent,
|
||||||
|
type APIButtonComponent,
|
||||||
|
type APIContainerComponent,
|
||||||
|
ButtonStyle,
|
||||||
|
ComponentType,
|
||||||
|
SeparatorSpacingSize,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
import { createComponentBuilder } from '../../../src/components/Components.js';
|
||||||
|
import { ContainerBuilder } from '../../../src/components/v2/Container.js';
|
||||||
|
import { SeparatorBuilder } from '../../../src/components/v2/Separator.js';
|
||||||
|
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay.js';
|
||||||
|
import { MediaGalleryBuilder, SectionBuilder } from '../../../src/index.js';
|
||||||
|
|
||||||
|
const containerWithTextDisplay: APIContainerComponent = {
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.TextDisplay,
|
||||||
|
content: 'test',
|
||||||
|
id: 123,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const button = {
|
||||||
|
type: ComponentType.Button as const,
|
||||||
|
style: ButtonStyle.Primary as const,
|
||||||
|
custom_id: 'test',
|
||||||
|
label: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionRow: APIActionRowComponent<APIButtonComponent> = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [button],
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerWithSeparatorData: APIContainerComponent = {
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Separator,
|
||||||
|
id: 1_234,
|
||||||
|
spacing: SeparatorSpacingSize.Small,
|
||||||
|
divider: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
accent_color: 0x00ff00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerWithSeparatorDataNoColor: APIContainerComponent = {
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Separator,
|
||||||
|
id: 1_234,
|
||||||
|
spacing: SeparatorSpacingSize.Small,
|
||||||
|
divider: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Container Components', () => {
|
||||||
|
describe('Assertion Tests', () => {
|
||||||
|
test('GIVEN valid components THEN do not throw', () => {
|
||||||
|
expect(() => new ContainerBuilder().addSeparatorComponents(new SeparatorBuilder())).not.toThrowError();
|
||||||
|
expect(() => new ContainerBuilder().spliceComponents(0, 0, new SeparatorBuilder())).not.toThrowError();
|
||||||
|
expect(() => new ContainerBuilder().addSeparatorComponents([new SeparatorBuilder()])).not.toThrowError();
|
||||||
|
expect(() =>
|
||||||
|
new ContainerBuilder().spliceComponents(0, 0, [{ type: ComponentType.Separator }]),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||||
|
const containerData: APIContainerComponent = {
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.TextDisplay,
|
||||||
|
content: 'test',
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.Separator,
|
||||||
|
spacing: SeparatorSpacingSize.Large,
|
||||||
|
divider: true,
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.File,
|
||||||
|
file: {
|
||||||
|
url: 'attachment://file.png',
|
||||||
|
},
|
||||||
|
spoiler: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
accent_color: 0xff00ff,
|
||||||
|
spoiler: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(new ContainerBuilder(containerData).toJSON()).toEqual(containerData);
|
||||||
|
expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||||
|
const containerWithTextDisplay: APIContainerComponent = {
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.TextDisplay,
|
||||||
|
content: 'test',
|
||||||
|
id: 123,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerWithSeparatorData: APIContainerComponent = {
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Separator,
|
||||||
|
id: 1_234,
|
||||||
|
spacing: SeparatorSpacingSize.Small,
|
||||||
|
divider: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
accent_color: 0x00ff00,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(new ContainerBuilder(containerWithTextDisplay).toJSON()).toEqual(containerWithTextDisplay);
|
||||||
|
expect(new ContainerBuilder(containerWithSeparatorData).toJSON()).toEqual(containerWithSeparatorData);
|
||||||
|
expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
||||||
|
const textDisplay = new TextDisplayBuilder().setContent('test').setId(123);
|
||||||
|
const separator = new SeparatorBuilder().setId(1_234).setSpacing(SeparatorSpacingSize.Small).setDivider(false);
|
||||||
|
|
||||||
|
expect(new ContainerBuilder().addTextDisplayComponents(textDisplay).toJSON()).toEqual(containerWithTextDisplay);
|
||||||
|
expect(new ContainerBuilder().addSeparatorComponents(separator).toJSON()).toEqual(
|
||||||
|
containerWithSeparatorDataNoColor,
|
||||||
|
);
|
||||||
|
expect(new ContainerBuilder().addTextDisplayComponents([textDisplay]).toJSON()).toEqual(containerWithTextDisplay);
|
||||||
|
expect(new ContainerBuilder().addSeparatorComponents([separator]).toJSON()).toEqual(
|
||||||
|
containerWithSeparatorDataNoColor,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid accent color THEN valid JSON output is given', () => {
|
||||||
|
expect(
|
||||||
|
new ContainerBuilder({
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.TextDisplay,
|
||||||
|
content: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.setAccentColor(0xff00ff)
|
||||||
|
.toJSON(),
|
||||||
|
).toEqual({
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.TextDisplay,
|
||||||
|
content: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
accent_color: 0xff00ff,
|
||||||
|
});
|
||||||
|
expect(new ContainerBuilder(containerWithSeparatorData).clearAccentColor().toJSON()).toEqual(
|
||||||
|
containerWithSeparatorDataNoColor,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid method parameters THEN valid JSON is given', () => {
|
||||||
|
expect(
|
||||||
|
new ContainerBuilder()
|
||||||
|
.addMediaGalleryComponents(
|
||||||
|
new MediaGalleryBuilder()
|
||||||
|
.addItems({ media: { url: 'https://discord.com' } })
|
||||||
|
.setId(3)
|
||||||
|
.clearId(),
|
||||||
|
)
|
||||||
|
.setSpoiler()
|
||||||
|
.toJSON(),
|
||||||
|
).toEqual({
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.MediaGallery,
|
||||||
|
items: [{ media: { url: 'https://discord.com' } }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
spoiler: true,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
new ContainerBuilder()
|
||||||
|
.addSectionComponents(
|
||||||
|
new SectionBuilder()
|
||||||
|
.addTextDisplayComponents({ type: ComponentType.TextDisplay, content: 'test' })
|
||||||
|
.setPrimaryButtonAccessory(button),
|
||||||
|
)
|
||||||
|
.addFileComponents({ type: ComponentType.File, file: { url: 'attachment://discord.png' } })
|
||||||
|
.setSpoiler(false)
|
||||||
|
.setId(5)
|
||||||
|
.toJSON(),
|
||||||
|
).toEqual({
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.TextDisplay,
|
||||||
|
content: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
accessory: button,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ComponentType.File,
|
||||||
|
file: { url: 'attachment://discord.png' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
spoiler: false,
|
||||||
|
id: 5,
|
||||||
|
});
|
||||||
|
expect(new ContainerBuilder().addActionRowComponents(actionRow).setSpoiler(true).toJSON()).toEqual({
|
||||||
|
type: ComponentType.Container,
|
||||||
|
components: [actionRow],
|
||||||
|
spoiler: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
45
packages/builders/__tests__/components/v2/file.test.ts
Normal file
45
packages/builders/__tests__/components/v2/file.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ComponentType } from 'discord-api-types/v10';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { FileBuilder } from '../../../src/components/v2/File';
|
||||||
|
|
||||||
|
const dummy = {
|
||||||
|
type: ComponentType.File as const,
|
||||||
|
file: { url: 'attachment://owo.png' },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('File', () => {
|
||||||
|
describe('File url', () => {
|
||||||
|
test('GIVEN a file with a pre-defined url THEN return valid toJSON data', () => {
|
||||||
|
const file = new FileBuilder({ file: { url: 'attachment://owo.png' } });
|
||||||
|
expect(file.toJSON()).toEqual({ ...dummy, file: { url: 'attachment://owo.png' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a file using File#setURL THEN return valid toJSON data', () => {
|
||||||
|
const file = new FileBuilder();
|
||||||
|
file.setURL('attachment://uwu.png');
|
||||||
|
|
||||||
|
expect(file.toJSON()).toEqual({ ...dummy, file: { url: 'attachment://uwu.png' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a file with an invalid url THEN throws error', () => {
|
||||||
|
const file = new FileBuilder();
|
||||||
|
file.setURL('https://google.com');
|
||||||
|
|
||||||
|
expect(() => file.toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('File spoiler', () => {
|
||||||
|
test('GIVEN a file with a pre-defined spoiler status THEN return valid toJSON data', () => {
|
||||||
|
const file = new FileBuilder({ ...dummy, spoiler: true });
|
||||||
|
expect(file.toJSON()).toEqual({ ...dummy, spoiler: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a file using File#setSpoiler THEN return valid toJSON data', () => {
|
||||||
|
const file = new FileBuilder({ ...dummy });
|
||||||
|
file.setSpoiler(false);
|
||||||
|
|
||||||
|
expect(file.toJSON()).toEqual({ ...dummy, spoiler: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
117
packages/builders/__tests__/components/v2/mediaGallery.test.ts
Normal file
117
packages/builders/__tests__/components/v2/mediaGallery.test.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { ComponentType } from 'discord-api-types/v10';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery';
|
||||||
|
import { MediaGalleryItemBuilder } from '../../../src/components/v2/MediaGalleryItem';
|
||||||
|
|
||||||
|
describe('MediaGallery', () => {
|
||||||
|
test('GIVEN an empty media gallery THEN throws error', () => {
|
||||||
|
const gallery = new MediaGalleryBuilder();
|
||||||
|
expect(() => gallery.toJSON()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MediaGallery items', () => {
|
||||||
|
test('GIVEN a media gallery with pre-defined items THEN return valid toJSON data', () => {
|
||||||
|
const items = [
|
||||||
|
{ media: { url: 'https://google.com' } },
|
||||||
|
{ media: { url: 'https://discord.com' }, description: 'Discord' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const gallery = new MediaGalleryBuilder({
|
||||||
|
type: ComponentType.MediaGallery,
|
||||||
|
items,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(gallery.toJSON()).toEqual({
|
||||||
|
type: ComponentType.MediaGallery,
|
||||||
|
items,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery with items added via addItems THEN return valid toJSON data', () => {
|
||||||
|
const gallery = new MediaGalleryBuilder();
|
||||||
|
const item1 = new MediaGalleryItemBuilder().setURL('https://google.com');
|
||||||
|
const item2 = new MediaGalleryItemBuilder().setURL('https://discord.com').setDescription('Discord');
|
||||||
|
|
||||||
|
gallery.addItems(item1, item2);
|
||||||
|
|
||||||
|
expect(gallery.toJSON()).toEqual({
|
||||||
|
type: ComponentType.MediaGallery,
|
||||||
|
items: [
|
||||||
|
{ media: { url: 'https://google.com' } },
|
||||||
|
{ media: { url: 'https://discord.com' }, description: 'Discord' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery with items added via addItems with raw objects THEN return valid toJSON data', () => {
|
||||||
|
const gallery = new MediaGalleryBuilder();
|
||||||
|
|
||||||
|
gallery.addItems(
|
||||||
|
{ media: { url: 'https://google.com' } },
|
||||||
|
{ media: { url: 'https://discord.com' }, description: 'Discord' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(gallery.toJSON()).toEqual({
|
||||||
|
type: ComponentType.MediaGallery,
|
||||||
|
items: [
|
||||||
|
{ media: { url: 'https://google.com' } },
|
||||||
|
{ media: { url: 'https://discord.com' }, description: 'Discord' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery with items added via addItems with builder functions THEN return valid toJSON data', () => {
|
||||||
|
const gallery = new MediaGalleryBuilder();
|
||||||
|
|
||||||
|
gallery.addItems(
|
||||||
|
(builder) => builder.setURL('https://google.com'),
|
||||||
|
(builder) => builder.setURL('https://discord.com').setDescription('Discord'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(gallery.toJSON()).toEqual({
|
||||||
|
type: ComponentType.MediaGallery,
|
||||||
|
items: [
|
||||||
|
{ media: { url: 'https://google.com' } },
|
||||||
|
{ media: { url: 'https://discord.com' }, description: 'Discord' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery with array of items passed to addItems THEN return valid toJSON data', () => {
|
||||||
|
const gallery = new MediaGalleryBuilder();
|
||||||
|
const items = [
|
||||||
|
new MediaGalleryItemBuilder().setURL('https://google.com'),
|
||||||
|
new MediaGalleryItemBuilder().setURL('https://discord.com').setDescription('Discord'),
|
||||||
|
];
|
||||||
|
|
||||||
|
gallery.addItems(items);
|
||||||
|
|
||||||
|
expect(gallery.toJSON()).toEqual({
|
||||||
|
type: ComponentType.MediaGallery,
|
||||||
|
items: [
|
||||||
|
{ media: { url: 'https://google.com' } },
|
||||||
|
{ media: { url: 'https://discord.com' }, description: 'Discord' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery with items added via addItems with builder functions THEN return valid toJSON data', () => {
|
||||||
|
const gallery = new MediaGalleryBuilder();
|
||||||
|
|
||||||
|
gallery
|
||||||
|
.addItems(
|
||||||
|
new MediaGalleryItemBuilder().setURL('https://google.com'),
|
||||||
|
new MediaGalleryItemBuilder().setURL('https://discord.com').setDescription('Discord'),
|
||||||
|
)
|
||||||
|
.spliceItems(1, 1, new MediaGalleryItemBuilder().setURL('https://discord.js.org').setDescription('Discord.JS'));
|
||||||
|
|
||||||
|
expect(gallery.toJSON()).toEqual({
|
||||||
|
type: ComponentType.MediaGallery,
|
||||||
|
items: [
|
||||||
|
{ media: { url: 'https://google.com' } },
|
||||||
|
{ media: { url: 'https://discord.js.org' }, description: 'Discord.JS' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { MediaGalleryItemBuilder } from '../../../src/components/v2/MediaGalleryItem';
|
||||||
|
|
||||||
|
const dummy = {
|
||||||
|
media: { url: 'https://google.com' },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('MediaGalleryItem', () => {
|
||||||
|
describe('MediaGalleryItem url', () => {
|
||||||
|
test('GIVEN a media gallery item with a pre-defined url THEN return valid toJSON data', () => {
|
||||||
|
const item = new MediaGalleryItemBuilder({ media: { url: 'https://google.com' } });
|
||||||
|
expect(item.toJSON()).toEqual({ media: { url: 'https://google.com' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery item with a set url THEN return valid toJSON data', () => {
|
||||||
|
const item = new MediaGalleryItemBuilder().setURL('https://google.com');
|
||||||
|
expect(item.toJSON()).toEqual({ media: { url: 'https://google.com' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(['owo', 'discord://user'])(
|
||||||
|
'GIVEN a media gallery item with an invalid URL (%s) THEN throws error',
|
||||||
|
(input) => {
|
||||||
|
const item = new MediaGalleryItemBuilder();
|
||||||
|
|
||||||
|
item.setURL(input);
|
||||||
|
expect(() => item.toJSON()).toThrowError();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MediaGalleryItem description', () => {
|
||||||
|
test('GIVEN a media gallery item with a pre-defined description THEN return valid toJSON data', () => {
|
||||||
|
const item = new MediaGalleryItemBuilder({ ...dummy, description: 'foo' });
|
||||||
|
expect(item.toJSON()).toEqual({ ...dummy, description: 'foo' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery item with a set description THEN return valid toJSON data', () => {
|
||||||
|
const item = new MediaGalleryItemBuilder({ ...dummy });
|
||||||
|
item.setDescription('foo');
|
||||||
|
|
||||||
|
expect(item.toJSON()).toEqual({ ...dummy, description: 'foo' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery item with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
||||||
|
const item = new MediaGalleryItemBuilder({ description: 'foo', ...dummy });
|
||||||
|
item.clearDescription();
|
||||||
|
|
||||||
|
expect(item.toJSON()).toEqual({ ...dummy });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery item with an invalid description THEN throws error', () => {
|
||||||
|
const item = new MediaGalleryItemBuilder();
|
||||||
|
|
||||||
|
item.setDescription('a'.repeat(1_025));
|
||||||
|
expect(() => item.toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MediaGalleryItem spoiler', () => {
|
||||||
|
test('GIVEN a media gallery item with a pre-defined spoiler status THEN return valid toJSON data', () => {
|
||||||
|
const item = new MediaGalleryItemBuilder({ ...dummy, spoiler: true });
|
||||||
|
expect(item.toJSON()).toEqual({ ...dummy, spoiler: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a media gallery item with a set spoiler status THEN return valid toJSON data', () => {
|
||||||
|
const item = new MediaGalleryItemBuilder({ ...dummy });
|
||||||
|
item.setSpoiler(false);
|
||||||
|
|
||||||
|
expect(item.toJSON()).toEqual({ ...dummy, spoiler: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
166
packages/builders/__tests__/components/v2/section.test.ts
Normal file
166
packages/builders/__tests__/components/v2/section.test.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { PrimaryButtonBuilder } from '../../../src/components/button/CustomIdButton';
|
||||||
|
import { SectionBuilder } from '../../../src/components/v2/Section';
|
||||||
|
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay';
|
||||||
|
import { ThumbnailBuilder } from '../../../src/components/v2/Thumbnail';
|
||||||
|
|
||||||
|
describe('Section', () => {
|
||||||
|
describe('Validation', () => {
|
||||||
|
test('GIVEN empty section builder THEN throws error on toJSON', () => {
|
||||||
|
const section = new SectionBuilder();
|
||||||
|
expect(() => section.toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN section with text components but no accessory THEN throws error on toJSON', () => {
|
||||||
|
const section = new SectionBuilder().addTextDisplayComponents(new TextDisplayBuilder().setContent('Hello world'));
|
||||||
|
expect(() => section.toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN section with accessory but no text components THEN throws error on toJSON', () => {
|
||||||
|
const section = new SectionBuilder().setThumbnailAccessory(
|
||||||
|
new ThumbnailBuilder().setURL('https://example.com/image.png'),
|
||||||
|
);
|
||||||
|
expect(() => section.toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Text display components', () => {
|
||||||
|
test('GIVEN section with predefined text components THEN returns valid toJSON data', () => {
|
||||||
|
const section = new SectionBuilder({
|
||||||
|
components: [{ type: ComponentType.TextDisplay, content: 'Hello world' }],
|
||||||
|
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://example.com/image.png' } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(section.toJSON()).toEqual({
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [{ type: ComponentType.TextDisplay, content: 'Hello world' }],
|
||||||
|
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://example.com/image.png' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN section with added text components THEN returns valid toJSON data', () => {
|
||||||
|
const section = new SectionBuilder()
|
||||||
|
.addTextDisplayComponents(new TextDisplayBuilder().setContent('Hello world'))
|
||||||
|
.setThumbnailAccessory(new ThumbnailBuilder().setURL('https://example.com/image.png'));
|
||||||
|
|
||||||
|
expect(section.toJSON()).toEqual({
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [{ type: ComponentType.TextDisplay, content: 'Hello world' }],
|
||||||
|
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://example.com/image.png' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN section with multiple text components THEN returns valid toJSON data', () => {
|
||||||
|
const section = new SectionBuilder()
|
||||||
|
.addTextDisplayComponents(
|
||||||
|
new TextDisplayBuilder().setContent('Line 1'),
|
||||||
|
new TextDisplayBuilder().setContent('Line 2'),
|
||||||
|
new TextDisplayBuilder().setContent('Line 3'),
|
||||||
|
)
|
||||||
|
.setThumbnailAccessory(new ThumbnailBuilder().setURL('https://example.com/image.png'));
|
||||||
|
|
||||||
|
expect(section.toJSON()).toEqual({
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [
|
||||||
|
{ type: ComponentType.TextDisplay, content: 'Line 1' },
|
||||||
|
{ type: ComponentType.TextDisplay, content: 'Line 2' },
|
||||||
|
{ type: ComponentType.TextDisplay, content: 'Line 3' },
|
||||||
|
],
|
||||||
|
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://example.com/image.png' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN section with spliced text components THEN returns valid toJSON data', () => {
|
||||||
|
const section = new SectionBuilder()
|
||||||
|
.addTextDisplayComponents(
|
||||||
|
new TextDisplayBuilder().setContent('Original 1'),
|
||||||
|
new TextDisplayBuilder().setContent('Will be removed'),
|
||||||
|
new TextDisplayBuilder().setContent('Original 3'),
|
||||||
|
)
|
||||||
|
.spliceTextDisplayComponents(1, 1, new TextDisplayBuilder().setContent('Replacement'))
|
||||||
|
.setThumbnailAccessory(new ThumbnailBuilder().setURL('https://example.com/image.png'));
|
||||||
|
|
||||||
|
expect(section.toJSON()).toEqual({
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [
|
||||||
|
{ type: ComponentType.TextDisplay, content: 'Original 1' },
|
||||||
|
{ type: ComponentType.TextDisplay, content: 'Replacement' },
|
||||||
|
{ type: ComponentType.TextDisplay, content: 'Original 3' },
|
||||||
|
],
|
||||||
|
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://example.com/image.png' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessory components', () => {
|
||||||
|
test('GIVEN section with thumbnail accessory THEN returns valid toJSON data', () => {
|
||||||
|
const section = new SectionBuilder()
|
||||||
|
.addTextDisplayComponents(new TextDisplayBuilder().setContent('Hello world'))
|
||||||
|
.setThumbnailAccessory(new ThumbnailBuilder().setURL('https://example.com/image.png'));
|
||||||
|
|
||||||
|
expect(section.toJSON()).toEqual({
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [{ type: ComponentType.TextDisplay, content: 'Hello world' }],
|
||||||
|
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://example.com/image.png' } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN section with primary button accessory THEN returns valid toJSON data', () => {
|
||||||
|
const section = new SectionBuilder()
|
||||||
|
.addTextDisplayComponents(new TextDisplayBuilder().setContent('Hello world'))
|
||||||
|
.setPrimaryButtonAccessory(new PrimaryButtonBuilder().setCustomId('click_me').setLabel('Click me'));
|
||||||
|
|
||||||
|
expect(section.toJSON()).toEqual({
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [{ type: ComponentType.TextDisplay, content: 'Hello world' }],
|
||||||
|
accessory: {
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: 1,
|
||||||
|
custom_id: 'click_me',
|
||||||
|
label: 'Click me',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN section with primary button accessory JSON THEN returns valid toJSON data', () => {
|
||||||
|
const section = new SectionBuilder()
|
||||||
|
.addTextDisplayComponents(new TextDisplayBuilder().setContent('Hello world'))
|
||||||
|
.setPrimaryButtonAccessory({
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
custom_id: 'click_me',
|
||||||
|
label: 'Click me',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(section.toJSON()).toEqual({
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [{ type: ComponentType.TextDisplay, content: 'Hello world' }],
|
||||||
|
accessory: {
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: 1,
|
||||||
|
custom_id: 'click_me',
|
||||||
|
label: 'Click me',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN changing accessory type THEN returns the latest accessory in toJSON', () => {
|
||||||
|
const section = new SectionBuilder()
|
||||||
|
.addTextDisplayComponents(new TextDisplayBuilder().setContent('Hello world'))
|
||||||
|
.setThumbnailAccessory(new ThumbnailBuilder().setURL('https://example.com/image.png'))
|
||||||
|
.setPrimaryButtonAccessory(new PrimaryButtonBuilder().setCustomId('click_me').setLabel('Click me'));
|
||||||
|
|
||||||
|
expect(section.toJSON()).toEqual({
|
||||||
|
type: ComponentType.Section,
|
||||||
|
components: [{ type: ComponentType.TextDisplay, content: 'Hello world' }],
|
||||||
|
accessory: {
|
||||||
|
type: ComponentType.Button,
|
||||||
|
style: 1,
|
||||||
|
custom_id: 'click_me',
|
||||||
|
label: 'Click me',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
packages/builders/__tests__/components/v2/separator.test.ts
Normal file
35
packages/builders/__tests__/components/v2/separator.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { SeparatorBuilder } from '../../../src/components/v2/Separator';
|
||||||
|
|
||||||
|
describe('Separator', () => {
|
||||||
|
describe('Divider', () => {
|
||||||
|
test('GIVEN a separator with a pre-defined divider THEN return valid toJSON data', () => {
|
||||||
|
const separator = new SeparatorBuilder({ divider: true });
|
||||||
|
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, divider: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a separator with a set divider THEN return valid toJSON data', () => {
|
||||||
|
const separator = new SeparatorBuilder().setDivider(false);
|
||||||
|
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, divider: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Spacing', () => {
|
||||||
|
test('GIVEN a separator with a pre-defined spacing THEN return valid toJSON data', () => {
|
||||||
|
const separator = new SeparatorBuilder({ spacing: SeparatorSpacingSize.Small });
|
||||||
|
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, spacing: SeparatorSpacingSize.Small });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a separator with a set spacing THEN return valid toJSON data', () => {
|
||||||
|
const separator = new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large);
|
||||||
|
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, spacing: SeparatorSpacingSize.Large });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a separator with a set spacing THEN clear spacing THEN return valid toJSON data', () => {
|
||||||
|
const separator = new SeparatorBuilder({ spacing: SeparatorSpacingSize.Small });
|
||||||
|
separator.clearSpacing();
|
||||||
|
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentType } from 'discord-api-types/v10';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay';
|
||||||
|
|
||||||
|
describe('TextDisplay', () => {
|
||||||
|
describe('TextDisplay content', () => {
|
||||||
|
test('GIVEN a text display with a pre-defined content THEN return valid toJSON data', () => {
|
||||||
|
const textDisplay = new TextDisplayBuilder({ content: 'foo' });
|
||||||
|
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a text display with a set content THEN return valid toJSON data', () => {
|
||||||
|
const textDisplay = new TextDisplayBuilder().setContent('foo');
|
||||||
|
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a text display with a pre-defined content THEN overwritten content THEN return valid toJSON data', () => {
|
||||||
|
const textDisplay = new TextDisplayBuilder({ content: 'foo' });
|
||||||
|
textDisplay.setContent('bar');
|
||||||
|
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'bar' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
71
packages/builders/__tests__/components/v2/thumbnail.test.ts
Normal file
71
packages/builders/__tests__/components/v2/thumbnail.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { ComponentType } from 'discord-api-types/v10';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { ThumbnailBuilder } from '../../../src/components/v2/Thumbnail';
|
||||||
|
|
||||||
|
const dummy = {
|
||||||
|
type: ComponentType.Thumbnail as const,
|
||||||
|
media: { url: 'https://google.com' },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Thumbnail', () => {
|
||||||
|
describe('Thumbnail url', () => {
|
||||||
|
test('GIVEN a thumbnail with a pre-defined url THEN return valid toJSON data', () => {
|
||||||
|
const thumbnail = new ThumbnailBuilder({ media: { url: 'https://google.com' } });
|
||||||
|
expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a thumbnail with a set url THEN return valid toJSON data', () => {
|
||||||
|
const thumbnail = new ThumbnailBuilder().setURL('https://google.com');
|
||||||
|
expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL (%s) THEN throws error', (input) => {
|
||||||
|
const thumbnail = new ThumbnailBuilder();
|
||||||
|
|
||||||
|
thumbnail.setURL(input);
|
||||||
|
expect(() => thumbnail.toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Thumbnail description', () => {
|
||||||
|
test('GIVEN a thumbnail with a pre-defined description THEN return valid toJSON data', () => {
|
||||||
|
const thumbnail = new ThumbnailBuilder({ ...dummy, description: 'foo' });
|
||||||
|
expect(thumbnail.toJSON()).toEqual({ ...dummy, description: 'foo' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a thumbnail with a set description THEN return valid toJSON data', () => {
|
||||||
|
const thumbnail = new ThumbnailBuilder({ ...dummy });
|
||||||
|
thumbnail.setDescription('foo');
|
||||||
|
|
||||||
|
expect(thumbnail.toJSON()).toEqual({ ...dummy, description: 'foo' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a thumbnail with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
||||||
|
const thumbnail = new ThumbnailBuilder({ description: 'foo', ...dummy });
|
||||||
|
thumbnail.clearDescription();
|
||||||
|
|
||||||
|
expect(thumbnail.toJSON()).toEqual({ ...dummy });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a thumbnail with an invalid description THEN throws error', () => {
|
||||||
|
const thumbnail = new ThumbnailBuilder();
|
||||||
|
|
||||||
|
thumbnail.setDescription('a'.repeat(1_025));
|
||||||
|
expect(() => thumbnail.toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Thumbnail spoiler', () => {
|
||||||
|
test('GIVEN a thumbnail with a pre-defined spoiler status THEN return valid toJSON data', () => {
|
||||||
|
const thumbnail = new ThumbnailBuilder({ ...dummy, spoiler: true });
|
||||||
|
expect(thumbnail.toJSON()).toEqual({ ...dummy, spoiler: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a thumbnail with a set spoiler status THEN return valid toJSON data', () => {
|
||||||
|
const thumbnail = new ThumbnailBuilder({ ...dummy });
|
||||||
|
thumbnail.setSpoiler(false);
|
||||||
|
|
||||||
|
expect(thumbnail.toJSON()).toEqual({ ...dummy, spoiler: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,7 +18,7 @@ describe('Message', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN bad action row THEN it throws', () => {
|
test('GIVEN bad action row THEN it throws', () => {
|
||||||
const message = new MessageBuilder().setComponents((row) =>
|
const message = new MessageBuilder().addActionRowComponents((row) =>
|
||||||
row.addTextInputComponent((input) => input.setCustomId('abc').setLabel('def')),
|
row.addTextInputComponent((input) => input.setCustomId('abc').setLabel('def')),
|
||||||
);
|
);
|
||||||
expect(() => message.toJSON()).toThrow();
|
expect(() => message.toJSON()).toThrow();
|
||||||
@@ -32,7 +32,9 @@ describe('Message', () => {
|
|||||||
.addEmbeds(new EmbedBuilder().setTitle('foo').setDescription('bar'))
|
.addEmbeds(new EmbedBuilder().setTitle('foo').setDescription('bar'))
|
||||||
.setAllowedMentions({ parse: [AllowedMentionsTypes.Role], roles: ['123'] })
|
.setAllowedMentions({ parse: [AllowedMentionsTypes.Role], roles: ['123'] })
|
||||||
.setMessageReference({ channel_id: '123', message_id: '123' })
|
.setMessageReference({ channel_id: '123', message_id: '123' })
|
||||||
.setComponents((row) => row.addPrimaryButtonComponents((button) => button.setCustomId('abc').setLabel('def')))
|
.addActionRowComponents((row) =>
|
||||||
|
row.addPrimaryButtonComponents((button) => button.setCustomId('abc').setLabel('def')),
|
||||||
|
)
|
||||||
.setStickerIds('123', '456')
|
.setStickerIds('123', '456')
|
||||||
.addAttachments((attachment) => attachment.setId('hi!').setFilename('abc'))
|
.addAttachments((attachment) => attachment.setId('hi!').setFilename('abc'))
|
||||||
.setFlags(MessageFlags.Ephemeral)
|
.setFlags(MessageFlags.Ephemeral)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export interface ActionRowBuilderData
|
|||||||
* @typeParam ComponentType - The types of components this action row holds
|
* @typeParam ComponentType - The types of components this action row holds
|
||||||
*/
|
*/
|
||||||
export class ActionRowBuilder extends ComponentBuilder<APIActionRowComponent<APIComponentInActionRow>> {
|
export class ActionRowBuilder extends ComponentBuilder<APIActionRowComponent<APIComponentInActionRow>> {
|
||||||
private readonly data: ActionRowBuilderData;
|
protected readonly data: ActionRowBuilderData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The components within this action row.
|
* The components within this action row.
|
||||||
|
|||||||
@@ -1,17 +1,38 @@
|
|||||||
import type { JSONEncodable } from '@discordjs/util';
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import type { APIActionRowComponent, APIComponentInActionRow } from 'discord-api-types/v10';
|
import type { APIBaseComponent, ComponentType } from 'discord-api-types/v10';
|
||||||
|
|
||||||
/**
|
export interface ComponentBuilderBaseData {
|
||||||
* Any action row component data represented as an object.
|
id?: number | undefined;
|
||||||
*/
|
}
|
||||||
export type AnyAPIActionRowComponent = APIActionRowComponent<APIComponentInActionRow> | APIComponentInActionRow;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base component builder that contains common symbols for all sorts of components.
|
* 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
|
* @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.
|
* Serializes this builder to API-compatible JSON data.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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 { ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||||
import { ActionRowBuilder } from './ActionRow.js';
|
import { ActionRowBuilder } from './ActionRow.js';
|
||||||
import type { AnyAPIActionRowComponent } from './Component.js';
|
|
||||||
import { ComponentBuilder } from './Component.js';
|
import { ComponentBuilder } from './Component.js';
|
||||||
import type { BaseButtonBuilder } from './button/Button.js';
|
import type { BaseButtonBuilder } from './button/Button.js';
|
||||||
import {
|
import {
|
||||||
@@ -18,11 +23,33 @@ import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
|||||||
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
||||||
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||||
import { TextInputBuilder } from './textInput/TextInput.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.
|
* 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.
|
* 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}.
|
* The channel select component type is associated with a {@link ChannelSelectMenuBuilder}.
|
||||||
*/
|
*/
|
||||||
[ComponentType.ChannelSelect]: 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(
|
export function createComponentBuilder(
|
||||||
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
|
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
|
||||||
): ComponentBuilder<AnyAPIActionRowComponent> {
|
): ComponentBuilder<APIBaseComponent<ComponentType>> {
|
||||||
if (data instanceof ComponentBuilder) {
|
if (data instanceof ComponentBuilder) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -144,36 +199,20 @@ export function createComponentBuilder(
|
|||||||
return new MentionableSelectMenuBuilder(data);
|
return new MentionableSelectMenuBuilder(data);
|
||||||
case ComponentType.ChannelSelect:
|
case ComponentType.ChannelSelect:
|
||||||
return new ChannelSelectMenuBuilder(data);
|
return new ChannelSelectMenuBuilder(data);
|
||||||
|
case ComponentType.Thumbnail:
|
||||||
// Will be handled later
|
return new ThumbnailBuilder(data);
|
||||||
case ComponentType.Section: {
|
case ComponentType.File:
|
||||||
throw new Error('Not implemented yet: ComponentType.Section case');
|
return new FileBuilder(data);
|
||||||
}
|
case ComponentType.Separator:
|
||||||
|
return new SeparatorBuilder(data);
|
||||||
case ComponentType.TextDisplay: {
|
case ComponentType.TextDisplay:
|
||||||
throw new Error('Not implemented yet: ComponentType.TextDisplay case');
|
return new TextDisplayBuilder(data);
|
||||||
}
|
case ComponentType.MediaGallery:
|
||||||
|
return new MediaGalleryBuilder(data);
|
||||||
case ComponentType.Thumbnail: {
|
case ComponentType.Section:
|
||||||
throw new Error('Not implemented yet: ComponentType.Thumbnail case');
|
return new SectionBuilder(data);
|
||||||
}
|
case ComponentType.Container:
|
||||||
|
return new ContainerBuilder(data);
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// @ts-expect-error This case can still occur if we get a newer unsupported component type
|
// @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}`);
|
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}`);
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
|
|||||||
extends ComponentBuilder<Data>
|
extends ComponentBuilder<Data>
|
||||||
implements JSONEncodable<APISelectMenuComponent>
|
implements JSONEncodable<APISelectMenuComponent>
|
||||||
{
|
{
|
||||||
protected abstract readonly data: Partial<
|
protected abstract override readonly data: Partial<
|
||||||
Pick<Data, 'custom_id' | 'disabled' | 'max_values' | 'min_values' | 'placeholder'>
|
Pick<Data, 'custom_id' | 'disabled' | 'id' | 'max_values' | 'min_values' | 'placeholder'>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { textInputPredicate } from './Assertions.js';
|
|||||||
* A builder that creates API-compatible JSON data for text inputs.
|
* A builder that creates API-compatible JSON data for text inputs.
|
||||||
*/
|
*/
|
||||||
export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
||||||
private readonly data: Partial<APITextInputComponent>;
|
protected readonly data: Partial<APITextInputComponent>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new text input from API data.
|
* Creates a new text input from API data.
|
||||||
|
|||||||
78
packages/builders/src/components/v2/Assertions.ts
Normal file
78
packages/builders/src/components/v2/Assertions.ts
Normal 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(),
|
||||||
|
});
|
||||||
232
packages/builders/src/components/v2/Container.ts
Normal file
232
packages/builders/src/components/v2/Container.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
packages/builders/src/components/v2/File.ts
Normal file
72
packages/builders/src/components/v2/File.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
packages/builders/src/components/v2/MediaGallery.ts
Normal file
118
packages/builders/src/components/v2/MediaGallery.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
packages/builders/src/components/v2/MediaGalleryItem.ts
Normal file
87
packages/builders/src/components/v2/MediaGalleryItem.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
257
packages/builders/src/components/v2/Section.ts
Normal file
257
packages/builders/src/components/v2/Section.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
packages/builders/src/components/v2/Separator.ts
Normal file
76
packages/builders/src/components/v2/Separator.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
packages/builders/src/components/v2/TextDisplay.ts
Normal file
57
packages/builders/src/components/v2/TextDisplay.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
packages/builders/src/components/v2/Thumbnail.ts
Normal file
91
packages/builders/src/components/v2/Thumbnail.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,15 @@ export * from './components/Assertions.js';
|
|||||||
export * from './components/Component.js';
|
export * from './components/Component.js';
|
||||||
export * from './components/Components.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/ApplicationCommandNumericOptionMinMaxValueMixin.js';
|
||||||
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.js';
|
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.js';
|
||||||
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
||||||
|
|||||||
@@ -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 { z } from 'zod';
|
||||||
import { embedPredicate } from './embed/Assertions.js';
|
import { embedPredicate } from './embed/Assertions.js';
|
||||||
import { pollPredicate } from './poll/Assertions.js';
|
import { pollPredicate } from './poll/Assertions.js';
|
||||||
@@ -27,40 +27,49 @@ export const messageReferencePredicate = z.object({
|
|||||||
type: z.nativeEnum(MessageReferenceType).optional(),
|
type: z.nativeEnum(MessageReferenceType).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messagePredicate = z
|
const baseMessagePredicate = z.object({
|
||||||
.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(),
|
content: z.string().optional(),
|
||||||
nonce: z.union([z.string().max(25), z.number()]).optional(),
|
|
||||||
tts: z.boolean().optional(),
|
|
||||||
embeds: embedPredicate.array().max(10).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(),
|
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(),
|
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(
|
.refine(
|
||||||
(data) =>
|
(data) =>
|
||||||
@@ -70,5 +79,37 @@ export const messagePredicate = z
|
|||||||
(data.attachments !== undefined && data.attachments.length > 0) ||
|
(data.attachments !== undefined && data.attachments.length > 0) ||
|
||||||
(data.components !== undefined && data.components.length > 0) ||
|
(data.components !== undefined && data.components.length > 0) ||
|
||||||
(data.sticker_ids !== undefined && data.sticker_ids.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]);
|
||||||
|
|||||||
@@ -10,9 +10,24 @@ import type {
|
|||||||
RESTPostAPIChannelMessageJSONBody,
|
RESTPostAPIChannelMessageJSONBody,
|
||||||
Snowflake,
|
Snowflake,
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
APIComponentInActionRow,
|
APIContainerComponent,
|
||||||
|
APIFileComponent,
|
||||||
|
APIMediaGalleryComponent,
|
||||||
|
APISectionComponent,
|
||||||
|
APISeparatorComponent,
|
||||||
|
APITextDisplayComponent,
|
||||||
|
APIMessageTopLevelComponent,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { ActionRowBuilder } from '../components/ActionRow.js';
|
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 { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
||||||
import { resolveBuilder } from '../util/resolveBuilder.js';
|
import { resolveBuilder } from '../util/resolveBuilder.js';
|
||||||
import { validate } from '../util/validation.js';
|
import { validate } from '../util/validation.js';
|
||||||
@@ -32,7 +47,7 @@ export interface MessageBuilderData
|
|||||||
> {
|
> {
|
||||||
allowed_mentions?: AllowedMentionsBuilder;
|
allowed_mentions?: AllowedMentionsBuilder;
|
||||||
attachments: AttachmentBuilder[];
|
attachments: AttachmentBuilder[];
|
||||||
components: ActionRowBuilder[];
|
components: MessageTopLevelComponentBuilder[];
|
||||||
embeds: EmbedBuilder[];
|
embeds: EmbedBuilder[];
|
||||||
message_reference?: MessageReferenceBuilder;
|
message_reference?: MessageReferenceBuilder;
|
||||||
poll?: PollBuilder;
|
poll?: PollBuilder;
|
||||||
@@ -54,7 +69,7 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
|||||||
/**
|
/**
|
||||||
* Gets the components of this message.
|
* Gets the components of this message.
|
||||||
*/
|
*/
|
||||||
public get components(): readonly ActionRowBuilder[] {
|
public get components(): readonly MessageTopLevelComponentBuilder[] {
|
||||||
return this.data.components;
|
return this.data.components;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +92,7 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
|||||||
attachments: data.attachments?.map((attachment) => new AttachmentBuilder(attachment)) ?? [],
|
attachments: data.attachments?.map((attachment) => new AttachmentBuilder(attachment)) ?? [],
|
||||||
embeds: data.embeds?.map((embed) => new EmbedBuilder(embed)) ?? [],
|
embeds: data.embeds?.map((embed) => new EmbedBuilder(embed)) ?? [],
|
||||||
poll: data.poll ? new PollBuilder(data.poll) : undefined,
|
poll: data.poll ? new PollBuilder(data.poll) : undefined,
|
||||||
components:
|
components: data.components?.map((component) => createComponentBuilder(component)) ?? [],
|
||||||
data.components?.map(
|
|
||||||
(component) => new ActionRowBuilder(component as unknown as APIActionRowComponent<APIComponentInActionRow>),
|
|
||||||
) ?? [],
|
|
||||||
message_reference: data.message_reference ? new MessageReferenceBuilder(data.message_reference) : undefined,
|
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<
|
...components: RestOrArray<
|
||||||
| ActionRowBuilder
|
| ActionRowBuilder
|
||||||
| APIActionRowComponent<APIComponentInMessageActionRow>
|
| APIActionRowComponent<APIComponentInMessageActionRow>
|
||||||
@@ -287,6 +299,110 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
|||||||
return this;
|
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.
|
* Removes, replaces, or inserts components for this message.
|
||||||
*
|
*
|
||||||
@@ -318,35 +434,17 @@ export class MessageBuilder implements JSONEncodable<RESTPostAPIChannelMessageJS
|
|||||||
public spliceComponents(
|
public spliceComponents(
|
||||||
start: number,
|
start: number,
|
||||||
deleteCount: number,
|
deleteCount: number,
|
||||||
...components: RestOrArray<
|
...components: RestOrArray<APIMessageTopLevelComponent | MessageTopLevelComponentBuilder>
|
||||||
| ActionRowBuilder
|
|
||||||
| APIActionRowComponent<APIComponentInMessageActionRow>
|
|
||||||
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
|
||||||
>
|
|
||||||
): this {
|
): this {
|
||||||
this.data.components ??= [];
|
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);
|
this.data.components.splice(start, deleteCount, ...resolved);
|
||||||
return this;
|
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.
|
* Sets the sticker ids of this message.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import type { APIEmbedAuthor } from 'discord-api-types/v10';
|
import type { APIEmbedAuthor } from 'discord-api-types/v10';
|
||||||
import { validate } from '../../util/validation.js';
|
import { validate } from '../../util/validation.js';
|
||||||
import { embedAuthorPredicate } from './Assertions.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.
|
* 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>;
|
private readonly data: Partial<APIEmbedAuthor>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||||
import { validate } from '../../util/validation.js';
|
import { validate } from '../../util/validation.js';
|
||||||
import { embedFieldPredicate } from './Assertions.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.
|
* 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>;
|
private readonly data: Partial<APIEmbedField>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import type { APIEmbedFooter } from 'discord-api-types/v10';
|
import type { APIEmbedFooter } from 'discord-api-types/v10';
|
||||||
import { validate } from '../../util/validation.js';
|
import { validate } from '../../util/validation.js';
|
||||||
import { embedFooterPredicate } from './Assertions.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.
|
* 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>;
|
private readonly data: Partial<APIEmbedFooter>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import type { APIPollAnswer, APIPollMedia } from 'discord-api-types/v10';
|
import type { APIPollAnswer, APIPollMedia } from 'discord-api-types/v10';
|
||||||
import { resolveBuilder } from '../../util/resolveBuilder';
|
import { resolveBuilder } from '../../util/resolveBuilder';
|
||||||
import { validate } from '../../util/validation';
|
import { validate } from '../../util/validation';
|
||||||
@@ -8,11 +9,11 @@ export interface PollAnswerData extends Omit<APIPollAnswer, 'answer_id' | 'poll_
|
|||||||
poll_media: PollAnswerMediaBuilder;
|
poll_media: PollAnswerMediaBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PollAnswerBuilder {
|
export class PollAnswerBuilder implements JSONEncodable<Omit<APIPollAnswer, 'answer_id'>> {
|
||||||
/**
|
/**
|
||||||
* The API data associated with this poll answer.
|
* The API data associated with this poll answer.
|
||||||
*/
|
*/
|
||||||
protected readonly data: PollAnswerData;
|
private readonly data: PollAnswerData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new poll answer from API data.
|
* Creates a new poll answer from API data.
|
||||||
|
|||||||
Reference in New Issue
Block a user