refactor: Don't return builders from API data (#7584)

* refactor: don't return builders from API data

* Update packages/discord.js/src/structures/ActionRow.js

Co-authored-by: Antonio Román <kyradiscord@gmail.com>

* fix: circular dependency

* fix: circular dependency pt.2

* chore: make requested changes

* chore: bump dapi-types

* chore: convert text input

* chore: convert text input

* feat: handle cases of unknown component types better

* refactor: refactor modal to builder

* feat: add #from for easy builder conversions

* refactor: make requested changes

* chore: make requested changes

* style: fix linting error

Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: almeidx <almeidx@pm.me>
This commit is contained in:
Suneet Tipirneni
2022-03-12 13:39:23 -05:00
committed by GitHub
parent 230c0c4cb1
commit 549716e4fc
44 changed files with 974 additions and 705 deletions

View File

@@ -1,13 +1,13 @@
import { APIActionRowComponent, APIMessageActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9';
import {
APIActionRowComponent,
APIActionRowComponentTypes,
APIMessageActionRowComponent,
ButtonStyle,
ComponentType,
} from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src';
ActionRowBuilder,
ButtonBuilder,
createComponentBuilder,
SelectMenuBuilder,
SelectMenuOptionBuilder,
} from '../../src';
const rowWithButtonData: APIActionRowComponent<APIMessageComponent> = {
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
type: ComponentType.ActionRow,
components: [
{
@@ -19,7 +19,7 @@ const rowWithButtonData: APIActionRowComponent<APIMessageComponent> = {
],
};
const rowWithSelectMenuData: APIActionRowComponent<APIMessageComponent> = {
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
type: ComponentType.ActionRow,
components: [
{
@@ -44,8 +44,8 @@ const rowWithSelectMenuData: APIActionRowComponent<APIMessageComponent> = {
describe('Action Row Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid components THEN do not throw', () => {
expect(() => new ActionRow().addComponents(new ButtonComponent())).not.toThrowError();
expect(() => new ActionRow().setComponents(new ButtonComponent())).not.toThrowError();
expect(() => new ActionRowBuilder().addComponents(new ButtonBuilder())).not.toThrowError();
expect(() => new ActionRowBuilder().setComponents(new ButtonBuilder())).not.toThrowError();
});
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
@@ -78,13 +78,12 @@ describe('Action Row Components', () => {
],
};
expect(new ActionRow(actionRowData).toJSON()).toEqual(actionRowData);
expect(new ActionRow().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponent({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
expect(() => createComponent({ type: 42, components: [] })).toThrowError();
expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData);
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
});
test('GIVEN valid builder options THEN valid JSON output is given', () => {
const rowWithButtonData: APIActionRowComponent<APIActionRowComponentTypes> = {
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
type: ComponentType.ActionRow,
components: [
{
@@ -96,7 +95,7 @@ describe('Action Row Components', () => {
],
};
const rowWithSelectMenuData: APIActionRowComponent<APIActionRowComponentTypes> = {
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
type: ComponentType.ActionRow,
components: [
{
@@ -118,22 +117,24 @@ describe('Action Row Components', () => {
],
};
const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
const selectMenu = new SelectMenuComponent()
expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
});
test('GIVEN valid builder options THEN valid JSON output is given', () => {
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
const selectMenu = new SelectMenuBuilder()
.setCustomId('1234')
.setMaxValues(10)
.setMinValues(12)
.setOptions(
new SelectMenuOption().setLabel('one').setValue('one'),
new SelectMenuOption().setLabel('two').setValue('two'),
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
);
expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
});
test('Given JSON data THEN builder is equal to it and itself', () => {
expect(new ActionRow(rowWithSelectMenuData).equals(rowWithSelectMenuData)).toBeTruthy();
expect(new ActionRow(rowWithButtonData).equals(new ActionRow(rowWithButtonData))).toBeTruthy();
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
});
});
});

View File

@@ -5,9 +5,9 @@ import {
ComponentType,
} from 'discord-api-types/v9';
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions';
import { ButtonComponent } from '../../src/components/button/Button';
import { ButtonBuilder } from '../../src/components/button/Button';
const buttonComponent = () => new ButtonComponent();
const buttonComponent = () => new ButtonBuilder();
const longStr =
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
@@ -119,7 +119,7 @@ describe('Button Components', () => {
disabled: true,
};
expect(new ButtonComponent(interactionData).toJSON()).toEqual(interactionData);
expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
expect(
buttonComponent()
@@ -138,21 +138,9 @@ describe('Button Components', () => {
url: 'https://google.com',
};
expect(new ButtonComponent(linkData).toJSON()).toEqual(linkData);
expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData);
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
});
test('Given JSON data THEN builder is equal to it and itself', () => {
const buttonData: APIButtonComponentWithCustomId = {
type: ComponentType.Button,
custom_id: 'test',
label: 'test',
style: ButtonStyle.Primary,
disabled: true,
};
expect(new ButtonComponent(buttonData).equals(buttonData)).toBeTruthy();
expect(new ButtonComponent(buttonData).equals(new ButtonComponent(buttonData))).toBeTruthy();
});
});
});

View File

@@ -1,8 +1,8 @@
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v9';
import { SelectMenuComponent, SelectMenuOption } from '../../src/index';
import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index';
const selectMenu = () => new SelectMenuComponent();
const selectMenuOption = () => new SelectMenuOption();
const selectMenu = () => new SelectMenuBuilder();
const selectMenuOption = () => new SelectMenuOptionBuilder();
const longStr = 'a'.repeat(256);
@@ -44,8 +44,8 @@ describe('Select Menu Components', () => {
.setEmoji({ name: 'test' })
.setDescription('description');
expect(() => selectMenu().addOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions([option])).not.toThrowError();
expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError();
expect(() => selectMenu().setOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError();
});
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
@@ -70,16 +70,11 @@ describe('Select Menu Components', () => {
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
expect(
new SelectMenuComponent(selectMenuDataWithoutOptions)
.addOptions(new SelectMenuOption(selectMenuOptionData))
new SelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions(new SelectMenuOptionBuilder(selectMenuOptionData))
.toJSON(),
).toEqual(selectMenuData);
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
});
test('Given JSON data THEN builder is equal to it and itself', () => {
expect(new SelectMenuComponent(selectMenuData).equals(selectMenuData)).toBeTruthy();
expect(new SelectMenuComponent(selectMenuData).equals(new SelectMenuComponent(selectMenuData))).toBeTruthy();
expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
});
});
});

View File

@@ -7,11 +7,11 @@ import {
valueValidator,
textInputStyleValidator,
} from '../../src/components/textInput/Assertions';
import { TextInputComponent } from '../../src/components/textInput/TextInput';
import { TextInputBuilder } from '../../src/components/textInput/TextInput';
const superLongStr = 'a'.repeat(5000);
const textInputComponent = () => new TextInputComponent();
const textInputComponent = () => new TextInputBuilder();
describe('Text Input Components', () => {
describe('Assertion Tests', () => {
@@ -109,7 +109,7 @@ describe('Text Input Components', () => {
style: TextInputStyle.Paragraph,
};
expect(new TextInputComponent(textInputData).toJSON()).toEqual(textInputData);
expect(new TextInputBuilder(textInputData).toJSON()).toEqual(textInputData);
expect(
textInputComponent()
.setCustomId(textInputData.custom_id)

View File

@@ -1,12 +1,18 @@
import { APIModalInteractionResponseCallbackData, ComponentType, TextInputStyle } from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, Modal, ModalActionRowComponent, TextInputComponent } from '../../src';
import {
ActionRowBuilder,
ButtonBuilder,
ModalBuilder,
ModalActionRowComponentBuilder,
TextInputBuilder,
} from '../../src';
import {
componentsValidator,
titleValidator,
validateRequiredParameters,
} from '../../src/interactions/modals/Assertions';
const modal = () => new Modal();
const modal = () => new ModalBuilder();
describe('Modals', () => {
describe('Assertion Tests', () => {
@@ -19,33 +25,37 @@ describe('Modals', () => {
});
test('GIVEN valid components THEN validator does not throw', () => {
expect(() => componentsValidator.parse([new ActionRow(), new ActionRow()])).not.toThrowError();
expect(() => componentsValidator.parse([new ActionRowBuilder(), new ActionRowBuilder()])).not.toThrowError();
});
test('GIVEN invalid components THEN validator does throw', () => {
expect(() => componentsValidator.parse([new ButtonComponent(), new TextInputComponent()])).toThrowError();
expect(() => componentsValidator.parse([new ButtonBuilder(), new TextInputBuilder()])).toThrowError();
});
test('GIVEN valid required parameters THEN validator does not throw', () => {
expect(() => validateRequiredParameters('123', 'title', [new ActionRow(), new ActionRow()])).not.toThrowError();
expect(() =>
validateRequiredParameters('123', 'title', [new ActionRowBuilder(), new ActionRowBuilder()]),
).not.toThrowError();
});
test('GIVEN invalid required parameters THEN validator does throw', () => {
expect(() =>
// @ts-expect-error
validateRequiredParameters('123', undefined, [new ActionRow(), new ButtonComponent()]),
validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]),
).toThrowError();
});
});
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() => modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRow())).not.toThrowError();
expect(() =>
modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRowBuilder()),
).not.toThrowError();
});
test('GIVEN invalid fields THEN builder does throw', () => {
expect(() =>
// @ts-expect-error
modal().setTitle('test').setCustomId('foobar').setComponents([new ActionRow()]).toJSON(),
modal().setTitle('test').setCustomId('foobar').setComponents([new ActionRowBuilder()]).toJSON(),
).toThrowError();
expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError();
// @ts-expect-error
@@ -71,15 +81,15 @@ describe('Modals', () => {
],
};
expect(new Modal(modalData).toJSON()).toEqual(modalData);
expect(new ModalBuilder(modalData).toJSON()).toEqual(modalData);
expect(
modal()
.setTitle(modalData.title)
.setCustomId('custom id')
.setComponents(
new ActionRow<ModalActionRowComponent>().addComponents(
new TextInputComponent().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
),
)
.toJSON(),

View File

@@ -1,11 +1,11 @@
import { Embed } from '../../src';
import { EmbedBuilder, embedLength } from '../../src';
const alpha = 'abcdefghijklmnopqrstuvwxyz';
describe('Embed', () => {
describe('Embed getters', () => {
test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => {
const embed = new Embed({
const embed = new EmbedBuilder({
title: alpha,
description: alpha,
fields: [{ name: alpha, value: alpha }],
@@ -13,38 +13,38 @@ describe('Embed', () => {
footer: { text: alpha },
});
expect(embed.length).toBe(alpha.length * 6);
expect(embedLength(embed.data)).toBe(alpha.length * 6);
});
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(embed.length).toBe(0);
expect(embedLength(embed.data)).toBe(0);
});
});
describe('Embed title', () => {
test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => {
const embed = new Embed({ title: 'foo' });
const embed = new EmbedBuilder({ title: 'foo' });
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
});
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setTitle('foo');
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
});
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
const embed = new Embed({ title: 'foo' });
const embed = new EmbedBuilder({ title: 'foo' });
embed.setTitle(null);
expect(embed.toJSON()).toStrictEqual({ title: undefined });
});
test('GIVEN an embed with an invalid title THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.setTitle('a'.repeat(257))).toThrowError();
});
@@ -52,26 +52,26 @@ describe('Embed', () => {
describe('Embed description', () => {
test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => {
const embed = new Embed({ description: 'foo' });
const embed = new EmbedBuilder({ description: 'foo' });
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
});
test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setDescription('foo');
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
});
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
const embed = new Embed({ description: 'foo' });
const embed = new EmbedBuilder({ description: 'foo' });
embed.setDescription(null);
expect(embed.toJSON()).toStrictEqual({ description: undefined });
});
test('GIVEN an embed with an invalid description THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.setDescription('a'.repeat(4097))).toThrowError();
});
@@ -79,14 +79,14 @@ describe('Embed', () => {
describe('Embed URL', () => {
test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => {
const embed = new Embed({ url: 'https://discord.js.org/' });
const embed = new EmbedBuilder({ url: 'https://discord.js.org/' });
expect(embed.toJSON()).toStrictEqual({
url: 'https://discord.js.org/',
});
});
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setURL('https://discord.js.org/');
expect(embed.toJSON()).toStrictEqual({
@@ -95,14 +95,14 @@ describe('Embed', () => {
});
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
const embed = new Embed({ url: 'https://discord.js.org' });
const embed = new EmbedBuilder({ url: 'https://discord.js.org' });
embed.setURL(null);
expect(embed.toJSON()).toStrictEqual({ url: undefined });
});
test('GIVEN an embed with an invalid URL THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.setURL('owo')).toThrowError();
});
@@ -110,24 +110,24 @@ describe('Embed', () => {
describe('Embed Color', () => {
test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => {
const embed = new Embed({ color: 0xff0000 });
const embed = new EmbedBuilder({ color: 0xff0000 });
expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
});
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
expect(new Embed().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
expect(new Embed().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
expect(new EmbedBuilder().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
expect(new EmbedBuilder().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
});
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
const embed = new Embed({ color: 0xff0000 });
const embed = new EmbedBuilder({ color: 0xff0000 });
embed.setColor(null);
expect(embed.toJSON()).toStrictEqual({ color: undefined });
});
test('GIVEN an embed with an invalid color THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
// @ts-expect-error
expect(() => embed.setColor('RED')).toThrowError();
@@ -141,33 +141,33 @@ describe('Embed', () => {
const now = new Date();
test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => {
const embed = new Embed({ timestamp: now.toISOString() });
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
});
test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setTimestamp(now);
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setTimestamp(now.getTime());
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setTimestamp();
expect(embed.toJSON()).toStrictEqual({ timestamp: embed.timestamp });
expect(embed.toJSON()).toStrictEqual({ timestamp: embed.data.timestamp });
});
test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => {
const embed = new Embed({ timestamp: now.toISOString() });
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
embed.setTimestamp(null);
expect(embed.toJSON()).toStrictEqual({ timestamp: undefined });
@@ -176,14 +176,14 @@ describe('Embed', () => {
describe('Embed Thumbnail', () => {
test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => {
const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setThumbnail('https://discord.js.org/static/logo.svg');
expect(embed.toJSON()).toStrictEqual({
@@ -192,14 +192,14 @@ describe('Embed', () => {
});
test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => {
const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
embed.setThumbnail(null);
expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined });
});
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.setThumbnail('owo')).toThrowError();
});
@@ -207,14 +207,14 @@ describe('Embed', () => {
describe('Embed Image', () => {
test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => {
const embed = new Embed({ image: { url: 'https://discord.js.org/static/logo.svg' } });
const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
image: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setImage('https://discord.js.org/static/logo.svg');
expect(embed.toJSON()).toStrictEqual({
@@ -223,14 +223,14 @@ describe('Embed', () => {
});
test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => {
const embed = new Embed({ image: { url: 'https://discord.js/org/static/logo.svg' } });
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } });
embed.setImage(null);
expect(embed.toJSON()).toStrictEqual({ image: undefined });
});
test('GIVEN an embed with an invalid image THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.setImage('owo')).toThrowError();
});
@@ -238,7 +238,7 @@ describe('Embed', () => {
describe('Embed Author', () => {
test('GIVEN an embed with a pre-defined author THEN returns valid toJSON data', () => {
const embed = new Embed({
const embed = new EmbedBuilder({
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
expect(embed.toJSON()).toStrictEqual({
@@ -247,7 +247,7 @@ describe('Embed', () => {
});
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setAuthor({
name: 'Wumpus',
iconURL: 'https://discord.js.org/static/logo.svg',
@@ -260,7 +260,7 @@ describe('Embed', () => {
});
test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => {
const embed = new Embed({
const embed = new EmbedBuilder({
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
embed.setAuthor(null);
@@ -269,7 +269,7 @@ describe('Embed', () => {
});
test('GIVEN an embed with an invalid author name THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError();
});
@@ -277,7 +277,7 @@ describe('Embed', () => {
describe('Embed Footer', () => {
test('GIVEN an embed with a pre-defined footer THEN returns valid toJSON data', () => {
const embed = new Embed({
const embed = new EmbedBuilder({
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
expect(embed.toJSON()).toStrictEqual({
@@ -286,7 +286,7 @@ describe('Embed', () => {
});
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' });
expect(embed.toJSON()).toStrictEqual({
@@ -295,14 +295,16 @@ describe('Embed', () => {
});
test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => {
const embed = new Embed({ footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' } });
const embed = new EmbedBuilder({
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
embed.setFooter(null);
expect(embed.toJSON()).toStrictEqual({ footer: undefined });
});
test('GIVEN an embed with invalid footer text THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.setFooter({ text: 'a'.repeat(2049) })).toThrowError();
});
@@ -310,7 +312,7 @@ describe('Embed', () => {
describe('Embed Fields', () => {
test('GIVEN an embed with a pre-defined field THEN returns valid toJSON data', () => {
const embed = new Embed({
const embed = new EmbedBuilder({
fields: [{ name: 'foo', value: 'bar' }],
});
expect(embed.toJSON()).toStrictEqual({
@@ -319,7 +321,7 @@ describe('Embed', () => {
});
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.addFields({ name: 'foo', value: 'bar' });
expect(embed.toJSON()).toStrictEqual({
@@ -328,7 +330,7 @@ describe('Embed', () => {
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
@@ -337,7 +339,7 @@ describe('Embed', () => {
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
expect(() =>
@@ -346,7 +348,7 @@ describe('Embed', () => {
});
test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
expect(() =>
@@ -355,7 +357,7 @@ describe('Embed', () => {
});
test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() =>
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
@@ -363,7 +365,7 @@ describe('Embed', () => {
});
test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() =>
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
@@ -372,7 +374,7 @@ describe('Embed', () => {
describe('GIVEN invalid field amount THEN throws error', () => {
test('', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() =>
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
@@ -382,7 +384,7 @@ describe('Embed', () => {
describe('GIVEN invalid field name THEN throws error', () => {
test('', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
});
@@ -390,7 +392,7 @@ describe('Embed', () => {
describe('GIVEN invalid field name length THEN throws error', () => {
test('', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
});
@@ -398,7 +400,7 @@ describe('Embed', () => {
describe('GIVEN invalid field value length THEN throws error', () => {
test('', () => {
const embed = new Embed();
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError();
});

View File

@@ -1,26 +1,29 @@
import {
APIActionRowComponent,
type APIActionRowComponent,
ComponentType,
APIMessageActionRowComponent,
APIModalActionRowComponent,
ComponentType,
} from 'discord-api-types/v9';
import type { ButtonComponent, SelectMenuComponent, TextInputComponent } from '../index';
import { Component } from './Component';
import { createComponent } from './Components';
import isEqual from 'fast-deep-equal';
import type { ButtonBuilder, SelectMenuBuilder, TextInputBuilder } from '..';
import { ComponentBuilder } from './Component';
import { createComponentBuilder } from './Components';
export type MessageComponent = MessageActionRowComponent | ActionRow<MessageActionRowComponent>;
export type ModalComponent = ModalActionRowComponent | ActionRow<ModalActionRowComponent>;
export type MessageComponentBuilder =
| MessageActionRowComponentBuilder
| ActionRowBuilder<MessageActionRowComponentBuilder>;
export type ModalComponentBuilder = ModalActionRowComponentBuilder | ActionRowBuilder<ModalActionRowComponentBuilder>;
export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent;
export type ModalActionRowComponent = TextInputComponent;
export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder;
export type ModalActionRowComponentBuilder = TextInputBuilder;
/**
* Represents an action row component
*/
export class ActionRow<
T extends ModalActionRowComponent | MessageActionRowComponent = ModalActionRowComponent | MessageActionRowComponent,
> extends Component<
export class ActionRowBuilder<
T extends MessageActionRowComponentBuilder | ModalActionRowComponentBuilder =
| MessageActionRowComponentBuilder
| ModalActionRowComponentBuilder,
> extends ComponentBuilder<
Omit<
Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> & {
type: ComponentType.ActionRow;
@@ -31,14 +34,14 @@ export class ActionRow<
/**
* The components within this action row
*/
public readonly components: T[];
private readonly components: T[];
public constructor({
components,
...data
}: Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> = {}) {
super({ type: ComponentType.ActionRow, ...data });
this.components = (components?.map((c) => createComponent(c)) ?? []) as T[];
this.components = (components?.map((c) => createComponentBuilder(c)) ?? []) as T[];
}
/**
@@ -66,14 +69,4 @@ export class ActionRow<
components: this.components.map((component) => component.toJSON()) as ReturnType<T['toJSON']>[],
};
}
public equals(other: APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent> | ActionRow) {
if (other instanceof ActionRow) {
return isEqual(other.data, this.data) && isEqual(other.components, this.components);
}
return isEqual(other, {
...this.data,
components: this.components.map((component) => component.toJSON()),
});
}
}

View File

@@ -1,6 +1,6 @@
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9';
import { z } from 'zod';
import type { SelectMenuOption } from './selectMenu/SelectMenuOption';
import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption';
export const customIdValidator = z.string().min(1).max(100);
@@ -24,7 +24,7 @@ export const minMaxValidator = z.number().int().min(0).max(25);
export const optionsValidator = z.object({}).array().nonempty();
export function validateRequiredSelectMenuParameters(options: SelectMenuOption[], customId?: string) {
export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
customIdValidator.parse(customId);
optionsValidator.parse(options);
}

View File

@@ -4,17 +4,16 @@ import type {
APIActionRowComponentTypes,
APIBaseComponent,
APIMessageActionRowComponent,
APIModalActionRowComponent,
APIMessageComponent,
ComponentType,
APIModalActionRowComponent,
APIModalComponent,
ComponentType,
} from 'discord-api-types/v9';
import type { Equatable } from '../util/equatable';
/**
* Represents a discord component
*/
export abstract class Component<
export abstract class ComponentBuilder<
DataType extends Partial<APIBaseComponent<ComponentType>> & {
type: ComponentType;
} = APIBaseComponent<ComponentType>,
@@ -23,11 +22,6 @@ export abstract class Component<
| APIModalComponent
| APIMessageComponent
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>
>,
Equatable<
| Component
| APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>
>
{
/**
@@ -39,21 +33,7 @@ export abstract class Component<
| APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>;
public abstract equals(
other:
| Component
| APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>,
): boolean;
public constructor(data: DataType) {
this.data = data;
}
/**
* The type of this component
*/
public get type(): DataType['type'] {
return this.data.type;
}
}

View File

@@ -1,37 +1,41 @@
import { APIBaseComponent, APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, Component, SelectMenuComponent, TextInputComponent } from '../index';
import type { MessageComponent, ModalActionRowComponent } from './ActionRow';
import { APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v9';
import { ActionRowBuilder, ButtonBuilder, ComponentBuilder, SelectMenuBuilder, TextInputBuilder } from '../index';
import type { MessageComponentBuilder, ModalComponentBuilder } from './ActionRow';
export interface MappedComponentTypes {
[ComponentType.ActionRow]: ActionRow;
[ComponentType.Button]: ButtonComponent;
[ComponentType.SelectMenu]: SelectMenuComponent;
[ComponentType.TextInput]: TextInputComponent;
[ComponentType.ActionRow]: ActionRowBuilder;
[ComponentType.Button]: ButtonBuilder;
[ComponentType.SelectMenu]: SelectMenuBuilder;
[ComponentType.TextInput]: TextInputBuilder;
}
/**
* Factory for creating components from API data
* @param data The api data to transform to a component class
*/
export function createComponent<T extends keyof MappedComponentTypes>(
export function createComponentBuilder<T extends keyof MappedComponentTypes>(
data: (APIMessageComponent | APIModalComponent) & { type: T },
): MappedComponentTypes[T];
export function createComponent<C extends MessageComponent | ModalActionRowComponent>(data: C): C;
export function createComponent(data: APIModalComponent | APIMessageComponent | Component): Component {
if (data instanceof Component) {
export function createComponentBuilder<C extends MessageComponentBuilder | ModalComponentBuilder>(data: C): C;
export function createComponentBuilder(
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
): ComponentBuilder {
if (data instanceof ComponentBuilder) {
return data;
}
switch (data.type) {
case ComponentType.ActionRow:
return new ActionRow(data);
return new ActionRowBuilder(data);
case ComponentType.Button:
return new ButtonComponent(data);
return new ButtonBuilder(data);
case ComponentType.SelectMenu:
return new SelectMenuComponent(data);
return new SelectMenuBuilder(data);
case ComponentType.TextInput:
return new TextInputComponent(data);
return new TextInputBuilder(data);
default:
throw new Error(`Cannot serialize component type: ${(data as APIBaseComponent<ComponentType>).type}`);
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Cannot properly serialize component type: ${data.type}`);
}
}

View File

@@ -1,4 +1,10 @@
import type { ButtonStyle, APIMessageComponentEmoji, APIButtonComponent } from 'discord-api-types/v9';
import type {
ButtonStyle,
APIMessageComponentEmoji,
APIButtonComponent,
APIButtonComponentWithCustomId,
APIButtonComponentWithURL,
} from 'discord-api-types/v9';
import {
buttonLabelValidator,
buttonStyleValidator,
@@ -8,12 +14,12 @@ import {
urlValidator,
validateRequiredButtonParameters,
} from '../Assertions';
import { UnsafeButtonComponent } from './UnsafeButton';
import { UnsafeButtonBuilder } from './UnsafeButton';
/**
* Represents a validated button component
*/
export class ButtonComponent extends UnsafeButtonComponent {
export class ButtonBuilder extends UnsafeButtonBuilder {
public override setStyle(style: ButtonStyle) {
return super.setStyle(buttonStyleValidator.parse(style));
}
@@ -39,7 +45,13 @@ export class ButtonComponent extends UnsafeButtonComponent {
}
public override toJSON(): APIButtonComponent {
validateRequiredButtonParameters(this.style, this.label, this.emoji, this.customId, this.url);
validateRequiredButtonParameters(
this.data.style,
this.data.label,
this.data.emoji,
(this.data as APIButtonComponentWithCustomId).custom_id,
(this.data as APIButtonComponentWithURL).url,
);
return super.toJSON();
}
}

View File

@@ -6,59 +6,18 @@ import {
type APIButtonComponentWithURL,
type APIButtonComponentWithCustomId,
} from 'discord-api-types/v9';
import { Component } from '../Component';
import isEqual from 'fast-deep-equal';
import { ComponentBuilder } from '../Component';
/**
* Represents a non-validated button component
*/
export class UnsafeButtonComponent extends Component<Partial<APIButtonComponent> & { type: ComponentType.Button }> {
export class UnsafeButtonBuilder extends ComponentBuilder<
Partial<APIButtonComponent> & { type: ComponentType.Button }
> {
public constructor(data?: Partial<APIButtonComponent>) {
super({ type: ComponentType.Button, ...data });
}
/**
* The style of this button
*/
public get style() {
return this.data.style;
}
/**
* The label of this button
*/
public get label() {
return this.data.label;
}
/**
* The emoji used in this button
*/
public get emoji() {
return this.data.emoji;
}
/**
* Whether this button is disabled
*/
public get disabled() {
return this.data.disabled;
}
/**
* The custom id of this button (only defined on non-link buttons)
*/
public get customId(): string | undefined {
return (this.data as APIButtonComponentWithCustomId).custom_id;
}
/**
* The URL of this button (only defined on link buttons)
*/
public get url(): string | undefined {
return (this.data as APIButtonComponentWithURL).url;
}
/**
* Sets the style of this button
* @param style The style of the button
@@ -119,11 +78,4 @@ export class UnsafeButtonComponent extends Component<Partial<APIButtonComponent>
...this.data,
} as APIButtonComponent;
}
public equals(other: APIButtonComponent | UnsafeButtonComponent) {
if (other instanceof UnsafeButtonComponent) {
return isEqual(other.data, this.data);
}
return isEqual(other, this.data);
}
}

View File

@@ -6,12 +6,12 @@ import {
placeholderValidator,
validateRequiredSelectMenuParameters,
} from '../Assertions';
import { UnsafeSelectMenuComponent } from './UnsafeSelectMenu';
import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu';
/**
* Represents a validated select menu component
*/
export class SelectMenuComponent extends UnsafeSelectMenuComponent {
export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
public override setPlaceholder(placeholder: string) {
return super.setPlaceholder(placeholderValidator.parse(placeholder));
}
@@ -33,7 +33,7 @@ export class SelectMenuComponent extends UnsafeSelectMenuComponent {
}
public override toJSON(): APISelectMenuComponent {
validateRequiredSelectMenuParameters(this.options, this.customId);
validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
return super.toJSON();
}
}

View File

@@ -5,12 +5,12 @@ import {
labelValueValidator,
validateRequiredSelectMenuOptionParameters,
} from '../Assertions';
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption';
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
/**
* Represents a validated option within a select menu component
*/
export class SelectMenuOption extends UnsafeSelectMenuOption {
export class SelectMenuOptionBuilder extends UnsafeSelectMenuOptionBuilder {
public override setDescription(description: string) {
return super.setDescription(labelValueValidator.parse(description));
}
@@ -24,7 +24,7 @@ export class SelectMenuOption extends UnsafeSelectMenuOption {
}
public override toJSON(): APISelectMenuOption {
validateRequiredSelectMenuOptionParameters(this.label, this.value);
validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value);
return super.toJSON();
}
}

View File

@@ -1,58 +1,22 @@
import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9';
import { Component } from '../Component';
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption';
import isEqual from 'fast-deep-equal';
import { ComponentBuilder } from '../Component';
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
/**
* Represents a non-validated select menu component
*/
export class UnsafeSelectMenuComponent extends Component<
export class UnsafeSelectMenuBuilder extends ComponentBuilder<
Partial<Omit<APISelectMenuComponent, 'options'>> & { type: ComponentType.SelectMenu }
> {
/**
* The options within this select menu
*/
public readonly options: UnsafeSelectMenuOption[];
protected readonly options: UnsafeSelectMenuOptionBuilder[];
public constructor(data?: Partial<APISelectMenuComponent>) {
const { options, ...initData } = data ?? {};
super({ type: ComponentType.SelectMenu, ...initData });
this.options = options?.map((o) => new UnsafeSelectMenuOption(o)) ?? [];
}
/**
* The placeholder for this select menu
*/
public get placeholder() {
return this.data.placeholder;
}
/**
* The maximum amount of options that can be selected
*/
public get maxValues() {
return this.data.max_values;
}
/**
* The minimum amount of options that must be selected
*/
public get minValues() {
return this.data.min_values;
}
/**
* The custom id of this select menu
*/
public get customId() {
return this.data.custom_id;
}
/**
* Whether this select menu is disabled
*/
public get disabled() {
return this.data.disabled;
this.options = options?.map((o) => new UnsafeSelectMenuOptionBuilder(o)) ?? [];
}
/**
@@ -105,10 +69,10 @@ export class UnsafeSelectMenuComponent extends Component<
* @param options The options to add to this select menu
* @returns
*/
public addOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) {
public addOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
this.options.push(
...options.map((option) =>
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option),
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
),
);
return this;
@@ -118,12 +82,12 @@ export class UnsafeSelectMenuComponent extends Component<
* Sets the options on this select menu
* @param options The options to set on this select menu
*/
public setOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) {
public setOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
this.options.splice(
0,
this.options.length,
...options.map((option) =>
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option),
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
),
);
return this;
@@ -136,14 +100,4 @@ export class UnsafeSelectMenuComponent extends Component<
options: this.options.map((o) => o.toJSON()),
} as APISelectMenuComponent;
}
public equals(other: APISelectMenuComponent | UnsafeSelectMenuComponent): boolean {
if (other instanceof UnsafeSelectMenuComponent) {
return isEqual(other.data, this.data) && isEqual(other.options, this.options);
}
return isEqual(other, {
...this.data,
options: this.options.map((o) => o.toJSON()),
});
}
}

View File

@@ -3,43 +3,8 @@ import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-
/**
* Represents a non-validated option within a select menu component
*/
export class UnsafeSelectMenuOption {
public constructor(protected data: Partial<APISelectMenuOption> = {}) {}
/**
* The label for this option
*/
public get label() {
return this.data.label;
}
/**
* The value for this option
*/
public get value() {
return this.data.value;
}
/**
* The description for this option
*/
public get description() {
return this.data.description;
}
/**
* The emoji for this option
*/
public get emoji() {
return this.data.emoji;
}
/**
* Whether this option is selected by default
*/
public get default() {
return this.data.default;
}
export class UnsafeSelectMenuOptionBuilder {
public constructor(public data: Partial<APISelectMenuOption> = {}) {}
/**
* Sets the label of this option

View File

@@ -7,9 +7,9 @@ import {
valueValidator,
validateRequiredParameters,
} from './Assertions';
import { UnsafeTextInputComponent } from './UnsafeTextInput';
import { UnsafeTextInputBuilder } from './UnsafeTextInput';
export class TextInputComponent extends UnsafeTextInputComponent {
export class TextInputBuilder extends UnsafeTextInputBuilder {
public override setMinLength(minLength: number) {
return super.setMinLength(minLengthValidator.parse(minLength));
}

View File

@@ -1,73 +1,17 @@
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v9';
import { Component } from '../../index';
import { ComponentBuilder } from '../../index';
import isEqual from 'fast-deep-equal';
export class UnsafeTextInputComponent extends Component<
export class UnsafeTextInputBuilder extends ComponentBuilder<
Partial<APITextInputComponent> & { type: ComponentType.TextInput }
> {
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
super({ type: ComponentType.TextInput, ...data });
}
/**
* The style of this text input
*/
public get style() {
return this.data.style;
}
/**
* The custom id of this text input
*/
public get customId() {
return this.data.custom_id;
}
/**
* The label for this text input
*/
public get label() {
return this.data.label;
}
/**
* The placeholder text for this text input
*/
public get placeholder() {
return this.data.placeholder;
}
/**
* The default value for this text input
*/
public get value() {
return this.data.value;
}
/**
* The minimum length of this text input
*/
public get minLength() {
return this.data.min_length;
}
/**
* The maximum length of this text input
*/
public get maxLength() {
return this.data.max_length;
}
/**
* Whether this text input is required
*/
public get required() {
return this.data.required;
}
/**
* Sets the custom id for this text input
* @param customId The custom id of this text input
* @param customId The custom id of this text inputå
*/
public setCustomId(customId: string) {
this.data.custom_id = customId;
@@ -144,8 +88,8 @@ export class UnsafeTextInputComponent extends Component<
} as APITextInputComponent;
}
public equals(other: UnsafeTextInputComponent | APITextInputComponent): boolean {
if (other instanceof UnsafeTextInputComponent) {
public equals(other: UnsafeTextInputBuilder | APITextInputComponent): boolean {
if (other instanceof UnsafeTextInputBuilder) {
return isEqual(other.data, this.data);
}

View File

@@ -37,3 +37,5 @@ export * as ContextMenuCommandAssertions from './interactions/contextMenuCommand
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder';
export * from './util/jsonEncodable';
export * from './util/equatable';
export * from './util/componentUtil';

View File

@@ -1,14 +1,14 @@
import { z } from 'zod';
import { ActionRow, type ModalActionRowComponent } from '../..';
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../..';
import { customIdValidator } from '../../components/Assertions';
export const titleValidator = z.string().min(1).max(45);
export const componentsValidator = z.array(z.instanceof(ActionRow)).min(1);
export const componentsValidator = z.array(z.instanceof(ActionRowBuilder)).min(1);
export function validateRequiredParameters(
customId?: string,
title?: string,
components?: ActionRow<ModalActionRowComponent>[],
components?: ActionRowBuilder<ModalActionRowComponentBuilder>[],
) {
customIdValidator.parse(customId);
titleValidator.parse(title);

View File

@@ -1,9 +1,9 @@
import type { APIModalInteractionResponseCallbackData } from 'discord-api-types/v9';
import { customIdValidator } from '../../components/Assertions';
import { titleValidator, validateRequiredParameters } from './Assertions';
import { UnsafeModal } from './UnsafeModal';
import { UnsafeModalBuilder } from './UnsafeModal';
export class Modal extends UnsafeModal {
export class ModalBuilder extends UnsafeModalBuilder {
public override setCustomId(customId: string): this {
return super.setCustomId(customIdValidator.parse(customId));
}

View File

@@ -3,29 +3,16 @@ import type {
APIModalActionRowComponent,
APIModalInteractionResponseCallbackData,
} from 'discord-api-types/v9';
import { ActionRow, createComponent, JSONEncodable, ModalActionRowComponent } from '../../index';
import { ActionRowBuilder, createComponentBuilder, JSONEncodable, ModalActionRowComponentBuilder } from '../../index';
export class UnsafeModal implements JSONEncodable<APIModalInteractionResponseCallbackData> {
export class UnsafeModalBuilder implements JSONEncodable<APIModalInteractionResponseCallbackData> {
protected readonly data: Partial<Omit<APIModalInteractionResponseCallbackData, 'components'>>;
public readonly components: ActionRow<ModalActionRowComponent>[] = [];
public readonly components: ActionRowBuilder<ModalActionRowComponentBuilder>[] = [];
public constructor({ components, ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
this.data = { ...data };
this.components = (components?.map((c) => createComponent(c)) ?? []) as ActionRow<ModalActionRowComponent>[];
}
/**
* The custom id of this modal
*/
public get customId() {
return this.data.custom_id;
}
/**
* The title of this modal
*/
public get title() {
return this.data.title;
this.components = (components?.map((c) => createComponentBuilder(c)) ??
[]) as ActionRowBuilder<ModalActionRowComponentBuilder>[];
}
/**
@@ -51,11 +38,16 @@ export class UnsafeModal implements JSONEncodable<APIModalInteractionResponseCal
* @param components The components to add to this modal
*/
public addComponents(
...components: (ActionRow<ModalActionRowComponent> | APIActionRowComponent<APIModalActionRowComponent>)[]
...components: (
| ActionRowBuilder<ModalActionRowComponentBuilder>
| APIActionRowComponent<APIModalActionRowComponent>
)[]
) {
this.components.push(
...components.map((component) =>
component instanceof ActionRow ? component : new ActionRow<ModalActionRowComponent>(component),
component instanceof ActionRowBuilder
? component
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
),
);
return this;
@@ -65,7 +57,7 @@ export class UnsafeModal implements JSONEncodable<APIModalInteractionResponseCal
* Sets the components in this modal
* @param components The components to set this modal to
*/
public setComponents(...components: ActionRow<ModalActionRowComponent>[]) {
public setComponents(...components: ActionRowBuilder<ModalActionRowComponentBuilder>[]) {
this.components.splice(0, this.components.length, ...components);
return this;
}

View File

@@ -10,15 +10,15 @@ import {
urlPredicate,
validateFieldLength,
} from './Assertions';
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbed } from './UnsafeEmbed';
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbedBuilder } from './UnsafeEmbed';
/**
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
*/
export class Embed extends UnsafeEmbed {
export class EmbedBuilder extends UnsafeEmbedBuilder {
public override addFields(...fields: APIEmbedField[]): this {
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(fields.length, this.fields);
validateFieldLength(fields.length, this.data.fields);
// Data assertions
return super.addFields(...embedFieldsArrayPredicate.parse(fields));
@@ -26,7 +26,7 @@ export class Embed extends UnsafeEmbed {
public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(fields.length - deleteCount, this.fields);
validateFieldLength(fields.length - deleteCount, this.data.fields);
// Data assertions
return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields));

View File

@@ -1,13 +1,4 @@
import type {
APIEmbed,
APIEmbedAuthor,
APIEmbedField,
APIEmbedFooter,
APIEmbedImage,
APIEmbedVideo,
} from 'discord-api-types/v9';
import type { Equatable } from '../../util/equatable';
import isEqual from 'fast-deep-equal';
import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v9';
export type RGBTuple = [red: number, green: number, blue: number];
@@ -40,7 +31,7 @@ export interface EmbedImageData extends Omit<APIEmbedImage, 'proxy_url'> {
/**
* Represents a non-validated embed in a message (image/video preview, rich embed, etc.)
*/
export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
export class UnsafeEmbedBuilder {
public readonly data: APIEmbed;
public constructor(data: APIEmbed = {}) {
@@ -48,133 +39,6 @@ export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString();
}
/**
* An array of fields of this embed
*/
public get fields() {
return this.data.fields;
}
/**
* The embed title
*/
public get title() {
return this.data.title;
}
/**
* The embed description
*/
public get description() {
return this.data.description;
}
/**
* The embed URL
*/
public get url() {
return this.data.url;
}
/**
* The embed color
*/
public get color() {
return this.data.color;
}
/**
* The timestamp of the embed in an ISO 8601 format
*/
public get timestamp() {
return this.data.timestamp;
}
/**
* The embed thumbnail data
*/
public get thumbnail(): EmbedImageData | undefined {
if (!this.data.thumbnail) return undefined;
return {
url: this.data.thumbnail.url,
proxyURL: this.data.thumbnail.proxy_url,
height: this.data.thumbnail.height,
width: this.data.thumbnail.width,
};
}
/**
* The embed image data
*/
public get image(): EmbedImageData | undefined {
if (!this.data.image) return undefined;
return {
url: this.data.image.url,
proxyURL: this.data.image.proxy_url,
height: this.data.image.height,
width: this.data.image.width,
};
}
/**
* Received video data
*/
public get video(): APIEmbedVideo | undefined {
return this.data.video;
}
/**
* The embed author data
*/
public get author(): EmbedAuthorData | undefined {
if (!this.data.author) return undefined;
return {
name: this.data.author.name,
url: this.data.author.url,
iconURL: this.data.author.icon_url,
proxyIconURL: this.data.author.proxy_icon_url,
};
}
/**
* Received data about the embed provider
*/
public get provider() {
return this.data.provider;
}
/**
* The embed footer data
*/
public get footer(): EmbedFooterData | undefined {
if (!this.data.footer) return undefined;
return {
text: this.data.footer.text,
iconURL: this.data.footer.icon_url,
proxyIconURL: this.data.footer.proxy_icon_url,
};
}
/**
* The accumulated length for the embed title, description, fields, footer text, and author name
*/
public get length(): number {
return (
(this.data.title?.length ?? 0) +
(this.data.description?.length ?? 0) +
(this.data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) +
(this.data.footer?.text.length ?? 0) +
(this.data.author?.name.length ?? 0)
);
}
/**
* The hex color of the current color of the embed
*/
public get hexColor() {
return typeof this.data.color === 'number' ? `#${this.data.color.toString(16).padStart(6, '0')}` : this.data.color;
}
/**
* Adds fields to the embed (max 25)
*
@@ -204,7 +68,7 @@ export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
* @param fields The fields to set
*/
public setFields(...fields: APIEmbedField[]) {
this.spliceFields(0, this.fields?.length ?? 0, ...fields);
this.spliceFields(0, this.data.fields?.length ?? 0, ...fields);
return this;
}
@@ -319,11 +183,4 @@ export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
public toJSON(): APIEmbed {
return { ...this.data };
}
public equals(other: UnsafeEmbed | APIEmbed) {
const { image: thisImage, thumbnail: thisThumbnail, ...thisData } = this.data;
const data = other instanceof UnsafeEmbed ? other.data : other;
const { image, thumbnail, ...otherData } = data;
return isEqual(otherData, thisData) && image?.url === thisImage?.url && thumbnail?.url === thisThumbnail?.url;
}
}

View File

@@ -0,0 +1,11 @@
import type { APIEmbed } from 'discord-api-types/v9';
export function embedLength(data: APIEmbed) {
return (
(data.title?.length ?? 0) +
(data.description?.length ?? 0) +
(data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) +
(data.footer?.text.length ?? 0) +
(data.author?.name.length ?? 0)
);
}

View File

@@ -53,6 +53,7 @@
"@sapphire/snowflake": "^3.1.0",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.27.3",
"fast-deep-equal": "^3.1.3",
"lodash.snakecase": "^4.1.1",
"undici": "^4.14.1",
"ws": "^8.5.0"

View File

@@ -71,6 +71,7 @@ exports.WebSocketShard = require('./client/websocket/WebSocketShard');
// Structures
exports.ActionRow = require('./structures/ActionRow');
exports.ActionRowBuilder = require('./structures/ActionRowBuilder');
exports.Activity = require('./structures/Presence').Activity;
exports.AnonymousGuild = require('./structures/AnonymousGuild');
exports.Application = require('./structures/interfaces/Application');
@@ -81,6 +82,7 @@ exports.BaseGuild = require('./structures/BaseGuild');
exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji');
exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel');
exports.BaseGuildVoiceChannel = require('./structures/BaseGuildVoiceChannel');
exports.ButtonBuilder = require('./structures/ButtonBuilder');
exports.ButtonComponent = require('./structures/ButtonComponent');
exports.ButtonInteraction = require('./structures/ButtonInteraction');
exports.CategoryChannel = require('./structures/CategoryChannel');
@@ -92,9 +94,11 @@ exports.ClientUser = require('./structures/ClientUser');
exports.CommandInteraction = require('./structures/CommandInteraction');
exports.Collector = require('./structures/interfaces/Collector');
exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver');
exports.Component = require('./structures/Component');
exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction');
exports.DMChannel = require('./structures/DMChannel');
exports.Embed = require('./structures/Embed');
exports.EmbedBuilder = require('./structures/EmbedBuilder');
exports.UnsafeEmbed = require('@discordjs/builders').UnsafeEmbed;
exports.Emoji = require('./structures/Emoji').Emoji;
exports.Guild = require('./structures/Guild').Guild;
@@ -136,6 +140,7 @@ exports.ReactionCollector = require('./structures/ReactionCollector');
exports.ReactionEmoji = require('./structures/ReactionEmoji');
exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets;
exports.Role = require('./structures/Role').Role;
exports.SelectMenuBuilder = require('./structures/SelectMenuBuilder');
exports.SelectMenuComponent = require('./structures/SelectMenuComponent');
exports.SelectMenuInteraction = require('./structures/SelectMenuInteraction');
exports.StageChannel = require('./structures/StageChannel');
@@ -146,6 +151,7 @@ exports.StoreChannel = require('./structures/StoreChannel');
exports.Team = require('./structures/Team');
exports.TeamMember = require('./structures/TeamMember');
exports.TextChannel = require('./structures/TextChannel');
exports.TextInputBuilder = require('./structures/TextInputBuilder');
exports.TextInputComponent = require('./structures/TextInputComponent');
exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember');
@@ -191,6 +197,7 @@ exports.InviteTargetType = require('discord-api-types/v9').InviteTargetType;
exports.Locale = require('discord-api-types/v9').Locale;
exports.MessageType = require('discord-api-types/v9').MessageType;
exports.MessageFlags = require('discord-api-types/v9').MessageFlags;
exports.ModalBuilder = require('@discordjs/builders').ModalBuilder;
exports.OAuth2Scopes = require('discord-api-types/v9').OAuth2Scopes;
exports.PermissionFlagsBits = require('discord-api-types/v9').PermissionFlagsBits;
exports.RESTJSONErrorCodes = require('discord-api-types/v9').RESTJSONErrorCodes;
@@ -200,10 +207,10 @@ exports.StickerFormatType = require('discord-api-types/v9').StickerFormatType;
exports.TextInputStyle = require('discord-api-types/v9').TextInputStyle;
exports.UserFlags = require('discord-api-types/v9').UserFlags;
exports.WebhookType = require('discord-api-types/v9').WebhookType;
exports.UnsafeButtonComponent = require('@discordjs/builders').UnsafeButtonComponent;
exports.UnsafeSelectMenuComponent = require('@discordjs/builders').UnsafeSelectMenuComponent;
exports.SelectMenuOption = require('@discordjs/builders').SelectMenuOption;
exports.UnsafeSelectMenuOption = require('@discordjs/builders').UnsafeSelectMenuOption;
exports.UnsafeButtonBuilder = require('@discordjs/builders').UnsafeButtonBuilder;
exports.UnsafeSelectMenuBuilder = require('@discordjs/builders').UnsafeSelectMenuBuilder;
exports.SelectMenuOptionBuilder = require('@discordjs/builders').SelectMenuOptionBuilder;
exports.UnsafeSelectMenuOptionBuilder = require('@discordjs/builders').UnsafeSelectMenuOptionBuilder;
exports.DiscordAPIError = require('@discordjs/rest').DiscordAPIError;
exports.HTTPError = require('@discordjs/rest').HTTPError;
exports.RateLimitError = require('@discordjs/rest').RateLimitError;

View File

@@ -1,14 +1,21 @@
'use strict';
const { ActionRow: BuildersActionRow, Component } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Component = require('./Component');
const Components = require('../util/Components');
class ActionRow extends BuildersActionRow {
constructor({ components, ...data } = {}) {
super({
components: components?.map(c => (c instanceof Component ? c : Transformers.toSnakeCase(c))),
...Transformers.toSnakeCase(data),
});
/**
* Represents an action row
* @extends {Component}
*/
class ActionRow extends Component {
constructor({ components, ...data }) {
super(data);
/**
* The components in this action row
* @type {Component[]}
* @readonly
*/
this.components = components.map(c => Components.createComponent(c));
}
}

View File

@@ -0,0 +1,15 @@
'use strict';
const { ActionRowBuilder: BuildersActionRow, ComponentBuilder } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class ActionRowBuilder extends BuildersActionRow {
constructor({ components, ...data } = {}) {
super({
components: components?.map(c => (c instanceof ComponentBuilder ? c : Transformers.toSnakeCase(c))),
...Transformers.toSnakeCase(data),
});
}
}
module.exports = ActionRowBuilder;

View File

@@ -0,0 +1,24 @@
'use strict';
const { ButtonBuilder: BuildersButtonComponent, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class ButtonBuilder extends BuildersButtonComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
/**
* Creates a new button builder from json data
* @param {JSONEncodable<APIButtonComponent> | APIButtonComponent} other The other data
* @returns {ButtonBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = ButtonBuilder;

View File

@@ -1,11 +1,64 @@
'use strict';
const { ButtonComponent: BuildersButtonComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Component = require('./Component');
class ButtonComponent extends BuildersButtonComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
/**
* Represents a button component
* @extends {Component}
*/
class ButtonComponent extends Component {
/**
* The style of this button
* @type {ButtonStyle}
* @readonly
*/
get style() {
return this.data.style;
}
/**
* The label of this button
* @type {?string}
* @readonly
*/
get label() {
return this.data.label ?? null;
}
/**
* The emoji used in this button
* @type {?APIMessageComponentEmoji}
* @readonly
*/
get emoji() {
return this.data.emoji ?? null;
}
/**
* Whether this button is disabled
* @type {?boolean}
* @readonly
*/
get disabled() {
return this.data.disabled ?? null;
}
/**
* The custom id of this button (only defined on non-link buttons)
* @type {?string}
* @readonly
*/
get customId() {
return this.data.custom_id ?? null;
}
/**
* The URL of this button (only defined on link buttons)
* @type {?string}
* @readonly
*/
get url() {
return this.data.url ?? null;
}
}

View File

@@ -0,0 +1,52 @@
'use strict';
const isEqual = require('fast-deep-equal');
/**
* Represents a component
*/
class Component {
/**
* Creates a new component from API data
* @param {APIMessageComponent} data The API component data
* @private
*/
constructor(data) {
/**
* The API data associated with this component
* @type {APIMessageComponent}
*/
this.data = data;
}
/**
* The type of the component
* @type {ComponentType}
* @readonly
*/
get type() {
return this.data.type;
}
/**
* Whether or not the given components are equal
* @param {Component|APIMessageComponent} other The component to compare against
* @returns {boolean}
*/
equals(other) {
if (other instanceof Component) {
return isEqual(other.data, this.data);
}
return isEqual(other, this.data);
}
/**
* Returns the API-compatible JSON for this component
* @returns {APIMessageComponent}
*/
toJSON() {
return { ...this.data };
}
}
module.exports = Component;

View File

@@ -1,15 +1,198 @@
'use strict';
const { Embed: BuildersEmbed } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Util = require('../util/Util');
const isEqual = require('fast-deep-equal');
const { Util } = require('../util/Util');
/**
* Represents an embed object
*/
class Embed extends BuildersEmbed {
class Embed {
/**
* Creates a new embed object
* @param {APIEmbed} data API embed data
* @private
*/
constructor(data) {
super(Transformers.toSnakeCase(data));
/**
* The API embed data
* @type {APIEmbed}
* @readonly
*/
this.data = { ...data };
}
/**
* An array of fields of this embed
* @type {?Array<APIEmbedField>}
* @readonly
*/
get fields() {
return this.data.fields ?? null;
}
/**
* The embed title
* @type {?string}
* @readonly
*/
get title() {
return this.data.title ?? null;
}
/**
* The embed description
* @type {?string}
* @readonly
*/
get description() {
return this.data.description ?? null;
}
/**
* The embed URL
* @type {?string}
* @readonly
*/
get url() {
return this.data.url ?? null;
}
/**
* The embed color
* @type {?number}
* @readonly
*/
get color() {
return this.data.color ?? null;
}
/**
* The timestamp of the embed in an ISO 8601 format
* @type {?string}
* @readonly
*/
get timestamp() {
return this.data.timestamp ?? null;
}
/**
* The embed thumbnail data
* @type {?EmbedImageData}
* @readonly
*/
get thumbnail() {
if (!this.data.thumbnail) return null;
return {
url: this.data.thumbnail.url,
proxyURL: this.data.thumbnail.proxy_url,
height: this.data.thumbnail.height,
width: this.data.thumbnail.width,
};
}
/**
* The embed image data
* @type {?EmbedImageData}
* @readonly
*/
get image() {
if (!this.data.image) return null;
return {
url: this.data.image.url,
proxyURL: this.data.image.proxy_url,
height: this.data.image.height,
width: this.data.image.width,
};
}
/**
* Received video data
* @type {?EmbedVideoData}
* @readonly
*/
get video() {
return this.data.video ?? null;
}
/**
* The embed author data
* @type {?EmbedAuthorData}
* @readonly
*/
get author() {
if (!this.data.author) return null;
return {
name: this.data.author.name,
url: this.data.author.url,
iconURL: this.data.author.icon_url,
proxyIconURL: this.data.author.proxy_icon_url,
};
}
/**
* Received data about the embed provider
* @type {?EmbedProvider}
* @readonly
*/
get provider() {
return this.data.provider ?? null;
}
/**
* The embed footer data
* @type {?EmbedFooterData}
* @readonly
*/
get footer() {
if (!this.data.footer) return null;
return {
text: this.data.footer.text,
iconURL: this.data.footer.icon_url,
proxyIconURL: this.data.footer.proxy_icon_url,
};
}
/**
* The accumulated length for the embed title, description, fields, footer text, and author name
* @type {number}
* @readonly
*/
get length() {
return (
(this.data.title?.length ?? 0) +
(this.data.description?.length ?? 0) +
(this.data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) +
(this.data.footer?.text.length ?? 0) +
(this.data.author?.name.length ?? 0)
);
}
/**
* The hex color of the current color of the embed
* @type {?string}
* @readonly
*/
get hexColor() {
return typeof this.data.color === 'number'
? `#${this.data.color.toString(16).padStart(6, '0')}`
: this.data.color ?? null;
}
/**
* Returns the API-compatible JSON for this embed
* @returns {APIEmbed}
*/
toJSON() {
return { ...this.data };
}
/**
* Whether or not the given embeds are equal
* @param {Embed|APIEmbed} other The embed to compare against
* @returns {boolean}
*/
equals(other) {
if (other instanceof Embed) {
return isEqual(other.data, this.data);
}
return isEqual(other, this.data);
}
/**

View File

@@ -0,0 +1,24 @@
'use strict';
const { EmbedBuilder: BuildersEmbed, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class EmbedBuilder extends BuildersEmbed {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
/**
* Creates a new embed builder from json data
* @param {JSONEncodable<APIEmbed> | APIEmbed} other The other data
* @returns {EmbedBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = EmbedBuilder;

View File

@@ -1,6 +1,5 @@
'use strict';
const { createComponent, Embed } = require('@discordjs/builders');
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const {
@@ -12,6 +11,7 @@ const {
} = require('discord-api-types/v9');
const Base = require('./Base');
const ClientApplication = require('./ClientApplication');
const Embed = require('./Embed');
const InteractionCollector = require('./InteractionCollector');
const MessageAttachment = require('./MessageAttachment');
const Mentions = require('./MessageMentions');
@@ -20,6 +20,7 @@ const ReactionCollector = require('./ReactionCollector');
const { Sticker } = require('./Sticker');
const { Error } = require('../errors');
const ReactionManager = require('../managers/ReactionManager');
const Components = require('../util/Components');
const { NonSystemMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
@@ -145,7 +146,7 @@ class Message extends Base {
* A list of MessageActionRows in the message
* @type {ActionRow[]}
*/
this.components = data.components.map(c => createComponent(c));
this.components = data.components.map(c => Components.createComponent(c));
} else {
this.components = this.components?.slice() ?? [];
}

View File

@@ -1,12 +0,0 @@
'use strict';
const { Modal: BuildersModal } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class Modal extends BuildersModal {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
}
module.exports = Modal;

View File

@@ -0,0 +1,24 @@
'use strict';
const { SelectMenuBuilder: BuildersSelectMenuComponent, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class SelectMenuBuilder extends BuildersSelectMenuComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
/**
* Creates a new select menu builder from json data
* @param {JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent} other The other data
* @returns {SelectMenuBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = SelectMenuBuilder;

View File

@@ -1,11 +1,64 @@
'use strict';
const { SelectMenuComponent: BuildersSelectMenuComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Component = require('./Component');
class SelectMenuComponent extends BuildersSelectMenuComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
/**
* Represents a select menu component
* @extends {Component}
*/
class SelectMenuComponent extends Component {
/**
* The placeholder for this select menu
* @type {?string}
* @readonly
*/
get placeholder() {
return this.data.placeholder ?? null;
}
/**
* The maximum amount of options that can be selected
* @type {?number}
* @readonly
*/
get maxValues() {
return this.data.max_values ?? null;
}
/**
* The minimum amount of options that must be selected
* @type {?number}
* @readonly
*/
get minValues() {
return this.data.min_values ?? null;
}
/**
* The custom id of this select menu
* @type {string}
* @readonly
*/
get customId() {
return this.data.custom_id;
}
/**
* Whether this select menu is disabled
* @type {?boolean}
* @readonly
*/
get disabled() {
return this.data.disabled ?? null;
}
/**
* The options in this select menu
* @type {APISelectMenuOption[]}
* @readonly
*/
get options() {
return this.data.options;
}
}

View File

@@ -0,0 +1,24 @@
'use strict';
const { TextInputBuilder: BuildersTextInputComponent, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class TextInputBuilder extends BuildersTextInputComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
/**
* Creates a new text input builder from json data
* @param {JSONEncodable<APITextInputComponent> | APITextInputComponent} other The other data
* @returns {TextInputBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = TextInputBuilder;

View File

@@ -1,11 +1,20 @@
'use strict';
const { TextInputComponent: BuildersTextInputComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Component = require('./Component');
class TextInputComponent extends BuildersTextInputComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
class TextInputComponent extends Component {
/**
* The custom id of this text input
*/
get customId() {
return this.data.custom_id;
}
/**
* The value for this text input
*/
get value() {
return this.data.value;
}
}

View File

@@ -1,7 +1,7 @@
'use strict';
// This file contains the typedefs for camel-cased json data
const { ComponentType } = require('discord-api-types/v9');
/**
* @typedef {Object} BaseComponentData
* @property {ComponentType} type The type of component
@@ -56,3 +56,34 @@
/**
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData} ComponentData
*/
class Components extends null {
/**
* Transforms API data into a component
* @param {APIMessageComponent|Component} data The data to create the component from
* @returns {Component}
*/
static createComponent(data) {
if (data instanceof Component) {
return data;
}
switch (data.type) {
case ComponentType.ActionRow:
return new ActionRow(data);
case ComponentType.Button:
return new ButtonComponent(data);
case ComponentType.SelectMenu:
return new SelectMenuComponent(data);
default:
throw new Error(`Found unknown component type: ${data.type}`);
}
}
}
module.exports = Components;
const ActionRow = require('../structures/ActionRow');
const ButtonComponent = require('../structures/ButtonComponent');
const Component = require('../structures/Component');
const SelectMenuComponent = require('../structures/SelectMenuComponent');

View File

@@ -1,24 +1,24 @@
import {
ActionRow as BuilderActionRow,
MessageActionRowComponent,
ActionRowBuilder as BuilderActionRow,
MessageActionRowComponentBuilder,
blockQuote,
bold,
ButtonComponent as BuilderButtonComponent,
ButtonBuilder as BuilderButtonComponent,
channelMention,
codeBlock,
Component,
Embed as BuildersEmbed,
EmbedBuilder as BuildersEmbed,
formatEmoji,
hideLinkEmbed,
hyperlink,
inlineCode,
italic,
JSONEncodable,
MappedComponentTypes,
memberNicknameMention,
Modal as BuilderModal,
quote,
roleMention,
SelectMenuComponent as BuilderSelectMenuComponent,
TextInputComponent as BuilderTextInputComponent,
SelectMenuBuilder as BuilderSelectMenuComponent,
TextInputBuilder as BuilderTextInputComponent,
spoiler,
strikethrough,
time,
@@ -26,7 +26,7 @@ import {
TimestampStylesString,
underscore,
userMention,
ModalActionRowComponent,
ModalActionRowComponentBuilder,
} from '@discordjs/builders';
import { Collection } from '@discordjs/collection';
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
@@ -105,6 +105,11 @@ import {
APITextInputComponent,
APIModalActionRowComponent,
APIModalComponent,
APISelectMenuOption,
APIEmbedField,
APIEmbedAuthor,
APIEmbedFooter,
APIEmbedImage,
} from 'discord-api-types/v9';
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
@@ -220,8 +225,10 @@ export interface ActionRowData<T extends ActionRowComponent | ActionRowComponent
components: T[];
}
export class ActionRow<
T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent,
export class ActionRowBuilder<
T extends MessageActionRowComponentBuilder | ModalActionRowComponentBuilder =
| MessageActionRowComponentBuilder
| ModalActionRowComponentBuilder,
> extends BuilderActionRow<T> {
constructor(
data?:
@@ -232,6 +239,14 @@ export class ActionRow<
);
}
export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent;
export type ModalActionRowComponent = TextInputComponent;
export class ActionRow<T extends MessageActionRowComponent | ModalActionRowComponent> {
private constructor(data: APIActionRowComponent<APIMessageActionRowComponent>);
public readonly components: T[];
}
export class ActivityFlagsBitField extends BitField<ActivityFlagsString> {
public static Flags: typeof ActivityFlags;
public static resolve(bit?: BitFieldResolvable<ActivityFlagsString, number>): number;
@@ -356,7 +371,9 @@ export interface InteractionResponseFields<Cached extends CacheType = CacheType>
deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
fetchReply(): Promise<GuildCacheMessage<Cached>>;
followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise<void>;
showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
): Promise<void>;
}
export abstract class CommandInteraction<Cached extends CacheType = CacheType> extends Interaction<Cached> {
@@ -395,7 +412,9 @@ export abstract class CommandInteraction<Cached extends CacheType = CacheType> e
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
public showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise<void>;
public showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
): Promise<void>;
private transformOption(
option: APIApplicationCommandOption,
resolved: APIApplicationCommandInteractionData['resolved'],
@@ -502,22 +521,53 @@ export class ButtonInteraction<Cached extends CacheType = CacheType> extends Mes
public inRawGuild(): this is ButtonInteraction<'raw'>;
}
export class ButtonComponent extends BuilderButtonComponent {
public constructor(data?: ButtonComponentData | (Omit<APIButtonComponent, 'type'> & { type?: ComponentType.Button }));
export class Component<T extends APIMessageComponent | APIModalComponent = APIMessageComponent | APIModalComponent> {
public readonly data: Readonly<T>;
public get type(): T['type'];
public toJSON(): T;
public equals(other: this | T): boolean;
}
export class SelectMenuComponent extends BuilderSelectMenuComponent {
export class ButtonComponent extends Component<APIButtonComponent> {
private constructor(data: APIButtonComponent);
public get style(): ButtonStyle;
public get label(): string | null;
public get emoji(): APIMessageComponentEmoji | null;
public get disabled(): boolean | null;
public get customId(): string | null;
public get url(): string | null;
}
export class ButtonBuilder extends BuilderButtonComponent {
public constructor(data?: ButtonComponentData | (Omit<APIButtonComponent, 'type'> & { type?: ComponentType.Button }));
public static from(other: JSONEncodable<APIButtonComponent> | APIButtonComponent): ButtonBuilder;
}
export class SelectMenuBuilder extends BuilderSelectMenuComponent {
public constructor(
data?: SelectMenuComponentData | (Omit<APISelectMenuComponent, 'type'> & { type?: ComponentType.SelectMenu }),
);
public static from(other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent): SelectMenuBuilder;
}
export class TextInputComponent extends BuilderTextInputComponent {
export class TextInputBuilder extends BuilderTextInputComponent {
public constructor(data?: TextInputComponentData | APITextInputComponent);
public static from(other: JSONEncodable<APITextInputComponent> | APITextInputComponent): TextInputBuilder;
}
export class Modal extends BuilderModal {
public constructor(data?: ModalData | APIModalActionRowComponent);
export class TextInputComponent extends Component<APITextInputComponent> {
public get customId(): string;
public get value(): string;
}
export class SelectMenuComponent extends Component<APISelectMenuComponent> {
private constructor(data: APISelectMenuComponent);
public get placeholder(): string | null;
public get maxValues(): number | null;
public get minValues(): number | null;
public get customId(): string;
public get disabled(): boolean | null;
public get options(): APISelectMenuOption[];
}
export interface EmbedData {
@@ -535,18 +585,43 @@ export interface EmbedData {
fields?: EmbedFieldData[];
}
export interface EmbedImageData {
url?: string;
export interface IconData {
iconURL?: string;
proxyIconURL?: string;
}
export type EmbedAuthorData = Omit<APIEmbedAuthor, 'icon_url' | 'proxy_icon_url'> & IconData;
export type EmbedFooterData = Omit<APIEmbedFooter, 'icon_url' | 'proxy_icon_url'> & IconData;
export interface EmbedProviderData {
name?: string;
url?: string;
}
export class Embed extends BuildersEmbed {
export interface EmbedImageData extends Omit<APIEmbedImage, 'proxy_url'> {
proxyURL?: string;
}
export class EmbedBuilder extends BuildersEmbed {
public constructor(data?: EmbedData | APIEmbed);
public override setColor(color: ColorResolvable | null): this;
public static from(other: JSONEncodable<APIEmbed> | APIEmbed): EmbedBuilder;
}
export class Embed {
private constructor(data: APIEmbed);
public readonly data: Readonly<APIEmbed>;
public get fields(): APIEmbedField[] | null;
public get title(): string | null;
public get description(): string | null;
public get url(): string | null;
public get color(): number | null;
public get timestamp(): string | null;
public get thumbnail(): EmbedImageData | null;
public get image(): EmbedImageData | null;
public equals(other: Embed | APIEmbed): boolean;
public toJSON(): APIEmbed;
}
export interface MappedChannelCategoryTypes {
@@ -1652,7 +1727,9 @@ export class MessageComponentInteraction<Cached extends CacheType = CacheType> e
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public update(options: string | MessagePayload | InteractionUpdateOptions): Promise<void>;
public showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise<void>;
public showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
): Promise<void>;
}
export class MessageContextMenuCommandInteraction<
@@ -1741,17 +1818,11 @@ export class MessageReaction {
public toJSON(): unknown;
}
export interface ModalFieldData {
value: string;
type: ComponentType;
customId: string;
}
export class ModalSubmitFieldsResolver {
constructor(components: ModalFieldData[][]);
public components: ModalFieldData[][];
public fields: Collection<string, ModalFieldData>;
public getField(customId: string): ModalFieldData;
constructor(components: ModalActionRowComponent[][]);
public components: ActionRow<ModalActionRowComponent>;
public fields: Collection<string, ModalActionRowComponent>;
public getField(customId: string): ModalActionRowComponent;
public getTextInputValue(customId: string): string;
}
@@ -1769,7 +1840,7 @@ export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = C
export interface ModalSubmitActionRow {
type: ComponentType.ActionRow;
components: ModalFieldData[];
components: ActionRow<TextInputComponent>[];
}
export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extends Interaction<Cached> {
@@ -2452,6 +2523,14 @@ export class Util extends null {
public static splitMessage(text: string, options?: SplitOptions): string[];
}
export class Components extends null {
public static createComponentBuilder<T extends keyof MappedComponentTypes>(
data: APIMessageComponent & { type: T },
): MappedComponentTypes[T];
public static createComponentBuilder<C extends Component>(data: C): C;
public static createComponentBuilder(data: APIMessageComponent | Component): Component;
}
export class Formatters extends null {
public static blockQuote: typeof blockQuote;
public static bold: typeof bold;
@@ -3966,12 +4045,6 @@ export interface EditGuildTemplateOptions {
description?: string;
}
export interface EmbedAuthorData {
name: string;
url?: string;
iconURL?: string;
}
export interface EmbedField {
name: string;
value: string;
@@ -3984,11 +4057,6 @@ export interface EmbedFieldData {
inline?: boolean;
}
export interface EmbedFooterData {
text: string;
iconURL?: string;
}
export type EmojiIdentifierResolvable = string | EmojiResolvable;
export type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji;
@@ -4646,7 +4714,11 @@ export interface MessageCollectorOptions extends CollectorOptions<[Message]> {
maxProcessed?: number;
}
export type MessageComponent = Component | ActionRow<MessageActionRowComponent> | ButtonComponent | SelectMenuComponent;
export type MessageComponent =
| Component
| ActionRowBuilder<MessageActionRowComponentBuilder | ModalActionRowComponentBuilder>
| ButtonComponent
| SelectMenuComponent;
export type MessageComponentCollectorOptions<T extends MessageComponentInteraction> = Omit<
InteractionCollectorOptions<T>,
@@ -4666,6 +4738,7 @@ export interface MessageEditOptions {
flags?: BitFieldResolvable<MessageFlagsString, number>;
allowedMentions?: MessageMentionOptions;
components?: (
| JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>
| ActionRow<MessageActionRowComponent>
| (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData | MessageActionRowComponent>)
| APIActionRowComponent<APIMessageActionRowComponent>
@@ -4705,8 +4778,9 @@ export interface MessageOptions {
tts?: boolean;
nonce?: string | number;
content?: string | null;
embeds?: (Embed | APIEmbed)[];
embeds?: (JSONEncodable<APIEmbed> | APIEmbed)[];
components?: (
| JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>
| ActionRow<MessageActionRowComponent>
| (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData | MessageActionRowComponent>)
| APIActionRowComponent<APIMessageActionRowComponent>
@@ -5255,6 +5329,8 @@ export {
ApplicationCommandType,
ApplicationCommandOptionType,
ApplicationCommandPermissionType,
APIEmbedField,
APISelectMenuOption,
AuditLogEvent,
ButtonStyle,
ChannelType,
@@ -5290,12 +5366,13 @@ export {
WebhookType,
} from 'discord-api-types/v9';
export {
UnsafeButtonComponent,
UnsafeSelectMenuComponent,
SelectMenuOption,
UnsafeSelectMenuOption,
MessageActionRowComponent,
UnsafeEmbed,
ModalActionRowComponent,
UnsafeButtonBuilder,
UnsafeSelectMenuBuilder,
SelectMenuOptionBuilder,
UnsafeSelectMenuOptionBuilder,
MessageActionRowComponentBuilder,
ModalActionRowComponentBuilder,
UnsafeEmbedBuilder,
ModalBuilder,
} from '@discordjs/builders';
export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';

View File

@@ -20,6 +20,8 @@ import {
AuditLogEvent,
ButtonStyle,
TextInputStyle,
APITextInputComponent,
APIEmbed,
} from 'discord-api-types/v9';
import {
ApplicationCommand,
@@ -59,7 +61,7 @@ import {
MessageCollector,
MessageComponentInteraction,
MessageReaction,
Modal,
ModalBuilder,
NewsChannel,
Options,
PartialTextBasedChannelFields,
@@ -95,10 +97,10 @@ import {
GuildAuditLogs,
StageInstance,
PartialDMChannel,
ActionRow,
ActionRowBuilder,
ButtonComponent,
SelectMenuComponent,
MessageActionRowComponent,
MessageActionRowComponentBuilder,
InteractionResponseFields,
ThreadChannelType,
Events,
@@ -109,6 +111,12 @@ import {
MessageActionRowComponentData,
PartialThreadMember,
ThreadMemberFlagsBitField,
ButtonBuilder,
EmbedBuilder,
MessageActionRowComponent,
SelectMenuBuilder,
TextInputBuilder,
TextInputComponent,
Embed,
} from '.';
import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd';
@@ -574,7 +582,7 @@ client.on('messageCreate', async message => {
assertIsMessage(channel.send({ embeds: [] }));
const attachment = new MessageAttachment('file.png');
const embed = new Embed();
const embed = new EmbedBuilder();
assertIsMessage(channel.send({ files: [attachment] }));
assertIsMessage(channel.send({ embeds: [embed] }));
assertIsMessage(channel.send({ embeds: [embed], files: [attachment] }));
@@ -744,23 +752,24 @@ client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
void new ActionRow<MessageActionRowComponent>();
void new ActionRowBuilder<MessageActionRowComponentBuilder>();
const button = new ButtonComponent();
const button = new ButtonBuilder();
const actionRow = new ActionRow<MessageActionRowComponent>({
const actionRow = new ActionRowBuilder<MessageActionRowComponentBuilder>({
type: ComponentType.ActionRow,
components: [button.toJSON()],
});
actionRow.toJSON();
await interaction.reply({ content: 'Hi!', components: [actionRow] });
// @ts-expect-error
interaction.reply({ content: 'Hi!', components: [[button]] });
// @ts-expect-error
void new ActionRow({});
void new ActionRowBuilder({});
// @ts-expect-error
await interaction.reply({ content: 'Hi!', components: [button] });
@@ -1336,34 +1345,34 @@ expectType<CategoryChannel | NewsChannel | StageChannel | StoreChannel | TextCha
);
expectType<NewsChannel | TextChannel | ThreadChannel>(GuildTextBasedChannel);
const button = new ButtonComponent({
const button = new ButtonBuilder({
label: 'test',
style: ButtonStyle.Primary,
customId: 'test',
});
const selectMenu = new SelectMenuComponent({
const selectMenu = new SelectMenuBuilder({
maxValues: 10,
minValues: 2,
customId: 'test',
});
new ActionRow({
new ActionRowBuilder({
components: [selectMenu.toJSON(), button.toJSON()],
});
new SelectMenuComponent({
new SelectMenuBuilder({
customId: 'foo',
});
new ButtonComponent({
new ButtonBuilder({
style: ButtonStyle.Danger,
});
// @ts-expect-error
new Embed().setColor('abc');
new EmbedBuilder().setColor('abc');
new Embed().setColor('#ffffff');
new EmbedBuilder().setColor('#ffffff');
expectNotAssignable<ActionRowData<MessageActionRowComponentData>>({
type: ComponentType.ActionRow,
@@ -1379,7 +1388,7 @@ declare const chatInputInteraction: ChatInputCommandInteraction;
expectType<MessageAttachment>(chatInputInteraction.options.getAttachment('attachment', true));
expectType<MessageAttachment | null>(chatInputInteraction.options.getAttachment('attachment'));
declare const modal: Modal;
declare const modal: ModalBuilder;
chatInputInteraction.showModal(modal);
@@ -1400,3 +1409,27 @@ chatInputInteraction.showModal({
},
],
});
declare const selectMenuData: APISelectMenuComponent;
SelectMenuBuilder.from(selectMenuData);
declare const selectMenuComp: SelectMenuComponent;
SelectMenuBuilder.from(selectMenuComp);
declare const buttonData: APIButtonComponent;
ButtonBuilder.from(buttonData);
declare const buttonComp: ButtonComponent;
ButtonBuilder.from(buttonComp);
declare const textInputData: APITextInputComponent;
TextInputBuilder.from(textInputData);
declare const textInputComp: TextInputComponent;
TextInputBuilder.from(textInputComp);
declare const embedData: APIEmbed;
EmbedBuilder.from(embedData);
declare const embedComp: Embed;
EmbedBuilder.from(embedComp);

View File

@@ -4446,6 +4446,7 @@ __metadata:
eslint-config-prettier: ^8.3.0
eslint-plugin-import: ^2.25.4
eslint-plugin-prettier: ^4.0.0
fast-deep-equal: ^3.1.3
husky: ^7.0.4
is-ci: ^3.0.1
jest: ^27.5.1