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

View File

@@ -5,9 +5,9 @@ import {
ComponentType, ComponentType,
} from 'discord-api-types/v9'; } from 'discord-api-types/v9';
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions'; 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 = const longStr =
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong'; 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
@@ -119,7 +119,7 @@ describe('Button Components', () => {
disabled: true, disabled: true,
}; };
expect(new ButtonComponent(interactionData).toJSON()).toEqual(interactionData); expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
expect( expect(
buttonComponent() buttonComponent()
@@ -138,21 +138,9 @@ describe('Button Components', () => {
url: 'https://google.com', 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)); 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 { 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 selectMenu = () => new SelectMenuBuilder();
const selectMenuOption = () => new SelectMenuOption(); const selectMenuOption = () => new SelectMenuOptionBuilder();
const longStr = 'a'.repeat(256); const longStr = 'a'.repeat(256);
@@ -44,8 +44,8 @@ describe('Select Menu Components', () => {
.setEmoji({ name: 'test' }) .setEmoji({ name: 'test' })
.setDescription('description'); .setDescription('description');
expect(() => selectMenu().addOptions(option)).not.toThrowError(); expect(() => selectMenu().addOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions([option])).not.toThrowError(); expect(() => selectMenu().setOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError(); expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError();
}); });
test('GIVEN invalid inputs THEN Select Menu does throw', () => { 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', () => { test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
expect( expect(
new SelectMenuComponent(selectMenuDataWithoutOptions) new SelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions(new SelectMenuOption(selectMenuOptionData)) .addOptions(new SelectMenuOptionBuilder(selectMenuOptionData))
.toJSON(), .toJSON(),
).toEqual(selectMenuData); ).toEqual(selectMenuData);
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); expect(new SelectMenuOptionBuilder(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();
}); });
}); });
}); });

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,29 @@
import { import {
APIActionRowComponent, type APIActionRowComponent,
ComponentType,
APIMessageActionRowComponent, APIMessageActionRowComponent,
APIModalActionRowComponent, APIModalActionRowComponent,
ComponentType,
} from 'discord-api-types/v9'; } from 'discord-api-types/v9';
import type { ButtonComponent, SelectMenuComponent, TextInputComponent } from '../index'; import type { ButtonBuilder, SelectMenuBuilder, TextInputBuilder } from '..';
import { Component } from './Component'; import { ComponentBuilder } from './Component';
import { createComponent } from './Components'; import { createComponentBuilder } from './Components';
import isEqual from 'fast-deep-equal';
export type MessageComponent = MessageActionRowComponent | ActionRow<MessageActionRowComponent>; export type MessageComponentBuilder =
export type ModalComponent = ModalActionRowComponent | ActionRow<ModalActionRowComponent>; | MessageActionRowComponentBuilder
| ActionRowBuilder<MessageActionRowComponentBuilder>;
export type ModalComponentBuilder = ModalActionRowComponentBuilder | ActionRowBuilder<ModalActionRowComponentBuilder>;
export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent; export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder;
export type ModalActionRowComponent = TextInputComponent; export type ModalActionRowComponentBuilder = TextInputBuilder;
/** /**
* Represents an action row component * Represents an action row component
*/ */
export class ActionRow< export class ActionRowBuilder<
T extends ModalActionRowComponent | MessageActionRowComponent = ModalActionRowComponent | MessageActionRowComponent, T extends MessageActionRowComponentBuilder | ModalActionRowComponentBuilder =
> extends Component< | MessageActionRowComponentBuilder
| ModalActionRowComponentBuilder,
> extends ComponentBuilder<
Omit< Omit<
Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> & { Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> & {
type: ComponentType.ActionRow; type: ComponentType.ActionRow;
@@ -31,14 +34,14 @@ export class ActionRow<
/** /**
* The components within this action row * The components within this action row
*/ */
public readonly components: T[]; private readonly components: T[];
public constructor({ public constructor({
components, components,
...data ...data
}: Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> = {}) { }: Partial<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>> = {}) {
super({ type: ComponentType.ActionRow, ...data }); 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']>[], 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 { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9';
import { z } from 'zod'; 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); 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 const optionsValidator = z.object({}).array().nonempty();
export function validateRequiredSelectMenuParameters(options: SelectMenuOption[], customId?: string) { export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
customIdValidator.parse(customId); customIdValidator.parse(customId);
optionsValidator.parse(options); optionsValidator.parse(options);
} }

View File

@@ -4,17 +4,16 @@ import type {
APIActionRowComponentTypes, APIActionRowComponentTypes,
APIBaseComponent, APIBaseComponent,
APIMessageActionRowComponent, APIMessageActionRowComponent,
APIModalActionRowComponent,
APIMessageComponent, APIMessageComponent,
ComponentType, APIModalActionRowComponent,
APIModalComponent, APIModalComponent,
ComponentType,
} from 'discord-api-types/v9'; } from 'discord-api-types/v9';
import type { Equatable } from '../util/equatable';
/** /**
* Represents a discord component * Represents a discord component
*/ */
export abstract class Component< export abstract class ComponentBuilder<
DataType extends Partial<APIBaseComponent<ComponentType>> & { DataType extends Partial<APIBaseComponent<ComponentType>> & {
type: ComponentType; type: ComponentType;
} = APIBaseComponent<ComponentType>, } = APIBaseComponent<ComponentType>,
@@ -23,11 +22,6 @@ export abstract class Component<
| APIModalComponent | APIModalComponent
| APIMessageComponent | APIMessageComponent
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent> | APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>
>,
Equatable<
| Component
| APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>
> >
{ {
/** /**
@@ -39,21 +33,7 @@ export abstract class Component<
| APIActionRowComponentTypes | APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>; | APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>;
public abstract equals(
other:
| Component
| APIActionRowComponentTypes
| APIActionRowComponent<APIModalActionRowComponent | APIMessageActionRowComponent>,
): boolean;
public constructor(data: DataType) { public constructor(data: DataType) {
this.data = data; 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 { APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, Component, SelectMenuComponent, TextInputComponent } from '../index'; import { ActionRowBuilder, ButtonBuilder, ComponentBuilder, SelectMenuBuilder, TextInputBuilder } from '../index';
import type { MessageComponent, ModalActionRowComponent } from './ActionRow'; import type { MessageComponentBuilder, ModalComponentBuilder } from './ActionRow';
export interface MappedComponentTypes { export interface MappedComponentTypes {
[ComponentType.ActionRow]: ActionRow; [ComponentType.ActionRow]: ActionRowBuilder;
[ComponentType.Button]: ButtonComponent; [ComponentType.Button]: ButtonBuilder;
[ComponentType.SelectMenu]: SelectMenuComponent; [ComponentType.SelectMenu]: SelectMenuBuilder;
[ComponentType.TextInput]: TextInputComponent; [ComponentType.TextInput]: TextInputBuilder;
} }
/** /**
* Factory for creating components from API data * Factory for creating components from API data
* @param data The api data to transform to a component class * @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 }, data: (APIMessageComponent | APIModalComponent) & { type: T },
): MappedComponentTypes[T]; ): MappedComponentTypes[T];
export function createComponent<C extends MessageComponent | ModalActionRowComponent>(data: C): C; export function createComponentBuilder<C extends MessageComponentBuilder | ModalComponentBuilder>(data: C): C;
export function createComponent(data: APIModalComponent | APIMessageComponent | Component): Component { export function createComponentBuilder(
if (data instanceof Component) { data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
): ComponentBuilder {
if (data instanceof ComponentBuilder) {
return data; return data;
} }
switch (data.type) { switch (data.type) {
case ComponentType.ActionRow: case ComponentType.ActionRow:
return new ActionRow(data); return new ActionRowBuilder(data);
case ComponentType.Button: case ComponentType.Button:
return new ButtonComponent(data); return new ButtonBuilder(data);
case ComponentType.SelectMenu: case ComponentType.SelectMenu:
return new SelectMenuComponent(data); return new SelectMenuBuilder(data);
case ComponentType.TextInput: case ComponentType.TextInput:
return new TextInputComponent(data); return new TextInputBuilder(data);
default: 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 { import {
buttonLabelValidator, buttonLabelValidator,
buttonStyleValidator, buttonStyleValidator,
@@ -8,12 +14,12 @@ import {
urlValidator, urlValidator,
validateRequiredButtonParameters, validateRequiredButtonParameters,
} from '../Assertions'; } from '../Assertions';
import { UnsafeButtonComponent } from './UnsafeButton'; import { UnsafeButtonBuilder } from './UnsafeButton';
/** /**
* Represents a validated button component * Represents a validated button component
*/ */
export class ButtonComponent extends UnsafeButtonComponent { export class ButtonBuilder extends UnsafeButtonBuilder {
public override setStyle(style: ButtonStyle) { public override setStyle(style: ButtonStyle) {
return super.setStyle(buttonStyleValidator.parse(style)); return super.setStyle(buttonStyleValidator.parse(style));
} }
@@ -39,7 +45,13 @@ export class ButtonComponent extends UnsafeButtonComponent {
} }
public override toJSON(): APIButtonComponent { 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(); return super.toJSON();
} }
} }

View File

@@ -6,59 +6,18 @@ import {
type APIButtonComponentWithURL, type APIButtonComponentWithURL,
type APIButtonComponentWithCustomId, type APIButtonComponentWithCustomId,
} from 'discord-api-types/v9'; } from 'discord-api-types/v9';
import { Component } from '../Component'; import { ComponentBuilder } from '../Component';
import isEqual from 'fast-deep-equal';
/** /**
* Represents a non-validated button 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>) { public constructor(data?: Partial<APIButtonComponent>) {
super({ type: ComponentType.Button, ...data }); 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 * Sets the style of this button
* @param style The style of the button * @param style The style of the button
@@ -119,11 +78,4 @@ export class UnsafeButtonComponent extends Component<Partial<APIButtonComponent>
...this.data, ...this.data,
} as APIButtonComponent; } 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, placeholderValidator,
validateRequiredSelectMenuParameters, validateRequiredSelectMenuParameters,
} from '../Assertions'; } from '../Assertions';
import { UnsafeSelectMenuComponent } from './UnsafeSelectMenu'; import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu';
/** /**
* Represents a validated select menu component * Represents a validated select menu component
*/ */
export class SelectMenuComponent extends UnsafeSelectMenuComponent { export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
public override setPlaceholder(placeholder: string) { public override setPlaceholder(placeholder: string) {
return super.setPlaceholder(placeholderValidator.parse(placeholder)); return super.setPlaceholder(placeholderValidator.parse(placeholder));
} }
@@ -33,7 +33,7 @@ export class SelectMenuComponent extends UnsafeSelectMenuComponent {
} }
public override toJSON(): APISelectMenuComponent { public override toJSON(): APISelectMenuComponent {
validateRequiredSelectMenuParameters(this.options, this.customId); validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
return super.toJSON(); return super.toJSON();
} }
} }

View File

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

View File

@@ -1,58 +1,22 @@
import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9';
import { Component } from '../Component'; import { ComponentBuilder } from '../Component';
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
import isEqual from 'fast-deep-equal';
/** /**
* Represents a non-validated select menu component * Represents a non-validated select menu component
*/ */
export class UnsafeSelectMenuComponent extends Component< export class UnsafeSelectMenuBuilder extends ComponentBuilder<
Partial<Omit<APISelectMenuComponent, 'options'>> & { type: ComponentType.SelectMenu } Partial<Omit<APISelectMenuComponent, 'options'>> & { type: ComponentType.SelectMenu }
> { > {
/** /**
* The options within this select menu * The options within this select menu
*/ */
public readonly options: UnsafeSelectMenuOption[]; protected readonly options: UnsafeSelectMenuOptionBuilder[];
public constructor(data?: Partial<APISelectMenuComponent>) { public constructor(data?: Partial<APISelectMenuComponent>) {
const { options, ...initData } = data ?? {}; const { options, ...initData } = data ?? {};
super({ type: ComponentType.SelectMenu, ...initData }); super({ type: ComponentType.SelectMenu, ...initData });
this.options = options?.map((o) => new UnsafeSelectMenuOption(o)) ?? []; this.options = options?.map((o) => new UnsafeSelectMenuOptionBuilder(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;
} }
/** /**
@@ -105,10 +69,10 @@ export class UnsafeSelectMenuComponent extends Component<
* @param options The options to add to this select menu * @param options The options to add to this select menu
* @returns * @returns
*/ */
public addOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) { public addOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
this.options.push( this.options.push(
...options.map((option) => ...options.map((option) =>
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option), option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
), ),
); );
return this; return this;
@@ -118,12 +82,12 @@ export class UnsafeSelectMenuComponent extends Component<
* Sets the options on this select menu * Sets the options on this select menu
* @param options The options to set 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( this.options.splice(
0, 0,
this.options.length, this.options.length,
...options.map((option) => ...options.map((option) =>
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option), option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
), ),
); );
return this; return this;
@@ -136,14 +100,4 @@ export class UnsafeSelectMenuComponent extends Component<
options: this.options.map((o) => o.toJSON()), options: this.options.map((o) => o.toJSON()),
} as APISelectMenuComponent; } 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 * Represents a non-validated option within a select menu component
*/ */
export class UnsafeSelectMenuOption { export class UnsafeSelectMenuOptionBuilder {
public constructor(protected data: Partial<APISelectMenuOption> = {}) {} public constructor(public 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;
}
/** /**
* Sets the label of this option * Sets the label of this option

View File

@@ -7,9 +7,9 @@ import {
valueValidator, valueValidator,
validateRequiredParameters, validateRequiredParameters,
} from './Assertions'; } 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) { public override setMinLength(minLength: number) {
return super.setMinLength(minLengthValidator.parse(minLength)); return super.setMinLength(minLengthValidator.parse(minLength));
} }

View File

@@ -1,73 +1,17 @@
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v9'; 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'; import isEqual from 'fast-deep-equal';
export class UnsafeTextInputComponent extends Component< export class UnsafeTextInputBuilder extends ComponentBuilder<
Partial<APITextInputComponent> & { type: ComponentType.TextInput } Partial<APITextInputComponent> & { type: ComponentType.TextInput }
> { > {
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) { public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
super({ type: ComponentType.TextInput, ...data }); 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 * 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) { public setCustomId(customId: string) {
this.data.custom_id = customId; this.data.custom_id = customId;
@@ -144,8 +88,8 @@ export class UnsafeTextInputComponent extends Component<
} as APITextInputComponent; } as APITextInputComponent;
} }
public equals(other: UnsafeTextInputComponent | APITextInputComponent): boolean { public equals(other: UnsafeTextInputBuilder | APITextInputComponent): boolean {
if (other instanceof UnsafeTextInputComponent) { if (other instanceof UnsafeTextInputBuilder) {
return isEqual(other.data, this.data); 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 './interactions/contextMenuCommands/ContextMenuCommandBuilder';
export * from './util/jsonEncodable'; export * from './util/jsonEncodable';
export * from './util/equatable';
export * from './util/componentUtil';

View File

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

View File

@@ -1,9 +1,9 @@
import type { APIModalInteractionResponseCallbackData } from 'discord-api-types/v9'; import type { APIModalInteractionResponseCallbackData } from 'discord-api-types/v9';
import { customIdValidator } from '../../components/Assertions'; import { customIdValidator } from '../../components/Assertions';
import { titleValidator, validateRequiredParameters } from './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 { public override setCustomId(customId: string): this {
return super.setCustomId(customIdValidator.parse(customId)); return super.setCustomId(customIdValidator.parse(customId));
} }

View File

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

View File

@@ -10,15 +10,15 @@ import {
urlPredicate, urlPredicate,
validateFieldLength, validateFieldLength,
} from './Assertions'; } 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.) * 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 { public override addFields(...fields: APIEmbedField[]): this {
// Ensure adding these fields won't exceed the 25 field limit // Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(fields.length, this.fields); validateFieldLength(fields.length, this.data.fields);
// Data assertions // Data assertions
return super.addFields(...embedFieldsArrayPredicate.parse(fields)); 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 { public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
// Ensure adding these fields won't exceed the 25 field limit // 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 // Data assertions
return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields)); return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields));

View File

@@ -1,13 +1,4 @@
import type { import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v9';
APIEmbed,
APIEmbedAuthor,
APIEmbedField,
APIEmbedFooter,
APIEmbedImage,
APIEmbedVideo,
} from 'discord-api-types/v9';
import type { Equatable } from '../../util/equatable';
import isEqual from 'fast-deep-equal';
export type RGBTuple = [red: number, green: number, blue: number]; 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.) * 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 readonly data: APIEmbed;
public constructor(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(); 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) * Adds fields to the embed (max 25)
* *
@@ -204,7 +68,7 @@ export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
* @param fields The fields to set * @param fields The fields to set
*/ */
public setFields(...fields: APIEmbedField[]) { public setFields(...fields: APIEmbedField[]) {
this.spliceFields(0, this.fields?.length ?? 0, ...fields); this.spliceFields(0, this.data.fields?.length ?? 0, ...fields);
return this; return this;
} }
@@ -319,11 +183,4 @@ export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
public toJSON(): APIEmbed { public toJSON(): APIEmbed {
return { ...this.data }; 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", "@sapphire/snowflake": "^3.1.0",
"@types/ws": "^8.2.2", "@types/ws": "^8.2.2",
"discord-api-types": "^0.27.3", "discord-api-types": "^0.27.3",
"fast-deep-equal": "^3.1.3",
"lodash.snakecase": "^4.1.1", "lodash.snakecase": "^4.1.1",
"undici": "^4.14.1", "undici": "^4.14.1",
"ws": "^8.5.0" "ws": "^8.5.0"

View File

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

View File

@@ -1,14 +1,21 @@
'use strict'; 'use strict';
const { ActionRow: BuildersActionRow, Component } = require('@discordjs/builders'); const Component = require('./Component');
const Transformers = require('../util/Transformers'); const Components = require('../util/Components');
class ActionRow extends BuildersActionRow { /**
constructor({ components, ...data } = {}) { * Represents an action row
super({ * @extends {Component}
components: components?.map(c => (c instanceof Component ? c : Transformers.toSnakeCase(c))), */
...Transformers.toSnakeCase(data), 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'; 'use strict';
const { ButtonComponent: BuildersButtonComponent } = require('@discordjs/builders'); const Component = require('./Component');
const Transformers = require('../util/Transformers');
class ButtonComponent extends BuildersButtonComponent { /**
constructor(data) { * Represents a button component
super(Transformers.toSnakeCase(data)); * @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'; 'use strict';
const { Embed: BuildersEmbed } = require('@discordjs/builders'); const isEqual = require('fast-deep-equal');
const Transformers = require('../util/Transformers'); const { Util } = require('../util/Util');
const Util = require('../util/Util');
/** class Embed {
* Represents an embed object /**
*/ * Creates a new embed object
class Embed extends BuildersEmbed { * @param {APIEmbed} data API embed data
* @private
*/
constructor(data) { 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'; 'use strict';
const { createComponent, Embed } = require('@discordjs/builders');
const { Collection } = require('@discordjs/collection'); const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake'); const { DiscordSnowflake } = require('@sapphire/snowflake');
const { const {
@@ -12,6 +11,7 @@ const {
} = require('discord-api-types/v9'); } = require('discord-api-types/v9');
const Base = require('./Base'); const Base = require('./Base');
const ClientApplication = require('./ClientApplication'); const ClientApplication = require('./ClientApplication');
const Embed = require('./Embed');
const InteractionCollector = require('./InteractionCollector'); const InteractionCollector = require('./InteractionCollector');
const MessageAttachment = require('./MessageAttachment'); const MessageAttachment = require('./MessageAttachment');
const Mentions = require('./MessageMentions'); const Mentions = require('./MessageMentions');
@@ -20,6 +20,7 @@ const ReactionCollector = require('./ReactionCollector');
const { Sticker } = require('./Sticker'); const { Sticker } = require('./Sticker');
const { Error } = require('../errors'); const { Error } = require('../errors');
const ReactionManager = require('../managers/ReactionManager'); const ReactionManager = require('../managers/ReactionManager');
const Components = require('../util/Components');
const { NonSystemMessageTypes } = require('../util/Constants'); const { NonSystemMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField'); const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField'); const PermissionsBitField = require('../util/PermissionsBitField');
@@ -145,7 +146,7 @@ class Message extends Base {
* A list of MessageActionRows in the message * A list of MessageActionRows in the message
* @type {ActionRow[]} * @type {ActionRow[]}
*/ */
this.components = data.components.map(c => createComponent(c)); this.components = data.components.map(c => Components.createComponent(c));
} else { } else {
this.components = this.components?.slice() ?? []; 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'; 'use strict';
const { SelectMenuComponent: BuildersSelectMenuComponent } = require('@discordjs/builders'); const Component = require('./Component');
const Transformers = require('../util/Transformers');
class SelectMenuComponent extends BuildersSelectMenuComponent { /**
constructor(data) { * Represents a select menu component
super(Transformers.toSnakeCase(data)); * @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'; 'use strict';
const { TextInputComponent: BuildersTextInputComponent } = require('@discordjs/builders'); const Component = require('./Component');
const Transformers = require('../util/Transformers');
class TextInputComponent extends BuildersTextInputComponent { class TextInputComponent extends Component {
constructor(data) { /**
super(Transformers.toSnakeCase(data)); * 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'; 'use strict';
// This file contains the typedefs for camel-cased json data // This file contains the typedefs for camel-cased json data
const { ComponentType } = require('discord-api-types/v9');
/** /**
* @typedef {Object} BaseComponentData * @typedef {Object} BaseComponentData
* @property {ComponentType} type The type of component * @property {ComponentType} type The type of component
@@ -56,3 +56,34 @@
/** /**
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData} ComponentData * @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 { import {
ActionRow as BuilderActionRow, ActionRowBuilder as BuilderActionRow,
MessageActionRowComponent, MessageActionRowComponentBuilder,
blockQuote, blockQuote,
bold, bold,
ButtonComponent as BuilderButtonComponent, ButtonBuilder as BuilderButtonComponent,
channelMention, channelMention,
codeBlock, codeBlock,
Component, EmbedBuilder as BuildersEmbed,
Embed as BuildersEmbed,
formatEmoji, formatEmoji,
hideLinkEmbed, hideLinkEmbed,
hyperlink, hyperlink,
inlineCode, inlineCode,
italic, italic,
JSONEncodable,
MappedComponentTypes,
memberNicknameMention, memberNicknameMention,
Modal as BuilderModal,
quote, quote,
roleMention, roleMention,
SelectMenuComponent as BuilderSelectMenuComponent, SelectMenuBuilder as BuilderSelectMenuComponent,
TextInputComponent as BuilderTextInputComponent, TextInputBuilder as BuilderTextInputComponent,
spoiler, spoiler,
strikethrough, strikethrough,
time, time,
@@ -26,7 +26,7 @@ import {
TimestampStylesString, TimestampStylesString,
underscore, underscore,
userMention, userMention,
ModalActionRowComponent, ModalActionRowComponentBuilder,
} from '@discordjs/builders'; } from '@discordjs/builders';
import { Collection } from '@discordjs/collection'; import { Collection } from '@discordjs/collection';
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
@@ -105,6 +105,11 @@ import {
APITextInputComponent, APITextInputComponent,
APIModalActionRowComponent, APIModalActionRowComponent,
APIModalComponent, APIModalComponent,
APISelectMenuOption,
APIEmbedField,
APIEmbedAuthor,
APIEmbedFooter,
APIEmbedImage,
} from 'discord-api-types/v9'; } from 'discord-api-types/v9';
import { ChildProcess } from 'node:child_process'; import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
@@ -220,8 +225,10 @@ export interface ActionRowData<T extends ActionRowComponent | ActionRowComponent
components: T[]; components: T[];
} }
export class ActionRow< export class ActionRowBuilder<
T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent, T extends MessageActionRowComponentBuilder | ModalActionRowComponentBuilder =
| MessageActionRowComponentBuilder
| ModalActionRowComponentBuilder,
> extends BuilderActionRow<T> { > extends BuilderActionRow<T> {
constructor( constructor(
data?: 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> { export class ActivityFlagsBitField extends BitField<ActivityFlagsString> {
public static Flags: typeof ActivityFlags; public static Flags: typeof ActivityFlags;
public static resolve(bit?: BitFieldResolvable<ActivityFlagsString, number>): number; public static resolve(bit?: BitFieldResolvable<ActivityFlagsString, number>): number;
@@ -356,7 +371,9 @@ export interface InteractionResponseFields<Cached extends CacheType = CacheType>
deferReply(options?: InteractionDeferReplyOptions): Promise<void>; deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
fetchReply(): Promise<GuildCacheMessage<Cached>>; fetchReply(): Promise<GuildCacheMessage<Cached>>;
followUp(options: string | MessagePayload | InteractionReplyOptions): 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> { 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 followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>; 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( private transformOption(
option: APIApplicationCommandOption, option: APIApplicationCommandOption,
resolved: APIApplicationCommandInteractionData['resolved'], resolved: APIApplicationCommandInteractionData['resolved'],
@@ -502,22 +521,53 @@ export class ButtonInteraction<Cached extends CacheType = CacheType> extends Mes
public inRawGuild(): this is ButtonInteraction<'raw'>; public inRawGuild(): this is ButtonInteraction<'raw'>;
} }
export class ButtonComponent extends BuilderButtonComponent { export class Component<T extends APIMessageComponent | APIModalComponent = APIMessageComponent | APIModalComponent> {
public constructor(data?: ButtonComponentData | (Omit<APIButtonComponent, 'type'> & { type?: ComponentType.Button })); 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( public constructor(
data?: SelectMenuComponentData | (Omit<APISelectMenuComponent, 'type'> & { type?: ComponentType.SelectMenu }), 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 constructor(data?: TextInputComponentData | APITextInputComponent);
public static from(other: JSONEncodable<APITextInputComponent> | APITextInputComponent): TextInputBuilder;
} }
export class Modal extends BuilderModal { export class TextInputComponent extends Component<APITextInputComponent> {
public constructor(data?: ModalData | APIModalActionRowComponent); 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 { export interface EmbedData {
@@ -535,18 +585,43 @@ export interface EmbedData {
fields?: EmbedFieldData[]; fields?: EmbedFieldData[];
} }
export interface EmbedImageData { export interface IconData {
url?: string; 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 { export interface EmbedProviderData {
name?: string; name?: string;
url?: 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 constructor(data?: EmbedData | APIEmbed);
public override setColor(color: ColorResolvable | null): this; 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 { export interface MappedChannelCategoryTypes {
@@ -1652,7 +1727,9 @@ export class MessageComponentInteraction<Cached extends CacheType = CacheType> e
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>; public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public update(options: string | MessagePayload | InteractionUpdateOptions): Promise<void>; 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< export class MessageContextMenuCommandInteraction<
@@ -1741,17 +1818,11 @@ export class MessageReaction {
public toJSON(): unknown; public toJSON(): unknown;
} }
export interface ModalFieldData {
value: string;
type: ComponentType;
customId: string;
}
export class ModalSubmitFieldsResolver { export class ModalSubmitFieldsResolver {
constructor(components: ModalFieldData[][]); constructor(components: ModalActionRowComponent[][]);
public components: ModalFieldData[][]; public components: ActionRow<ModalActionRowComponent>;
public fields: Collection<string, ModalFieldData>; public fields: Collection<string, ModalActionRowComponent>;
public getField(customId: string): ModalFieldData; public getField(customId: string): ModalActionRowComponent;
public getTextInputValue(customId: string): string; public getTextInputValue(customId: string): string;
} }
@@ -1769,7 +1840,7 @@ export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = C
export interface ModalSubmitActionRow { export interface ModalSubmitActionRow {
type: ComponentType.ActionRow; type: ComponentType.ActionRow;
components: ModalFieldData[]; components: ActionRow<TextInputComponent>[];
} }
export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extends Interaction<Cached> { 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[]; 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 { export class Formatters extends null {
public static blockQuote: typeof blockQuote; public static blockQuote: typeof blockQuote;
public static bold: typeof bold; public static bold: typeof bold;
@@ -3966,12 +4045,6 @@ export interface EditGuildTemplateOptions {
description?: string; description?: string;
} }
export interface EmbedAuthorData {
name: string;
url?: string;
iconURL?: string;
}
export interface EmbedField { export interface EmbedField {
name: string; name: string;
value: string; value: string;
@@ -3984,11 +4057,6 @@ export interface EmbedFieldData {
inline?: boolean; inline?: boolean;
} }
export interface EmbedFooterData {
text: string;
iconURL?: string;
}
export type EmojiIdentifierResolvable = string | EmojiResolvable; export type EmojiIdentifierResolvable = string | EmojiResolvable;
export type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; export type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji;
@@ -4646,7 +4714,11 @@ export interface MessageCollectorOptions extends CollectorOptions<[Message]> {
maxProcessed?: number; 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< export type MessageComponentCollectorOptions<T extends MessageComponentInteraction> = Omit<
InteractionCollectorOptions<T>, InteractionCollectorOptions<T>,
@@ -4666,6 +4738,7 @@ export interface MessageEditOptions {
flags?: BitFieldResolvable<MessageFlagsString, number>; flags?: BitFieldResolvable<MessageFlagsString, number>;
allowedMentions?: MessageMentionOptions; allowedMentions?: MessageMentionOptions;
components?: ( components?: (
| JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>
| ActionRow<MessageActionRowComponent> | ActionRow<MessageActionRowComponent>
| (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData | MessageActionRowComponent>) | (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData | MessageActionRowComponent>)
| APIActionRowComponent<APIMessageActionRowComponent> | APIActionRowComponent<APIMessageActionRowComponent>
@@ -4705,8 +4778,9 @@ export interface MessageOptions {
tts?: boolean; tts?: boolean;
nonce?: string | number; nonce?: string | number;
content?: string | null; content?: string | null;
embeds?: (Embed | APIEmbed)[]; embeds?: (JSONEncodable<APIEmbed> | APIEmbed)[];
components?: ( components?: (
| JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>
| ActionRow<MessageActionRowComponent> | ActionRow<MessageActionRowComponent>
| (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData | MessageActionRowComponent>) | (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData | MessageActionRowComponent>)
| APIActionRowComponent<APIMessageActionRowComponent> | APIActionRowComponent<APIMessageActionRowComponent>
@@ -5255,6 +5329,8 @@ export {
ApplicationCommandType, ApplicationCommandType,
ApplicationCommandOptionType, ApplicationCommandOptionType,
ApplicationCommandPermissionType, ApplicationCommandPermissionType,
APIEmbedField,
APISelectMenuOption,
AuditLogEvent, AuditLogEvent,
ButtonStyle, ButtonStyle,
ChannelType, ChannelType,
@@ -5290,12 +5366,13 @@ export {
WebhookType, WebhookType,
} from 'discord-api-types/v9'; } from 'discord-api-types/v9';
export { export {
UnsafeButtonComponent, UnsafeButtonBuilder,
UnsafeSelectMenuComponent, UnsafeSelectMenuBuilder,
SelectMenuOption, SelectMenuOptionBuilder,
UnsafeSelectMenuOption, UnsafeSelectMenuOptionBuilder,
MessageActionRowComponent, MessageActionRowComponentBuilder,
UnsafeEmbed, ModalActionRowComponentBuilder,
ModalActionRowComponent, UnsafeEmbedBuilder,
ModalBuilder,
} from '@discordjs/builders'; } from '@discordjs/builders';
export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest'; export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';

View File

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