diff --git a/packages/builders/__tests__/components/actionRow.test.ts b/packages/builders/__tests__/components/actionRow.test.ts index 4904104e4179..f44841b05a9e 100644 --- a/packages/builders/__tests__/components/actionRow.test.ts +++ b/packages/builders/__tests__/components/actionRow.test.ts @@ -1,13 +1,13 @@ +import { APIActionRowComponent, APIMessageActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9'; import { - APIActionRowComponent, - APIActionRowComponentTypes, - APIMessageActionRowComponent, - ButtonStyle, - ComponentType, -} from 'discord-api-types/v9'; -import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src'; + ActionRowBuilder, + ButtonBuilder, + createComponentBuilder, + SelectMenuBuilder, + SelectMenuOptionBuilder, +} from '../../src'; -const rowWithButtonData: APIActionRowComponent = { +const rowWithButtonData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { @@ -19,7 +19,7 @@ const rowWithButtonData: APIActionRowComponent = { ], }; -const rowWithSelectMenuData: APIActionRowComponent = { +const rowWithSelectMenuData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { @@ -44,8 +44,8 @@ const rowWithSelectMenuData: APIActionRowComponent = { describe('Action Row Components', () => { describe('Assertion Tests', () => { test('GIVEN valid components THEN do not throw', () => { - expect(() => new ActionRow().addComponents(new ButtonComponent())).not.toThrowError(); - expect(() => new ActionRow().setComponents(new ButtonComponent())).not.toThrowError(); + expect(() => new ActionRowBuilder().addComponents(new ButtonBuilder())).not.toThrowError(); + expect(() => new ActionRowBuilder().setComponents(new ButtonBuilder())).not.toThrowError(); }); test('GIVEN valid JSON input THEN valid JSON output is given', () => { @@ -78,13 +78,12 @@ describe('Action Row Components', () => { ], }; - expect(new ActionRow(actionRowData).toJSON()).toEqual(actionRowData); - expect(new ActionRow().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] }); - expect(() => createComponent({ type: ComponentType.ActionRow, components: [] })).not.toThrowError(); - expect(() => createComponent({ type: 42, components: [] })).toThrowError(); + expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData); + expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] }); + expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError(); }); test('GIVEN valid builder options THEN valid JSON output is given', () => { - const rowWithButtonData: APIActionRowComponent = { + const rowWithButtonData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { @@ -96,7 +95,7 @@ describe('Action Row Components', () => { ], }; - const rowWithSelectMenuData: APIActionRowComponent = { + const rowWithSelectMenuData: APIActionRowComponent = { type: ComponentType.ActionRow, components: [ { @@ -118,22 +117,24 @@ describe('Action Row Components', () => { ], }; - const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); - const selectMenu = new SelectMenuComponent() + expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData); + expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData); + expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] }); + expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError(); + }); + test('GIVEN valid builder options THEN valid JSON output is given', () => { + const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); + const selectMenu = new SelectMenuBuilder() .setCustomId('1234') .setMaxValues(10) .setMinValues(12) .setOptions( - new SelectMenuOption().setLabel('one').setValue('one'), - new SelectMenuOption().setLabel('two').setValue('two'), + new SelectMenuOptionBuilder().setLabel('one').setValue('one'), + new SelectMenuOptionBuilder().setLabel('two').setValue('two'), ); - expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData); - expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData); - }); - test('Given JSON data THEN builder is equal to it and itself', () => { - expect(new ActionRow(rowWithSelectMenuData).equals(rowWithSelectMenuData)).toBeTruthy(); - expect(new ActionRow(rowWithButtonData).equals(new ActionRow(rowWithButtonData))).toBeTruthy(); + expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData); + expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData); }); }); }); diff --git a/packages/builders/__tests__/components/button.test.ts b/packages/builders/__tests__/components/button.test.ts index 0193f1519e68..897a7f8021f1 100644 --- a/packages/builders/__tests__/components/button.test.ts +++ b/packages/builders/__tests__/components/button.test.ts @@ -5,9 +5,9 @@ import { ComponentType, } from 'discord-api-types/v9'; import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions'; -import { ButtonComponent } from '../../src/components/button/Button'; +import { ButtonBuilder } from '../../src/components/button/Button'; -const buttonComponent = () => new ButtonComponent(); +const buttonComponent = () => new ButtonBuilder(); const longStr = 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong'; @@ -119,7 +119,7 @@ describe('Button Components', () => { disabled: true, }; - expect(new ButtonComponent(interactionData).toJSON()).toEqual(interactionData); + expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData); expect( buttonComponent() @@ -138,21 +138,9 @@ describe('Button Components', () => { url: 'https://google.com', }; - expect(new ButtonComponent(linkData).toJSON()).toEqual(linkData); + expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData); expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url)); }); - test('Given JSON data THEN builder is equal to it and itself', () => { - const buttonData: APIButtonComponentWithCustomId = { - type: ComponentType.Button, - custom_id: 'test', - label: 'test', - style: ButtonStyle.Primary, - disabled: true, - }; - - expect(new ButtonComponent(buttonData).equals(buttonData)).toBeTruthy(); - expect(new ButtonComponent(buttonData).equals(new ButtonComponent(buttonData))).toBeTruthy(); - }); }); }); diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts index 8ade90075c79..cbaa3af79853 100644 --- a/packages/builders/__tests__/components/selectMenu.test.ts +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -1,8 +1,8 @@ import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v9'; -import { SelectMenuComponent, SelectMenuOption } from '../../src/index'; +import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index'; -const selectMenu = () => new SelectMenuComponent(); -const selectMenuOption = () => new SelectMenuOption(); +const selectMenu = () => new SelectMenuBuilder(); +const selectMenuOption = () => new SelectMenuOptionBuilder(); const longStr = 'a'.repeat(256); @@ -44,8 +44,8 @@ describe('Select Menu Components', () => { .setEmoji({ name: 'test' }) .setDescription('description'); expect(() => selectMenu().addOptions(option)).not.toThrowError(); - expect(() => selectMenu().setOptions([option])).not.toThrowError(); - expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError(); + expect(() => selectMenu().setOptions(option)).not.toThrowError(); + expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError(); }); test('GIVEN invalid inputs THEN Select Menu does throw', () => { @@ -70,16 +70,11 @@ describe('Select Menu Components', () => { test('GIVEN valid JSON input THEN valid JSON history is correct', () => { expect( - new SelectMenuComponent(selectMenuDataWithoutOptions) - .addOptions(new SelectMenuOption(selectMenuOptionData)) + new SelectMenuBuilder(selectMenuDataWithoutOptions) + .addOptions(new SelectMenuOptionBuilder(selectMenuOptionData)) .toJSON(), ).toEqual(selectMenuData); - expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); - }); - - test('Given JSON data THEN builder is equal to it and itself', () => { - expect(new SelectMenuComponent(selectMenuData).equals(selectMenuData)).toBeTruthy(); - expect(new SelectMenuComponent(selectMenuData).equals(new SelectMenuComponent(selectMenuData))).toBeTruthy(); + expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); }); }); }); diff --git a/packages/builders/__tests__/components/textInput.test.ts b/packages/builders/__tests__/components/textInput.test.ts index d88dc5285fed..0a04e722c567 100644 --- a/packages/builders/__tests__/components/textInput.test.ts +++ b/packages/builders/__tests__/components/textInput.test.ts @@ -7,11 +7,11 @@ import { valueValidator, textInputStyleValidator, } from '../../src/components/textInput/Assertions'; -import { TextInputComponent } from '../../src/components/textInput/TextInput'; +import { TextInputBuilder } from '../../src/components/textInput/TextInput'; const superLongStr = 'a'.repeat(5000); -const textInputComponent = () => new TextInputComponent(); +const textInputComponent = () => new TextInputBuilder(); describe('Text Input Components', () => { describe('Assertion Tests', () => { @@ -109,7 +109,7 @@ describe('Text Input Components', () => { style: TextInputStyle.Paragraph, }; - expect(new TextInputComponent(textInputData).toJSON()).toEqual(textInputData); + expect(new TextInputBuilder(textInputData).toJSON()).toEqual(textInputData); expect( textInputComponent() .setCustomId(textInputData.custom_id) diff --git a/packages/builders/__tests__/interactions/modal.test.ts b/packages/builders/__tests__/interactions/modal.test.ts index 3bb427670dd9..7638d43ca045 100644 --- a/packages/builders/__tests__/interactions/modal.test.ts +++ b/packages/builders/__tests__/interactions/modal.test.ts @@ -1,12 +1,18 @@ import { APIModalInteractionResponseCallbackData, ComponentType, TextInputStyle } from 'discord-api-types/v9'; -import { ActionRow, ButtonComponent, Modal, ModalActionRowComponent, TextInputComponent } from '../../src'; +import { + ActionRowBuilder, + ButtonBuilder, + ModalBuilder, + ModalActionRowComponentBuilder, + TextInputBuilder, +} from '../../src'; import { componentsValidator, titleValidator, validateRequiredParameters, } from '../../src/interactions/modals/Assertions'; -const modal = () => new Modal(); +const modal = () => new ModalBuilder(); describe('Modals', () => { describe('Assertion Tests', () => { @@ -19,33 +25,37 @@ describe('Modals', () => { }); test('GIVEN valid components THEN validator does not throw', () => { - expect(() => componentsValidator.parse([new ActionRow(), new ActionRow()])).not.toThrowError(); + expect(() => componentsValidator.parse([new ActionRowBuilder(), new ActionRowBuilder()])).not.toThrowError(); }); test('GIVEN invalid components THEN validator does throw', () => { - expect(() => componentsValidator.parse([new ButtonComponent(), new TextInputComponent()])).toThrowError(); + expect(() => componentsValidator.parse([new ButtonBuilder(), new TextInputBuilder()])).toThrowError(); }); test('GIVEN valid required parameters THEN validator does not throw', () => { - expect(() => validateRequiredParameters('123', 'title', [new ActionRow(), new ActionRow()])).not.toThrowError(); + expect(() => + validateRequiredParameters('123', 'title', [new ActionRowBuilder(), new ActionRowBuilder()]), + ).not.toThrowError(); }); test('GIVEN invalid required parameters THEN validator does throw', () => { expect(() => // @ts-expect-error - validateRequiredParameters('123', undefined, [new ActionRow(), new ButtonComponent()]), + validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]), ).toThrowError(); }); }); test('GIVEN valid fields THEN builder does not throw', () => { - expect(() => modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRow())).not.toThrowError(); + expect(() => + modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRowBuilder()), + ).not.toThrowError(); }); test('GIVEN invalid fields THEN builder does throw', () => { expect(() => // @ts-expect-error - modal().setTitle('test').setCustomId('foobar').setComponents([new ActionRow()]).toJSON(), + modal().setTitle('test').setCustomId('foobar').setComponents([new ActionRowBuilder()]).toJSON(), ).toThrowError(); expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError(); // @ts-expect-error @@ -71,15 +81,15 @@ describe('Modals', () => { ], }; - expect(new Modal(modalData).toJSON()).toEqual(modalData); + expect(new ModalBuilder(modalData).toJSON()).toEqual(modalData); expect( modal() .setTitle(modalData.title) .setCustomId('custom id') .setComponents( - new ActionRow().addComponents( - new TextInputComponent().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph), + new ActionRowBuilder().addComponents( + new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph), ), ) .toJSON(), diff --git a/packages/builders/__tests__/messages/embed.test.ts b/packages/builders/__tests__/messages/embed.test.ts index 8dc2c63c010f..332b9fe0732e 100644 --- a/packages/builders/__tests__/messages/embed.test.ts +++ b/packages/builders/__tests__/messages/embed.test.ts @@ -1,11 +1,11 @@ -import { Embed } from '../../src'; +import { EmbedBuilder, embedLength } from '../../src'; const alpha = 'abcdefghijklmnopqrstuvwxyz'; describe('Embed', () => { describe('Embed getters', () => { test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => { - const embed = new Embed({ + const embed = new EmbedBuilder({ title: alpha, description: alpha, fields: [{ name: alpha, value: alpha }], @@ -13,38 +13,38 @@ describe('Embed', () => { footer: { text: alpha }, }); - expect(embed.length).toBe(alpha.length * 6); + expect(embedLength(embed.data)).toBe(alpha.length * 6); }); test('GIVEN an embed with zero characters THEN returns amount of characters', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); - expect(embed.length).toBe(0); + expect(embedLength(embed.data)).toBe(0); }); }); describe('Embed title', () => { test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => { - const embed = new Embed({ title: 'foo' }); + const embed = new EmbedBuilder({ title: 'foo' }); expect(embed.toJSON()).toStrictEqual({ title: 'foo' }); }); test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setTitle('foo'); expect(embed.toJSON()).toStrictEqual({ title: 'foo' }); }); test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => { - const embed = new Embed({ title: 'foo' }); + const embed = new EmbedBuilder({ title: 'foo' }); embed.setTitle(null); expect(embed.toJSON()).toStrictEqual({ title: undefined }); }); test('GIVEN an embed with an invalid title THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setTitle('a'.repeat(257))).toThrowError(); }); @@ -52,26 +52,26 @@ describe('Embed', () => { describe('Embed description', () => { test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => { - const embed = new Embed({ description: 'foo' }); + const embed = new EmbedBuilder({ description: 'foo' }); expect(embed.toJSON()).toStrictEqual({ description: 'foo' }); }); test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setDescription('foo'); expect(embed.toJSON()).toStrictEqual({ description: 'foo' }); }); test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => { - const embed = new Embed({ description: 'foo' }); + const embed = new EmbedBuilder({ description: 'foo' }); embed.setDescription(null); expect(embed.toJSON()).toStrictEqual({ description: undefined }); }); test('GIVEN an embed with an invalid description THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setDescription('a'.repeat(4097))).toThrowError(); }); @@ -79,14 +79,14 @@ describe('Embed', () => { describe('Embed URL', () => { test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => { - const embed = new Embed({ url: 'https://discord.js.org/' }); + const embed = new EmbedBuilder({ url: 'https://discord.js.org/' }); expect(embed.toJSON()).toStrictEqual({ url: 'https://discord.js.org/', }); }); test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setURL('https://discord.js.org/'); expect(embed.toJSON()).toStrictEqual({ @@ -95,14 +95,14 @@ describe('Embed', () => { }); test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => { - const embed = new Embed({ url: 'https://discord.js.org' }); + const embed = new EmbedBuilder({ url: 'https://discord.js.org' }); embed.setURL(null); expect(embed.toJSON()).toStrictEqual({ url: undefined }); }); test('GIVEN an embed with an invalid URL THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setURL('owo')).toThrowError(); }); @@ -110,24 +110,24 @@ describe('Embed', () => { describe('Embed Color', () => { test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => { - const embed = new Embed({ color: 0xff0000 }); + const embed = new EmbedBuilder({ color: 0xff0000 }); expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 }); }); test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => { - expect(new Embed().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 }); - expect(new Embed().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 }); + expect(new EmbedBuilder().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 }); + expect(new EmbedBuilder().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 }); }); test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => { - const embed = new Embed({ color: 0xff0000 }); + const embed = new EmbedBuilder({ color: 0xff0000 }); embed.setColor(null); expect(embed.toJSON()).toStrictEqual({ color: undefined }); }); test('GIVEN an embed with an invalid color THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); // @ts-expect-error expect(() => embed.setColor('RED')).toThrowError(); @@ -141,33 +141,33 @@ describe('Embed', () => { const now = new Date(); test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => { - const embed = new Embed({ timestamp: now.toISOString() }); + const embed = new EmbedBuilder({ timestamp: now.toISOString() }); expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); }); test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setTimestamp(now); expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); }); test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setTimestamp(now.getTime()); expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); }); test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setTimestamp(); - expect(embed.toJSON()).toStrictEqual({ timestamp: embed.timestamp }); + expect(embed.toJSON()).toStrictEqual({ timestamp: embed.data.timestamp }); }); test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => { - const embed = new Embed({ timestamp: now.toISOString() }); + const embed = new EmbedBuilder({ timestamp: now.toISOString() }); embed.setTimestamp(null); expect(embed.toJSON()).toStrictEqual({ timestamp: undefined }); @@ -176,14 +176,14 @@ describe('Embed', () => { describe('Embed Thumbnail', () => { test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => { - const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); + const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); expect(embed.toJSON()).toStrictEqual({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, }); }); test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setThumbnail('https://discord.js.org/static/logo.svg'); expect(embed.toJSON()).toStrictEqual({ @@ -192,14 +192,14 @@ describe('Embed', () => { }); test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => { - const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); + const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); embed.setThumbnail(null); expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined }); }); test('GIVEN an embed with an invalid thumbnail THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setThumbnail('owo')).toThrowError(); }); @@ -207,14 +207,14 @@ describe('Embed', () => { describe('Embed Image', () => { test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => { - const embed = new Embed({ image: { url: 'https://discord.js.org/static/logo.svg' } }); + const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } }); expect(embed.toJSON()).toStrictEqual({ image: { url: 'https://discord.js.org/static/logo.svg' }, }); }); test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setImage('https://discord.js.org/static/logo.svg'); expect(embed.toJSON()).toStrictEqual({ @@ -223,14 +223,14 @@ describe('Embed', () => { }); test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => { - const embed = new Embed({ image: { url: 'https://discord.js/org/static/logo.svg' } }); + const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } }); embed.setImage(null); expect(embed.toJSON()).toStrictEqual({ image: undefined }); }); test('GIVEN an embed with an invalid image THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setImage('owo')).toThrowError(); }); @@ -238,7 +238,7 @@ describe('Embed', () => { describe('Embed Author', () => { test('GIVEN an embed with a pre-defined author THEN returns valid toJSON data', () => { - const embed = new Embed({ + const embed = new EmbedBuilder({ author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, }); expect(embed.toJSON()).toStrictEqual({ @@ -247,7 +247,7 @@ describe('Embed', () => { }); test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setAuthor({ name: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg', @@ -260,7 +260,7 @@ describe('Embed', () => { }); test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => { - const embed = new Embed({ + const embed = new EmbedBuilder({ author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, }); embed.setAuthor(null); @@ -269,7 +269,7 @@ describe('Embed', () => { }); test('GIVEN an embed with an invalid author name THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError(); }); @@ -277,7 +277,7 @@ describe('Embed', () => { describe('Embed Footer', () => { test('GIVEN an embed with a pre-defined footer THEN returns valid toJSON data', () => { - const embed = new Embed({ + const embed = new EmbedBuilder({ footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, }); expect(embed.toJSON()).toStrictEqual({ @@ -286,7 +286,7 @@ describe('Embed', () => { }); test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' }); expect(embed.toJSON()).toStrictEqual({ @@ -295,14 +295,16 @@ describe('Embed', () => { }); test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => { - const embed = new Embed({ footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' } }); + const embed = new EmbedBuilder({ + footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, + }); embed.setFooter(null); expect(embed.toJSON()).toStrictEqual({ footer: undefined }); }); test('GIVEN an embed with invalid footer text THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setFooter({ text: 'a'.repeat(2049) })).toThrowError(); }); @@ -310,7 +312,7 @@ describe('Embed', () => { describe('Embed Fields', () => { test('GIVEN an embed with a pre-defined field THEN returns valid toJSON data', () => { - const embed = new Embed({ + const embed = new EmbedBuilder({ fields: [{ name: 'foo', value: 'bar' }], }); expect(embed.toJSON()).toStrictEqual({ @@ -319,7 +321,7 @@ describe('Embed', () => { }); test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.addFields({ name: 'foo', value: 'bar' }); expect(embed.toJSON()).toStrictEqual({ @@ -328,7 +330,7 @@ describe('Embed', () => { }); test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' }); expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({ @@ -337,7 +339,7 @@ describe('Embed', () => { }); test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' }))); expect(() => @@ -346,7 +348,7 @@ describe('Embed', () => { }); test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' }))); expect(() => @@ -355,7 +357,7 @@ describe('Embed', () => { }); test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))), @@ -363,7 +365,7 @@ describe('Embed', () => { }); test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))), @@ -372,7 +374,7 @@ describe('Embed', () => { describe('GIVEN invalid field amount THEN throws error', () => { test('', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))), @@ -382,7 +384,7 @@ describe('Embed', () => { describe('GIVEN invalid field name THEN throws error', () => { test('', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError(); }); @@ -390,7 +392,7 @@ describe('Embed', () => { describe('GIVEN invalid field name length THEN throws error', () => { test('', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError(); }); @@ -398,7 +400,7 @@ describe('Embed', () => { describe('GIVEN invalid field value length THEN throws error', () => { test('', () => { - const embed = new Embed(); + const embed = new EmbedBuilder(); expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError(); }); diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 77c359bff3a5..0b7108367537 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -1,26 +1,29 @@ import { - APIActionRowComponent, + type APIActionRowComponent, + ComponentType, APIMessageActionRowComponent, APIModalActionRowComponent, - ComponentType, } from 'discord-api-types/v9'; -import type { ButtonComponent, SelectMenuComponent, TextInputComponent } from '../index'; -import { Component } from './Component'; -import { createComponent } from './Components'; -import isEqual from 'fast-deep-equal'; +import type { ButtonBuilder, SelectMenuBuilder, TextInputBuilder } from '..'; +import { ComponentBuilder } from './Component'; +import { createComponentBuilder } from './Components'; -export type MessageComponent = MessageActionRowComponent | ActionRow; -export type ModalComponent = ModalActionRowComponent | ActionRow; +export type MessageComponentBuilder = + | MessageActionRowComponentBuilder + | ActionRowBuilder; +export type ModalComponentBuilder = ModalActionRowComponentBuilder | ActionRowBuilder; -export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent; -export type ModalActionRowComponent = TextInputComponent; +export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder; +export type ModalActionRowComponentBuilder = TextInputBuilder; /** * Represents an action row component */ -export class ActionRow< - T extends ModalActionRowComponent | MessageActionRowComponent = ModalActionRowComponent | MessageActionRowComponent, -> extends Component< +export class ActionRowBuilder< + T extends MessageActionRowComponentBuilder | ModalActionRowComponentBuilder = + | MessageActionRowComponentBuilder + | ModalActionRowComponentBuilder, +> extends ComponentBuilder< Omit< Partial> & { type: ComponentType.ActionRow; @@ -31,14 +34,14 @@ export class ActionRow< /** * The components within this action row */ - public readonly components: T[]; + private readonly components: T[]; public constructor({ components, ...data }: Partial> = {}) { 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[], }; } - - public equals(other: APIActionRowComponent | 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()), - }); - } } diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 1d180085caba..f70017571c17 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -1,6 +1,6 @@ import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9'; import { z } from 'zod'; -import type { SelectMenuOption } from './selectMenu/SelectMenuOption'; +import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption'; export const customIdValidator = z.string().min(1).max(100); @@ -24,7 +24,7 @@ export const minMaxValidator = z.number().int().min(0).max(25); export const optionsValidator = z.object({}).array().nonempty(); -export function validateRequiredSelectMenuParameters(options: SelectMenuOption[], customId?: string) { +export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) { customIdValidator.parse(customId); optionsValidator.parse(options); } diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index a7ff010aed45..318deb5030f5 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -4,17 +4,16 @@ import type { APIActionRowComponentTypes, APIBaseComponent, APIMessageActionRowComponent, - APIModalActionRowComponent, APIMessageComponent, - ComponentType, + APIModalActionRowComponent, APIModalComponent, + ComponentType, } from 'discord-api-types/v9'; -import type { Equatable } from '../util/equatable'; /** * Represents a discord component */ -export abstract class Component< +export abstract class ComponentBuilder< DataType extends Partial> & { type: ComponentType; } = APIBaseComponent, @@ -23,11 +22,6 @@ export abstract class Component< | APIModalComponent | APIMessageComponent | APIActionRowComponent - >, - Equatable< - | Component - | APIActionRowComponentTypes - | APIActionRowComponent > { /** @@ -39,21 +33,7 @@ export abstract class Component< | APIActionRowComponentTypes | APIActionRowComponent; - public abstract equals( - other: - | Component - | APIActionRowComponentTypes - | APIActionRowComponent, - ): boolean; - public constructor(data: DataType) { this.data = data; } - - /** - * The type of this component - */ - public get type(): DataType['type'] { - return this.data.type; - } } diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 57997731b305..266d512c6012 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -1,37 +1,41 @@ -import { APIBaseComponent, APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v9'; -import { ActionRow, ButtonComponent, Component, SelectMenuComponent, TextInputComponent } from '../index'; -import type { MessageComponent, ModalActionRowComponent } from './ActionRow'; +import { APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v9'; +import { ActionRowBuilder, ButtonBuilder, ComponentBuilder, SelectMenuBuilder, TextInputBuilder } from '../index'; +import type { MessageComponentBuilder, ModalComponentBuilder } from './ActionRow'; export interface MappedComponentTypes { - [ComponentType.ActionRow]: ActionRow; - [ComponentType.Button]: ButtonComponent; - [ComponentType.SelectMenu]: SelectMenuComponent; - [ComponentType.TextInput]: TextInputComponent; + [ComponentType.ActionRow]: ActionRowBuilder; + [ComponentType.Button]: ButtonBuilder; + [ComponentType.SelectMenu]: SelectMenuBuilder; + [ComponentType.TextInput]: TextInputBuilder; } /** * Factory for creating components from API data * @param data The api data to transform to a component class */ -export function createComponent( +export function createComponentBuilder( data: (APIMessageComponent | APIModalComponent) & { type: T }, ): MappedComponentTypes[T]; -export function createComponent(data: C): C; -export function createComponent(data: APIModalComponent | APIMessageComponent | Component): Component { - if (data instanceof Component) { +export function createComponentBuilder(data: C): C; +export function createComponentBuilder( + data: APIMessageComponent | APIModalComponent | MessageComponentBuilder, +): ComponentBuilder { + if (data instanceof ComponentBuilder) { return data; } switch (data.type) { case ComponentType.ActionRow: - return new ActionRow(data); + return new ActionRowBuilder(data); case ComponentType.Button: - return new ButtonComponent(data); + return new ButtonBuilder(data); case ComponentType.SelectMenu: - return new SelectMenuComponent(data); + return new SelectMenuBuilder(data); case ComponentType.TextInput: - return new TextInputComponent(data); + return new TextInputBuilder(data); default: - throw new Error(`Cannot serialize component type: ${(data as APIBaseComponent).type}`); + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Cannot properly serialize component type: ${data.type}`); } } diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index 4cd7e832716c..de7a4808d502 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -1,4 +1,10 @@ -import type { ButtonStyle, APIMessageComponentEmoji, APIButtonComponent } from 'discord-api-types/v9'; +import type { + ButtonStyle, + APIMessageComponentEmoji, + APIButtonComponent, + APIButtonComponentWithCustomId, + APIButtonComponentWithURL, +} from 'discord-api-types/v9'; import { buttonLabelValidator, buttonStyleValidator, @@ -8,12 +14,12 @@ import { urlValidator, validateRequiredButtonParameters, } from '../Assertions'; -import { UnsafeButtonComponent } from './UnsafeButton'; +import { UnsafeButtonBuilder } from './UnsafeButton'; /** * Represents a validated button component */ -export class ButtonComponent extends UnsafeButtonComponent { +export class ButtonBuilder extends UnsafeButtonBuilder { public override setStyle(style: ButtonStyle) { return super.setStyle(buttonStyleValidator.parse(style)); } @@ -39,7 +45,13 @@ export class ButtonComponent extends UnsafeButtonComponent { } public override toJSON(): APIButtonComponent { - validateRequiredButtonParameters(this.style, this.label, this.emoji, this.customId, this.url); + validateRequiredButtonParameters( + this.data.style, + this.data.label, + this.data.emoji, + (this.data as APIButtonComponentWithCustomId).custom_id, + (this.data as APIButtonComponentWithURL).url, + ); return super.toJSON(); } } diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index c174afbbf81c..9c5e5ec71cf0 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -6,59 +6,18 @@ import { type APIButtonComponentWithURL, type APIButtonComponentWithCustomId, } from 'discord-api-types/v9'; -import { Component } from '../Component'; -import isEqual from 'fast-deep-equal'; +import { ComponentBuilder } from '../Component'; /** * Represents a non-validated button component */ -export class UnsafeButtonComponent extends Component & { type: ComponentType.Button }> { +export class UnsafeButtonBuilder extends ComponentBuilder< + Partial & { type: ComponentType.Button } +> { public constructor(data?: Partial) { super({ type: ComponentType.Button, ...data }); } - /** - * The style of this button - */ - public get style() { - return this.data.style; - } - - /** - * The label of this button - */ - public get label() { - return this.data.label; - } - - /** - * The emoji used in this button - */ - public get emoji() { - return this.data.emoji; - } - - /** - * Whether this button is disabled - */ - public get disabled() { - return this.data.disabled; - } - - /** - * The custom id of this button (only defined on non-link buttons) - */ - public get customId(): string | undefined { - return (this.data as APIButtonComponentWithCustomId).custom_id; - } - - /** - * The URL of this button (only defined on link buttons) - */ - public get url(): string | undefined { - return (this.data as APIButtonComponentWithURL).url; - } - /** * Sets the style of this button * @param style The style of the button @@ -119,11 +78,4 @@ export class UnsafeButtonComponent extends Component ...this.data, } as APIButtonComponent; } - - public equals(other: APIButtonComponent | UnsafeButtonComponent) { - if (other instanceof UnsafeButtonComponent) { - return isEqual(other.data, this.data); - } - return isEqual(other, this.data); - } } diff --git a/packages/builders/src/components/selectMenu/SelectMenu.ts b/packages/builders/src/components/selectMenu/SelectMenu.ts index 9d2c11c16107..55a6589480bb 100644 --- a/packages/builders/src/components/selectMenu/SelectMenu.ts +++ b/packages/builders/src/components/selectMenu/SelectMenu.ts @@ -6,12 +6,12 @@ import { placeholderValidator, validateRequiredSelectMenuParameters, } from '../Assertions'; -import { UnsafeSelectMenuComponent } from './UnsafeSelectMenu'; +import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu'; /** * Represents a validated select menu component */ -export class SelectMenuComponent extends UnsafeSelectMenuComponent { +export class SelectMenuBuilder extends UnsafeSelectMenuBuilder { public override setPlaceholder(placeholder: string) { return super.setPlaceholder(placeholderValidator.parse(placeholder)); } @@ -33,7 +33,7 @@ export class SelectMenuComponent extends UnsafeSelectMenuComponent { } public override toJSON(): APISelectMenuComponent { - validateRequiredSelectMenuParameters(this.options, this.customId); + validateRequiredSelectMenuParameters(this.options, this.data.custom_id); return super.toJSON(); } } diff --git a/packages/builders/src/components/selectMenu/SelectMenuOption.ts b/packages/builders/src/components/selectMenu/SelectMenuOption.ts index a5877943377d..f861a966ab17 100644 --- a/packages/builders/src/components/selectMenu/SelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/SelectMenuOption.ts @@ -5,12 +5,12 @@ import { labelValueValidator, validateRequiredSelectMenuOptionParameters, } from '../Assertions'; -import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; +import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption'; /** * Represents a validated option within a select menu component */ -export class SelectMenuOption extends UnsafeSelectMenuOption { +export class SelectMenuOptionBuilder extends UnsafeSelectMenuOptionBuilder { public override setDescription(description: string) { return super.setDescription(labelValueValidator.parse(description)); } @@ -24,7 +24,7 @@ export class SelectMenuOption extends UnsafeSelectMenuOption { } public override toJSON(): APISelectMenuOption { - validateRequiredSelectMenuOptionParameters(this.label, this.value); + validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value); return super.toJSON(); } } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 386f970f1758..2a33ec879be5 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -1,58 +1,22 @@ import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; -import { Component } from '../Component'; -import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; -import isEqual from 'fast-deep-equal'; +import { ComponentBuilder } from '../Component'; +import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption'; /** * Represents a non-validated select menu component */ -export class UnsafeSelectMenuComponent extends Component< +export class UnsafeSelectMenuBuilder extends ComponentBuilder< Partial> & { type: ComponentType.SelectMenu } > { /** * The options within this select menu */ - public readonly options: UnsafeSelectMenuOption[]; + protected readonly options: UnsafeSelectMenuOptionBuilder[]; public constructor(data?: Partial) { const { options, ...initData } = data ?? {}; super({ type: ComponentType.SelectMenu, ...initData }); - this.options = options?.map((o) => new UnsafeSelectMenuOption(o)) ?? []; - } - - /** - * The placeholder for this select menu - */ - public get placeholder() { - return this.data.placeholder; - } - - /** - * The maximum amount of options that can be selected - */ - public get maxValues() { - return this.data.max_values; - } - - /** - * The minimum amount of options that must be selected - */ - public get minValues() { - return this.data.min_values; - } - - /** - * The custom id of this select menu - */ - public get customId() { - return this.data.custom_id; - } - - /** - * Whether this select menu is disabled - */ - public get disabled() { - return this.data.disabled; + this.options = options?.map((o) => new UnsafeSelectMenuOptionBuilder(o)) ?? []; } /** @@ -105,10 +69,10 @@ export class UnsafeSelectMenuComponent extends Component< * @param options The options to add to this select menu * @returns */ - public addOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) { + public addOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) { this.options.push( ...options.map((option) => - option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option), + option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option), ), ); return this; @@ -118,12 +82,12 @@ export class UnsafeSelectMenuComponent extends Component< * Sets the options on this select menu * @param options The options to set on this select menu */ - public setOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) { + public setOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) { this.options.splice( 0, this.options.length, ...options.map((option) => - option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option), + option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option), ), ); return this; @@ -136,14 +100,4 @@ export class UnsafeSelectMenuComponent extends Component< options: this.options.map((o) => o.toJSON()), } as APISelectMenuComponent; } - - public equals(other: APISelectMenuComponent | UnsafeSelectMenuComponent): boolean { - if (other instanceof UnsafeSelectMenuComponent) { - return isEqual(other.data, this.data) && isEqual(other.options, this.options); - } - return isEqual(other, { - ...this.data, - options: this.options.map((o) => o.toJSON()), - }); - } } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts index 5b0e86ae418d..8cf0f1196998 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts @@ -3,43 +3,8 @@ import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api- /** * Represents a non-validated option within a select menu component */ -export class UnsafeSelectMenuOption { - public constructor(protected data: Partial = {}) {} - - /** - * The label for this option - */ - public get label() { - return this.data.label; - } - - /** - * The value for this option - */ - public get value() { - return this.data.value; - } - - /** - * The description for this option - */ - public get description() { - return this.data.description; - } - - /** - * The emoji for this option - */ - public get emoji() { - return this.data.emoji; - } - - /** - * Whether this option is selected by default - */ - public get default() { - return this.data.default; - } +export class UnsafeSelectMenuOptionBuilder { + public constructor(public data: Partial = {}) {} /** * Sets the label of this option diff --git a/packages/builders/src/components/textInput/TextInput.ts b/packages/builders/src/components/textInput/TextInput.ts index 15e260dc4085..ee70d9b0b5a0 100644 --- a/packages/builders/src/components/textInput/TextInput.ts +++ b/packages/builders/src/components/textInput/TextInput.ts @@ -7,9 +7,9 @@ import { valueValidator, validateRequiredParameters, } from './Assertions'; -import { UnsafeTextInputComponent } from './UnsafeTextInput'; +import { UnsafeTextInputBuilder } from './UnsafeTextInput'; -export class TextInputComponent extends UnsafeTextInputComponent { +export class TextInputBuilder extends UnsafeTextInputBuilder { public override setMinLength(minLength: number) { return super.setMinLength(minLengthValidator.parse(minLength)); } diff --git a/packages/builders/src/components/textInput/UnsafeTextInput.ts b/packages/builders/src/components/textInput/UnsafeTextInput.ts index 52462a7e8741..5b4af9f3f532 100644 --- a/packages/builders/src/components/textInput/UnsafeTextInput.ts +++ b/packages/builders/src/components/textInput/UnsafeTextInput.ts @@ -1,73 +1,17 @@ import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v9'; -import { Component } from '../../index'; +import { ComponentBuilder } from '../../index'; import isEqual from 'fast-deep-equal'; -export class UnsafeTextInputComponent extends Component< +export class UnsafeTextInputBuilder extends ComponentBuilder< Partial & { type: ComponentType.TextInput } > { public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) { super({ type: ComponentType.TextInput, ...data }); } - /** - * The style of this text input - */ - public get style() { - return this.data.style; - } - - /** - * The custom id of this text input - */ - public get customId() { - return this.data.custom_id; - } - - /** - * The label for this text input - */ - public get label() { - return this.data.label; - } - - /** - * The placeholder text for this text input - */ - public get placeholder() { - return this.data.placeholder; - } - - /** - * The default value for this text input - */ - public get value() { - return this.data.value; - } - - /** - * The minimum length of this text input - */ - public get minLength() { - return this.data.min_length; - } - - /** - * The maximum length of this text input - */ - public get maxLength() { - return this.data.max_length; - } - - /** - * Whether this text input is required - */ - public get required() { - return this.data.required; - } - /** * Sets the custom id for this text input - * @param customId The custom id of this text input + * @param customId The custom id of this text inputå */ public setCustomId(customId: string) { this.data.custom_id = customId; @@ -144,8 +88,8 @@ export class UnsafeTextInputComponent extends Component< } as APITextInputComponent; } - public equals(other: UnsafeTextInputComponent | APITextInputComponent): boolean { - if (other instanceof UnsafeTextInputComponent) { + public equals(other: UnsafeTextInputBuilder | APITextInputComponent): boolean { + if (other instanceof UnsafeTextInputBuilder) { return isEqual(other.data, this.data); } diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index aeba12bf3735..e04daf416595 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -37,3 +37,5 @@ export * as ContextMenuCommandAssertions from './interactions/contextMenuCommand export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder'; export * from './util/jsonEncodable'; +export * from './util/equatable'; +export * from './util/componentUtil'; diff --git a/packages/builders/src/interactions/modals/Assertions.ts b/packages/builders/src/interactions/modals/Assertions.ts index cb4e6ee33f2e..ecf59b9c3da4 100644 --- a/packages/builders/src/interactions/modals/Assertions.ts +++ b/packages/builders/src/interactions/modals/Assertions.ts @@ -1,14 +1,14 @@ import { z } from 'zod'; -import { ActionRow, type ModalActionRowComponent } from '../..'; +import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../..'; import { customIdValidator } from '../../components/Assertions'; export const titleValidator = z.string().min(1).max(45); -export const componentsValidator = z.array(z.instanceof(ActionRow)).min(1); +export const componentsValidator = z.array(z.instanceof(ActionRowBuilder)).min(1); export function validateRequiredParameters( customId?: string, title?: string, - components?: ActionRow[], + components?: ActionRowBuilder[], ) { customIdValidator.parse(customId); titleValidator.parse(title); diff --git a/packages/builders/src/interactions/modals/Modal.ts b/packages/builders/src/interactions/modals/Modal.ts index d61f26435ecd..3e305ac70cf6 100644 --- a/packages/builders/src/interactions/modals/Modal.ts +++ b/packages/builders/src/interactions/modals/Modal.ts @@ -1,9 +1,9 @@ import type { APIModalInteractionResponseCallbackData } from 'discord-api-types/v9'; import { customIdValidator } from '../../components/Assertions'; import { titleValidator, validateRequiredParameters } from './Assertions'; -import { UnsafeModal } from './UnsafeModal'; +import { UnsafeModalBuilder } from './UnsafeModal'; -export class Modal extends UnsafeModal { +export class ModalBuilder extends UnsafeModalBuilder { public override setCustomId(customId: string): this { return super.setCustomId(customIdValidator.parse(customId)); } diff --git a/packages/builders/src/interactions/modals/UnsafeModal.ts b/packages/builders/src/interactions/modals/UnsafeModal.ts index 915ad55d0e86..6a305b393486 100644 --- a/packages/builders/src/interactions/modals/UnsafeModal.ts +++ b/packages/builders/src/interactions/modals/UnsafeModal.ts @@ -3,29 +3,16 @@ import type { APIModalActionRowComponent, APIModalInteractionResponseCallbackData, } from 'discord-api-types/v9'; -import { ActionRow, createComponent, JSONEncodable, ModalActionRowComponent } from '../../index'; +import { ActionRowBuilder, createComponentBuilder, JSONEncodable, ModalActionRowComponentBuilder } from '../../index'; -export class UnsafeModal implements JSONEncodable { +export class UnsafeModalBuilder implements JSONEncodable { protected readonly data: Partial>; - public readonly components: ActionRow[] = []; + public readonly components: ActionRowBuilder[] = []; public constructor({ components, ...data }: Partial = {}) { this.data = { ...data }; - this.components = (components?.map((c) => createComponent(c)) ?? []) as ActionRow[]; - } - - /** - * The custom id of this modal - */ - public get customId() { - return this.data.custom_id; - } - - /** - * The title of this modal - */ - public get title() { - return this.data.title; + this.components = (components?.map((c) => createComponentBuilder(c)) ?? + []) as ActionRowBuilder[]; } /** @@ -51,11 +38,16 @@ export class UnsafeModal implements JSONEncodable | APIActionRowComponent)[] + ...components: ( + | ActionRowBuilder + | APIActionRowComponent + )[] ) { this.components.push( ...components.map((component) => - component instanceof ActionRow ? component : new ActionRow(component), + component instanceof ActionRowBuilder + ? component + : new ActionRowBuilder(component), ), ); return this; @@ -65,7 +57,7 @@ export class UnsafeModal implements JSONEncodable[]) { + public setComponents(...components: ActionRowBuilder[]) { this.components.splice(0, this.components.length, ...components); return this; } diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index cee3d47c4387..7c8ff26048e1 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -10,15 +10,15 @@ import { urlPredicate, validateFieldLength, } from './Assertions'; -import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbed } from './UnsafeEmbed'; +import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbedBuilder } from './UnsafeEmbed'; /** * Represents a validated embed in a message (image/video preview, rich embed, etc.) */ -export class Embed extends UnsafeEmbed { +export class EmbedBuilder extends UnsafeEmbedBuilder { public override addFields(...fields: APIEmbedField[]): this { // Ensure adding these fields won't exceed the 25 field limit - validateFieldLength(fields.length, this.fields); + validateFieldLength(fields.length, this.data.fields); // Data assertions return super.addFields(...embedFieldsArrayPredicate.parse(fields)); @@ -26,7 +26,7 @@ export class Embed extends UnsafeEmbed { public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { // Ensure adding these fields won't exceed the 25 field limit - validateFieldLength(fields.length - deleteCount, this.fields); + validateFieldLength(fields.length - deleteCount, this.data.fields); // Data assertions return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields)); diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index 698c95b61b36..1f9fb4ea1d0b 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -1,13 +1,4 @@ -import type { - APIEmbed, - APIEmbedAuthor, - APIEmbedField, - APIEmbedFooter, - APIEmbedImage, - APIEmbedVideo, -} from 'discord-api-types/v9'; -import type { Equatable } from '../../util/equatable'; -import isEqual from 'fast-deep-equal'; +import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v9'; export type RGBTuple = [red: number, green: number, blue: number]; @@ -40,7 +31,7 @@ export interface EmbedImageData extends Omit { /** * Represents a non-validated embed in a message (image/video preview, rich embed, etc.) */ -export class UnsafeEmbed implements Equatable { +export class UnsafeEmbedBuilder { public readonly data: APIEmbed; public constructor(data: APIEmbed = {}) { @@ -48,133 +39,6 @@ export class UnsafeEmbed implements Equatable { if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString(); } - /** - * An array of fields of this embed - */ - public get fields() { - return this.data.fields; - } - - /** - * The embed title - */ - public get title() { - return this.data.title; - } - - /** - * The embed description - */ - public get description() { - return this.data.description; - } - - /** - * The embed URL - */ - public get url() { - return this.data.url; - } - - /** - * The embed color - */ - public get color() { - return this.data.color; - } - - /** - * The timestamp of the embed in an ISO 8601 format - */ - public get timestamp() { - return this.data.timestamp; - } - - /** - * The embed thumbnail data - */ - public get thumbnail(): EmbedImageData | undefined { - if (!this.data.thumbnail) return undefined; - return { - url: this.data.thumbnail.url, - proxyURL: this.data.thumbnail.proxy_url, - height: this.data.thumbnail.height, - width: this.data.thumbnail.width, - }; - } - - /** - * The embed image data - */ - public get image(): EmbedImageData | undefined { - if (!this.data.image) return undefined; - return { - url: this.data.image.url, - proxyURL: this.data.image.proxy_url, - height: this.data.image.height, - width: this.data.image.width, - }; - } - - /** - * Received video data - */ - public get video(): APIEmbedVideo | undefined { - return this.data.video; - } - - /** - * The embed author data - */ - public get author(): EmbedAuthorData | undefined { - if (!this.data.author) return undefined; - return { - name: this.data.author.name, - url: this.data.author.url, - iconURL: this.data.author.icon_url, - proxyIconURL: this.data.author.proxy_icon_url, - }; - } - - /** - * Received data about the embed provider - */ - public get provider() { - return this.data.provider; - } - - /** - * The embed footer data - */ - public get footer(): EmbedFooterData | undefined { - if (!this.data.footer) return undefined; - return { - text: this.data.footer.text, - iconURL: this.data.footer.icon_url, - proxyIconURL: this.data.footer.proxy_icon_url, - }; - } - - /** - * The accumulated length for the embed title, description, fields, footer text, and author name - */ - public get length(): number { - return ( - (this.data.title?.length ?? 0) + - (this.data.description?.length ?? 0) + - (this.data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) + - (this.data.footer?.text.length ?? 0) + - (this.data.author?.name.length ?? 0) - ); - } - - /** - * The hex color of the current color of the embed - */ - public get hexColor() { - return typeof this.data.color === 'number' ? `#${this.data.color.toString(16).padStart(6, '0')}` : this.data.color; - } - /** * Adds fields to the embed (max 25) * @@ -204,7 +68,7 @@ export class UnsafeEmbed implements Equatable { * @param fields The fields to set */ public setFields(...fields: APIEmbedField[]) { - this.spliceFields(0, this.fields?.length ?? 0, ...fields); + this.spliceFields(0, this.data.fields?.length ?? 0, ...fields); return this; } @@ -319,11 +183,4 @@ export class UnsafeEmbed implements Equatable { public toJSON(): APIEmbed { return { ...this.data }; } - - public equals(other: UnsafeEmbed | APIEmbed) { - const { image: thisImage, thumbnail: thisThumbnail, ...thisData } = this.data; - const data = other instanceof UnsafeEmbed ? other.data : other; - const { image, thumbnail, ...otherData } = data; - return isEqual(otherData, thisData) && image?.url === thisImage?.url && thumbnail?.url === thisThumbnail?.url; - } } diff --git a/packages/builders/src/util/componentUtil.ts b/packages/builders/src/util/componentUtil.ts new file mode 100644 index 000000000000..5d2385a3e89a --- /dev/null +++ b/packages/builders/src/util/componentUtil.ts @@ -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) + ); +} diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 94fb37530166..25e724b9c9d3 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -53,6 +53,7 @@ "@sapphire/snowflake": "^3.1.0", "@types/ws": "^8.2.2", "discord-api-types": "^0.27.3", + "fast-deep-equal": "^3.1.3", "lodash.snakecase": "^4.1.1", "undici": "^4.14.1", "ws": "^8.5.0" diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 7945d6b625ef..7115cea8c69a 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -71,6 +71,7 @@ exports.WebSocketShard = require('./client/websocket/WebSocketShard'); // Structures exports.ActionRow = require('./structures/ActionRow'); +exports.ActionRowBuilder = require('./structures/ActionRowBuilder'); exports.Activity = require('./structures/Presence').Activity; exports.AnonymousGuild = require('./structures/AnonymousGuild'); exports.Application = require('./structures/interfaces/Application'); @@ -81,6 +82,7 @@ exports.BaseGuild = require('./structures/BaseGuild'); exports.BaseGuildEmoji = require('./structures/BaseGuildEmoji'); exports.BaseGuildTextChannel = require('./structures/BaseGuildTextChannel'); exports.BaseGuildVoiceChannel = require('./structures/BaseGuildVoiceChannel'); +exports.ButtonBuilder = require('./structures/ButtonBuilder'); exports.ButtonComponent = require('./structures/ButtonComponent'); exports.ButtonInteraction = require('./structures/ButtonInteraction'); exports.CategoryChannel = require('./structures/CategoryChannel'); @@ -92,9 +94,11 @@ exports.ClientUser = require('./structures/ClientUser'); exports.CommandInteraction = require('./structures/CommandInteraction'); exports.Collector = require('./structures/interfaces/Collector'); exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver'); +exports.Component = require('./structures/Component'); exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction'); exports.DMChannel = require('./structures/DMChannel'); exports.Embed = require('./structures/Embed'); +exports.EmbedBuilder = require('./structures/EmbedBuilder'); exports.UnsafeEmbed = require('@discordjs/builders').UnsafeEmbed; exports.Emoji = require('./structures/Emoji').Emoji; exports.Guild = require('./structures/Guild').Guild; @@ -136,6 +140,7 @@ exports.ReactionCollector = require('./structures/ReactionCollector'); exports.ReactionEmoji = require('./structures/ReactionEmoji'); exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets; exports.Role = require('./structures/Role').Role; +exports.SelectMenuBuilder = require('./structures/SelectMenuBuilder'); exports.SelectMenuComponent = require('./structures/SelectMenuComponent'); exports.SelectMenuInteraction = require('./structures/SelectMenuInteraction'); exports.StageChannel = require('./structures/StageChannel'); @@ -146,6 +151,7 @@ exports.StoreChannel = require('./structures/StoreChannel'); exports.Team = require('./structures/Team'); exports.TeamMember = require('./structures/TeamMember'); exports.TextChannel = require('./structures/TextChannel'); +exports.TextInputBuilder = require('./structures/TextInputBuilder'); exports.TextInputComponent = require('./structures/TextInputComponent'); exports.ThreadChannel = require('./structures/ThreadChannel'); exports.ThreadMember = require('./structures/ThreadMember'); @@ -191,6 +197,7 @@ exports.InviteTargetType = require('discord-api-types/v9').InviteTargetType; exports.Locale = require('discord-api-types/v9').Locale; exports.MessageType = require('discord-api-types/v9').MessageType; exports.MessageFlags = require('discord-api-types/v9').MessageFlags; +exports.ModalBuilder = require('@discordjs/builders').ModalBuilder; exports.OAuth2Scopes = require('discord-api-types/v9').OAuth2Scopes; exports.PermissionFlagsBits = require('discord-api-types/v9').PermissionFlagsBits; exports.RESTJSONErrorCodes = require('discord-api-types/v9').RESTJSONErrorCodes; @@ -200,10 +207,10 @@ exports.StickerFormatType = require('discord-api-types/v9').StickerFormatType; exports.TextInputStyle = require('discord-api-types/v9').TextInputStyle; exports.UserFlags = require('discord-api-types/v9').UserFlags; exports.WebhookType = require('discord-api-types/v9').WebhookType; -exports.UnsafeButtonComponent = require('@discordjs/builders').UnsafeButtonComponent; -exports.UnsafeSelectMenuComponent = require('@discordjs/builders').UnsafeSelectMenuComponent; -exports.SelectMenuOption = require('@discordjs/builders').SelectMenuOption; -exports.UnsafeSelectMenuOption = require('@discordjs/builders').UnsafeSelectMenuOption; +exports.UnsafeButtonBuilder = require('@discordjs/builders').UnsafeButtonBuilder; +exports.UnsafeSelectMenuBuilder = require('@discordjs/builders').UnsafeSelectMenuBuilder; +exports.SelectMenuOptionBuilder = require('@discordjs/builders').SelectMenuOptionBuilder; +exports.UnsafeSelectMenuOptionBuilder = require('@discordjs/builders').UnsafeSelectMenuOptionBuilder; exports.DiscordAPIError = require('@discordjs/rest').DiscordAPIError; exports.HTTPError = require('@discordjs/rest').HTTPError; exports.RateLimitError = require('@discordjs/rest').RateLimitError; diff --git a/packages/discord.js/src/structures/ActionRow.js b/packages/discord.js/src/structures/ActionRow.js index cc43ffe739db..3d3b247db244 100644 --- a/packages/discord.js/src/structures/ActionRow.js +++ b/packages/discord.js/src/structures/ActionRow.js @@ -1,14 +1,21 @@ 'use strict'; -const { ActionRow: BuildersActionRow, Component } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); +const Component = require('./Component'); +const Components = require('../util/Components'); -class ActionRow extends BuildersActionRow { - constructor({ components, ...data } = {}) { - super({ - components: components?.map(c => (c instanceof Component ? c : Transformers.toSnakeCase(c))), - ...Transformers.toSnakeCase(data), - }); +/** + * Represents an action row + * @extends {Component} + */ +class ActionRow extends Component { + constructor({ components, ...data }) { + super(data); + /** + * The components in this action row + * @type {Component[]} + * @readonly + */ + this.components = components.map(c => Components.createComponent(c)); } } diff --git a/packages/discord.js/src/structures/ActionRowBuilder.js b/packages/discord.js/src/structures/ActionRowBuilder.js new file mode 100644 index 000000000000..f7c97a9b1560 --- /dev/null +++ b/packages/discord.js/src/structures/ActionRowBuilder.js @@ -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; diff --git a/packages/discord.js/src/structures/ButtonBuilder.js b/packages/discord.js/src/structures/ButtonBuilder.js new file mode 100644 index 000000000000..403b1ff1c57c --- /dev/null +++ b/packages/discord.js/src/structures/ButtonBuilder.js @@ -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} other The other data + * @returns {ButtonBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = ButtonBuilder; diff --git a/packages/discord.js/src/structures/ButtonComponent.js b/packages/discord.js/src/structures/ButtonComponent.js index a6cce10821c9..b6dd52a4d35b 100644 --- a/packages/discord.js/src/structures/ButtonComponent.js +++ b/packages/discord.js/src/structures/ButtonComponent.js @@ -1,11 +1,64 @@ 'use strict'; -const { ButtonComponent: BuildersButtonComponent } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); +const Component = require('./Component'); -class ButtonComponent extends BuildersButtonComponent { - constructor(data) { - super(Transformers.toSnakeCase(data)); +/** + * Represents a button component + * @extends {Component} + */ +class ButtonComponent extends Component { + /** + * The style of this button + * @type {ButtonStyle} + * @readonly + */ + get style() { + return this.data.style; + } + + /** + * The label of this button + * @type {?string} + * @readonly + */ + get label() { + return this.data.label ?? null; + } + + /** + * The emoji used in this button + * @type {?APIMessageComponentEmoji} + * @readonly + */ + get emoji() { + return this.data.emoji ?? null; + } + + /** + * Whether this button is disabled + * @type {?boolean} + * @readonly + */ + get disabled() { + return this.data.disabled ?? null; + } + + /** + * The custom id of this button (only defined on non-link buttons) + * @type {?string} + * @readonly + */ + get customId() { + return this.data.custom_id ?? null; + } + + /** + * The URL of this button (only defined on link buttons) + * @type {?string} + * @readonly + */ + get url() { + return this.data.url ?? null; } } diff --git a/packages/discord.js/src/structures/Component.js b/packages/discord.js/src/structures/Component.js new file mode 100644 index 000000000000..f8251c51730e --- /dev/null +++ b/packages/discord.js/src/structures/Component.js @@ -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; diff --git a/packages/discord.js/src/structures/Embed.js b/packages/discord.js/src/structures/Embed.js index 86b3e50feafd..1fb148d4200f 100644 --- a/packages/discord.js/src/structures/Embed.js +++ b/packages/discord.js/src/structures/Embed.js @@ -1,15 +1,198 @@ 'use strict'; -const { Embed: BuildersEmbed } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); -const Util = require('../util/Util'); - -/** - * Represents an embed object - */ -class Embed extends BuildersEmbed { +const isEqual = require('fast-deep-equal'); +const { Util } = require('../util/Util'); + +class Embed { + /** + * Creates a new embed object + * @param {APIEmbed} data API embed data + * @private + */ constructor(data) { - super(Transformers.toSnakeCase(data)); + /** + * The API embed data + * @type {APIEmbed} + * @readonly + */ + this.data = { ...data }; + } + + /** + * An array of fields of this embed + * @type {?Array} + * @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); } /** diff --git a/packages/discord.js/src/structures/EmbedBuilder.js b/packages/discord.js/src/structures/EmbedBuilder.js new file mode 100644 index 000000000000..bdace0b23a97 --- /dev/null +++ b/packages/discord.js/src/structures/EmbedBuilder.js @@ -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} other The other data + * @returns {EmbedBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = EmbedBuilder; diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index dd8d28726031..b4602fc45a42 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -1,6 +1,5 @@ 'use strict'; -const { createComponent, Embed } = require('@discordjs/builders'); const { Collection } = require('@discordjs/collection'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { @@ -12,6 +11,7 @@ const { } = require('discord-api-types/v9'); const Base = require('./Base'); const ClientApplication = require('./ClientApplication'); +const Embed = require('./Embed'); const InteractionCollector = require('./InteractionCollector'); const MessageAttachment = require('./MessageAttachment'); const Mentions = require('./MessageMentions'); @@ -20,6 +20,7 @@ const ReactionCollector = require('./ReactionCollector'); const { Sticker } = require('./Sticker'); const { Error } = require('../errors'); const ReactionManager = require('../managers/ReactionManager'); +const Components = require('../util/Components'); const { NonSystemMessageTypes } = require('../util/Constants'); const MessageFlagsBitField = require('../util/MessageFlagsBitField'); const PermissionsBitField = require('../util/PermissionsBitField'); @@ -145,7 +146,7 @@ class Message extends Base { * A list of MessageActionRows in the message * @type {ActionRow[]} */ - this.components = data.components.map(c => createComponent(c)); + this.components = data.components.map(c => Components.createComponent(c)); } else { this.components = this.components?.slice() ?? []; } diff --git a/packages/discord.js/src/structures/Modal.js b/packages/discord.js/src/structures/Modal.js deleted file mode 100644 index b9ed1c399e32..000000000000 --- a/packages/discord.js/src/structures/Modal.js +++ /dev/null @@ -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; diff --git a/packages/discord.js/src/structures/SelectMenuBuilder.js b/packages/discord.js/src/structures/SelectMenuBuilder.js new file mode 100644 index 000000000000..649191de47fc --- /dev/null +++ b/packages/discord.js/src/structures/SelectMenuBuilder.js @@ -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} other The other data + * @returns {SelectMenuBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = SelectMenuBuilder; diff --git a/packages/discord.js/src/structures/SelectMenuComponent.js b/packages/discord.js/src/structures/SelectMenuComponent.js index 77589626ac25..1d3e80bc77d0 100644 --- a/packages/discord.js/src/structures/SelectMenuComponent.js +++ b/packages/discord.js/src/structures/SelectMenuComponent.js @@ -1,11 +1,64 @@ 'use strict'; -const { SelectMenuComponent: BuildersSelectMenuComponent } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); +const Component = require('./Component'); -class SelectMenuComponent extends BuildersSelectMenuComponent { - constructor(data) { - super(Transformers.toSnakeCase(data)); +/** + * Represents a select menu component + * @extends {Component} + */ +class SelectMenuComponent extends Component { + /** + * The placeholder for this select menu + * @type {?string} + * @readonly + */ + get placeholder() { + return this.data.placeholder ?? null; + } + + /** + * The maximum amount of options that can be selected + * @type {?number} + * @readonly + */ + get maxValues() { + return this.data.max_values ?? null; + } + + /** + * The minimum amount of options that must be selected + * @type {?number} + * @readonly + */ + get minValues() { + return this.data.min_values ?? null; + } + + /** + * The custom id of this select menu + * @type {string} + * @readonly + */ + get customId() { + return this.data.custom_id; + } + + /** + * Whether this select menu is disabled + * @type {?boolean} + * @readonly + */ + get disabled() { + return this.data.disabled ?? null; + } + + /** + * The options in this select menu + * @type {APISelectMenuOption[]} + * @readonly + */ + get options() { + return this.data.options; } } diff --git a/packages/discord.js/src/structures/TextInputBuilder.js b/packages/discord.js/src/structures/TextInputBuilder.js new file mode 100644 index 000000000000..b15ad9774a4a --- /dev/null +++ b/packages/discord.js/src/structures/TextInputBuilder.js @@ -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} other The other data + * @returns {TextInputBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = TextInputBuilder; diff --git a/packages/discord.js/src/structures/TextInputComponent.js b/packages/discord.js/src/structures/TextInputComponent.js index 28a214f2be16..58ea3a13e841 100644 --- a/packages/discord.js/src/structures/TextInputComponent.js +++ b/packages/discord.js/src/structures/TextInputComponent.js @@ -1,11 +1,20 @@ 'use strict'; -const { TextInputComponent: BuildersTextInputComponent } = require('@discordjs/builders'); -const Transformers = require('../util/Transformers'); +const Component = require('./Component'); -class TextInputComponent extends BuildersTextInputComponent { - constructor(data) { - super(Transformers.toSnakeCase(data)); +class TextInputComponent extends Component { + /** + * The custom id of this text input + */ + get customId() { + return this.data.custom_id; + } + + /** + * The value for this text input + */ + get value() { + return this.data.value; } } diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index d4696d8a9d62..95c00440e836 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -1,7 +1,7 @@ 'use strict'; // This file contains the typedefs for camel-cased json data - +const { ComponentType } = require('discord-api-types/v9'); /** * @typedef {Object} BaseComponentData * @property {ComponentType} type The type of component @@ -56,3 +56,34 @@ /** * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData} ComponentData */ + +class Components extends null { + /** + * Transforms API data into a component + * @param {APIMessageComponent|Component} data The data to create the component from + * @returns {Component} + */ + static createComponent(data) { + if (data instanceof Component) { + return data; + } + + switch (data.type) { + case ComponentType.ActionRow: + return new ActionRow(data); + case ComponentType.Button: + return new ButtonComponent(data); + case ComponentType.SelectMenu: + return new SelectMenuComponent(data); + default: + throw new Error(`Found unknown component type: ${data.type}`); + } + } +} + +module.exports = Components; + +const ActionRow = require('../structures/ActionRow'); +const ButtonComponent = require('../structures/ButtonComponent'); +const Component = require('../structures/Component'); +const SelectMenuComponent = require('../structures/SelectMenuComponent'); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 214829708d87..64edf2883bb3 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -1,24 +1,24 @@ import { - ActionRow as BuilderActionRow, - MessageActionRowComponent, + ActionRowBuilder as BuilderActionRow, + MessageActionRowComponentBuilder, blockQuote, bold, - ButtonComponent as BuilderButtonComponent, + ButtonBuilder as BuilderButtonComponent, channelMention, codeBlock, - Component, - Embed as BuildersEmbed, + EmbedBuilder as BuildersEmbed, formatEmoji, hideLinkEmbed, hyperlink, inlineCode, italic, + JSONEncodable, + MappedComponentTypes, memberNicknameMention, - Modal as BuilderModal, quote, roleMention, - SelectMenuComponent as BuilderSelectMenuComponent, - TextInputComponent as BuilderTextInputComponent, + SelectMenuBuilder as BuilderSelectMenuComponent, + TextInputBuilder as BuilderTextInputComponent, spoiler, strikethrough, time, @@ -26,7 +26,7 @@ import { TimestampStylesString, underscore, userMention, - ModalActionRowComponent, + ModalActionRowComponentBuilder, } from '@discordjs/builders'; import { Collection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; @@ -105,6 +105,11 @@ import { APITextInputComponent, APIModalActionRowComponent, APIModalComponent, + APISelectMenuOption, + APIEmbedField, + APIEmbedAuthor, + APIEmbedFooter, + APIEmbedImage, } from 'discord-api-types/v9'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -220,8 +225,10 @@ export interface ActionRowData extends BuilderActionRow { constructor( data?: @@ -232,6 +239,14 @@ export class ActionRow< ); } +export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent; +export type ModalActionRowComponent = TextInputComponent; + +export class ActionRow { + private constructor(data: APIActionRowComponent); + public readonly components: T[]; +} + export class ActivityFlagsBitField extends BitField { public static Flags: typeof ActivityFlags; public static resolve(bit?: BitFieldResolvable): number; @@ -356,7 +371,9 @@ export interface InteractionResponseFields deferReply(options?: InteractionDeferReplyOptions): Promise; fetchReply(): Promise>; followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; - showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise; + showModal( + modal: JSONEncodable | ModalData | APIModalInteractionResponseCallbackData, + ): Promise; } export abstract class CommandInteraction extends Interaction { @@ -395,7 +412,9 @@ export abstract class CommandInteraction e public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; - public showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise; + public showModal( + modal: JSONEncodable | ModalData | APIModalInteractionResponseCallbackData, + ): Promise; private transformOption( option: APIApplicationCommandOption, resolved: APIApplicationCommandInteractionData['resolved'], @@ -502,22 +521,53 @@ export class ButtonInteraction extends Mes public inRawGuild(): this is ButtonInteraction<'raw'>; } -export class ButtonComponent extends BuilderButtonComponent { +export class Component { + public readonly data: Readonly; + public get type(): T['type']; + public toJSON(): T; + public equals(other: this | T): boolean; +} + +export class ButtonComponent extends Component { + 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 & { type?: ComponentType.Button })); + public static from(other: JSONEncodable | APIButtonComponent): ButtonBuilder; } -export class SelectMenuComponent extends BuilderSelectMenuComponent { +export class SelectMenuBuilder extends BuilderSelectMenuComponent { public constructor( data?: SelectMenuComponentData | (Omit & { type?: ComponentType.SelectMenu }), ); + public static from(other: JSONEncodable | APISelectMenuComponent): SelectMenuBuilder; } -export class TextInputComponent extends BuilderTextInputComponent { +export class TextInputBuilder extends BuilderTextInputComponent { public constructor(data?: TextInputComponentData | APITextInputComponent); + public static from(other: JSONEncodable | APITextInputComponent): TextInputBuilder; } -export class Modal extends BuilderModal { - public constructor(data?: ModalData | APIModalActionRowComponent); +export class TextInputComponent extends Component { + public get customId(): string; + public get value(): string; +} + +export class SelectMenuComponent extends Component { + private constructor(data: APISelectMenuComponent); + public get placeholder(): string | null; + public get maxValues(): number | null; + public get minValues(): number | null; + public get customId(): string; + public get disabled(): boolean | null; + public get options(): APISelectMenuOption[]; } export interface EmbedData { @@ -535,18 +585,43 @@ export interface EmbedData { fields?: EmbedFieldData[]; } -export interface EmbedImageData { - url?: string; +export interface IconData { + iconURL?: string; + proxyIconURL?: string; } +export type EmbedAuthorData = Omit & IconData; + +export type EmbedFooterData = Omit & IconData; + export interface EmbedProviderData { name?: string; url?: string; } -export class Embed extends BuildersEmbed { +export interface EmbedImageData extends Omit { + proxyURL?: string; +} + +export class EmbedBuilder extends BuildersEmbed { public constructor(data?: EmbedData | APIEmbed); public override setColor(color: ColorResolvable | null): this; + public static from(other: JSONEncodable | APIEmbed): EmbedBuilder; +} + +export class Embed { + private constructor(data: APIEmbed); + public readonly data: Readonly; + public get fields(): APIEmbedField[] | null; + public get title(): string | null; + public get description(): string | null; + public get url(): string | null; + public get color(): number | null; + public get timestamp(): string | null; + public get thumbnail(): EmbedImageData | null; + public get image(): EmbedImageData | null; + public equals(other: Embed | APIEmbed): boolean; + public toJSON(): APIEmbed; } export interface MappedChannelCategoryTypes { @@ -1652,7 +1727,9 @@ export class MessageComponentInteraction e public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise>; public update(options: string | MessagePayload | InteractionUpdateOptions): Promise; - public showModal(modal: Modal | ModalData | APIModalInteractionResponseCallbackData): Promise; + public showModal( + modal: JSONEncodable | ModalData | APIModalInteractionResponseCallbackData, + ): Promise; } export class MessageContextMenuCommandInteraction< @@ -1741,17 +1818,11 @@ export class MessageReaction { public toJSON(): unknown; } -export interface ModalFieldData { - value: string; - type: ComponentType; - customId: string; -} - export class ModalSubmitFieldsResolver { - constructor(components: ModalFieldData[][]); - public components: ModalFieldData[][]; - public fields: Collection; - public getField(customId: string): ModalFieldData; + constructor(components: ModalActionRowComponent[][]); + public components: ActionRow; + public fields: Collection; + public getField(customId: string): ModalActionRowComponent; public getTextInputValue(customId: string): string; } @@ -1769,7 +1840,7 @@ export interface ModalMessageModalSubmitInteraction[]; } export class ModalSubmitInteraction extends Interaction { @@ -2452,6 +2523,14 @@ export class Util extends null { public static splitMessage(text: string, options?: SplitOptions): string[]; } +export class Components extends null { + public static createComponentBuilder( + data: APIMessageComponent & { type: T }, + ): MappedComponentTypes[T]; + public static createComponentBuilder(data: C): C; + public static createComponentBuilder(data: APIMessageComponent | Component): Component; +} + export class Formatters extends null { public static blockQuote: typeof blockQuote; public static bold: typeof bold; @@ -3966,12 +4045,6 @@ export interface EditGuildTemplateOptions { description?: string; } -export interface EmbedAuthorData { - name: string; - url?: string; - iconURL?: string; -} - export interface EmbedField { name: string; value: string; @@ -3984,11 +4057,6 @@ export interface EmbedFieldData { inline?: boolean; } -export interface EmbedFooterData { - text: string; - iconURL?: string; -} - export type EmojiIdentifierResolvable = string | EmojiResolvable; export type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; @@ -4646,7 +4714,11 @@ export interface MessageCollectorOptions extends CollectorOptions<[Message]> { maxProcessed?: number; } -export type MessageComponent = Component | ActionRow | ButtonComponent | SelectMenuComponent; +export type MessageComponent = + | Component + | ActionRowBuilder + | ButtonComponent + | SelectMenuComponent; export type MessageComponentCollectorOptions = Omit< InteractionCollectorOptions, @@ -4666,6 +4738,7 @@ export interface MessageEditOptions { flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; components?: ( + | JSONEncodable> | ActionRow | (Required & ActionRowData) | APIActionRowComponent @@ -4705,8 +4778,9 @@ export interface MessageOptions { tts?: boolean; nonce?: string | number; content?: string | null; - embeds?: (Embed | APIEmbed)[]; + embeds?: (JSONEncodable | APIEmbed)[]; components?: ( + | JSONEncodable> | ActionRow | (Required & ActionRowData) | APIActionRowComponent @@ -5255,6 +5329,8 @@ export { ApplicationCommandType, ApplicationCommandOptionType, ApplicationCommandPermissionType, + APIEmbedField, + APISelectMenuOption, AuditLogEvent, ButtonStyle, ChannelType, @@ -5290,12 +5366,13 @@ export { WebhookType, } from 'discord-api-types/v9'; export { - UnsafeButtonComponent, - UnsafeSelectMenuComponent, - SelectMenuOption, - UnsafeSelectMenuOption, - MessageActionRowComponent, - UnsafeEmbed, - ModalActionRowComponent, + UnsafeButtonBuilder, + UnsafeSelectMenuBuilder, + SelectMenuOptionBuilder, + UnsafeSelectMenuOptionBuilder, + MessageActionRowComponentBuilder, + ModalActionRowComponentBuilder, + UnsafeEmbedBuilder, + ModalBuilder, } from '@discordjs/builders'; export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest'; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 892acf532c65..5c289728efe9 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -20,6 +20,8 @@ import { AuditLogEvent, ButtonStyle, TextInputStyle, + APITextInputComponent, + APIEmbed, } from 'discord-api-types/v9'; import { ApplicationCommand, @@ -59,7 +61,7 @@ import { MessageCollector, MessageComponentInteraction, MessageReaction, - Modal, + ModalBuilder, NewsChannel, Options, PartialTextBasedChannelFields, @@ -95,10 +97,10 @@ import { GuildAuditLogs, StageInstance, PartialDMChannel, - ActionRow, + ActionRowBuilder, ButtonComponent, SelectMenuComponent, - MessageActionRowComponent, + MessageActionRowComponentBuilder, InteractionResponseFields, ThreadChannelType, Events, @@ -109,6 +111,12 @@ import { MessageActionRowComponentData, PartialThreadMember, ThreadMemberFlagsBitField, + ButtonBuilder, + EmbedBuilder, + MessageActionRowComponent, + SelectMenuBuilder, + TextInputBuilder, + TextInputComponent, Embed, } from '.'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; @@ -574,7 +582,7 @@ client.on('messageCreate', async message => { assertIsMessage(channel.send({ embeds: [] })); const attachment = new MessageAttachment('file.png'); - const embed = new Embed(); + const embed = new EmbedBuilder(); assertIsMessage(channel.send({ files: [attachment] })); assertIsMessage(channel.send({ embeds: [embed] })); assertIsMessage(channel.send({ embeds: [embed], files: [attachment] })); @@ -744,23 +752,24 @@ client.on('interactionCreate', async interaction => { if (!interaction.isCommand()) return; - void new ActionRow(); + void new ActionRowBuilder(); - const button = new ButtonComponent(); + const button = new ButtonBuilder(); - const actionRow = new ActionRow({ + const actionRow = new ActionRowBuilder({ type: ComponentType.ActionRow, components: [button.toJSON()], }); + actionRow.toJSON(); + await interaction.reply({ content: 'Hi!', components: [actionRow] }); // @ts-expect-error interaction.reply({ content: 'Hi!', components: [[button]] }); // @ts-expect-error - void new ActionRow({}); - + void new ActionRowBuilder({}); // @ts-expect-error await interaction.reply({ content: 'Hi!', components: [button] }); @@ -1336,34 +1345,34 @@ expectType(GuildTextBasedChannel); -const button = new ButtonComponent({ +const button = new ButtonBuilder({ label: 'test', style: ButtonStyle.Primary, customId: 'test', }); -const selectMenu = new SelectMenuComponent({ +const selectMenu = new SelectMenuBuilder({ maxValues: 10, minValues: 2, customId: 'test', }); -new ActionRow({ +new ActionRowBuilder({ components: [selectMenu.toJSON(), button.toJSON()], }); -new SelectMenuComponent({ +new SelectMenuBuilder({ customId: 'foo', }); -new ButtonComponent({ +new ButtonBuilder({ style: ButtonStyle.Danger, }); // @ts-expect-error -new Embed().setColor('abc'); +new EmbedBuilder().setColor('abc'); -new Embed().setColor('#ffffff'); +new EmbedBuilder().setColor('#ffffff'); expectNotAssignable>({ type: ComponentType.ActionRow, @@ -1379,7 +1388,7 @@ declare const chatInputInteraction: ChatInputCommandInteraction; expectType(chatInputInteraction.options.getAttachment('attachment', true)); expectType(chatInputInteraction.options.getAttachment('attachment')); -declare const modal: Modal; +declare const modal: ModalBuilder; chatInputInteraction.showModal(modal); @@ -1400,3 +1409,27 @@ chatInputInteraction.showModal({ }, ], }); + +declare const selectMenuData: APISelectMenuComponent; +SelectMenuBuilder.from(selectMenuData); + +declare const selectMenuComp: SelectMenuComponent; +SelectMenuBuilder.from(selectMenuComp); + +declare const buttonData: APIButtonComponent; +ButtonBuilder.from(buttonData); + +declare const buttonComp: ButtonComponent; +ButtonBuilder.from(buttonComp); + +declare const textInputData: APITextInputComponent; +TextInputBuilder.from(textInputData); + +declare const textInputComp: TextInputComponent; +TextInputBuilder.from(textInputComp); + +declare const embedData: APIEmbed; +EmbedBuilder.from(embedData); + +declare const embedComp: Embed; +EmbedBuilder.from(embedComp); diff --git a/yarn.lock b/yarn.lock index e7e6564cda65..2d177f4a3cda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4446,6 +4446,7 @@ __metadata: eslint-config-prettier: ^8.3.0 eslint-plugin-import: ^2.25.4 eslint-plugin-prettier: ^4.0.0 + fast-deep-equal: ^3.1.3 husky: ^7.0.4 is-ci: ^3.0.1 jest: ^27.5.1