mirror of
https://github.com/discordjs/discord.js.git
synced 2026-05-23 12:00:09 +00:00
Compare commits
48 Commits
@discordjs
...
@discordjs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5b71a756b | ||
|
|
6c781ede30 | ||
|
|
be38128ea1 | ||
|
|
737a97d068 | ||
|
|
b26af3cf38 | ||
|
|
169b05f319 | ||
|
|
bf0430f998 | ||
|
|
2da2fa01b2 | ||
|
|
1c5701651a | ||
|
|
b6a8264d6b | ||
|
|
a7196dc969 | ||
|
|
a03661844f | ||
|
|
fb2b7281e0 | ||
|
|
c303bf3329 | ||
|
|
c2c8cce1d7 | ||
|
|
abb84ce88f | ||
|
|
d317ca1053 | ||
|
|
072fbb228a | ||
|
|
548c25488a | ||
|
|
16a44f83e5 | ||
|
|
0dda270ea5 | ||
|
|
ee988e3e75 | ||
|
|
104ad754f3 | ||
|
|
0ff239a602 | ||
|
|
89fd19e08a | ||
|
|
6a6c7d0333 | ||
|
|
083f6abb38 | ||
|
|
5cc13b735c | ||
|
|
1e4d1dc04f | ||
|
|
177d81f596 | ||
|
|
bf4cfeb4bf | ||
|
|
11b236ff65 | ||
|
|
1d5b9837de | ||
|
|
8065b80cea | ||
|
|
3b26680672 | ||
|
|
c4dbd7ee9f | ||
|
|
72771b79aa | ||
|
|
63dbe48055 | ||
|
|
67c8953a10 | ||
|
|
30e35d909e | ||
|
|
6a5707c786 | ||
|
|
9b821e5dfc | ||
|
|
a04172325a | ||
|
|
154c00ded9 | ||
|
|
3b927449ae | ||
|
|
fcce0d95bb | ||
|
|
93e0f4cd10 | ||
|
|
abaae4ff16 |
@@ -2,6 +2,52 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# [@discordjs/builders@1.13.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.12.2...@discordjs/builders@1.13.0) - (2025-10-24)
|
||||
|
||||
## Features
|
||||
|
||||
- V1 builders file uploads support (#11196) ([1417c49](https://github.com/discordjs/discord.js/commit/1417c498a40b843d772ecf88dfff5f87a1665042))
|
||||
|
||||
## Testing
|
||||
|
||||
- Fix type error ([f780c6a](https://github.com/discordjs/discord.js/commit/f780c6a5500f7ea5c7a1ea7cd6720f6159d9d36e))
|
||||
|
||||
# [@discordjs/builders@1.12.2](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.12.1...@discordjs/builders@1.12.2) - (2025-10-09)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Assertions:** Literal default values ([43362c9](https://github.com/discordjs/discord.js/commit/43362c93525f98d72b894eb0fc6b358d30ec45b9))
|
||||
|
||||
# [@discordjs/builders@1.12.1](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.12.0...@discordjs/builders@1.12.1) - (2025-10-08)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **builders:** Text display component support for modals (#11155) ([99b8436](https://github.com/discordjs/discord.js/commit/99b8436117bc12654278337abc4a23f5bdf4ba46))
|
||||
|
||||
# [@discordjs/builders@1.12.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.11.3...@discordjs/builders@1.12.0) - (2025-10-08)
|
||||
|
||||
## Features
|
||||
|
||||
- **builders:** Modal select menus in builders v1 (#11138) ([ac683b9](https://github.com/discordjs/discord.js/commit/ac683b9d040635de8514c80a9d433d9c6d63701b))
|
||||
|
||||
# [@discordjs/builders@1.11.3](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.11.2...@discordjs/builders@1.11.3) - (2025-08-10)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **contextMenuCommands:** Remove regular expression validation (#10996) ([4906aae](https://github.com/discordjs/discord.js/commit/4906aaea4c0e6e868fa658d3359026eb662fbcb8))
|
||||
|
||||
# [@discordjs/builders@1.11.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.10.1...@discordjs/builders@1.11.0) - (2025-04-25)
|
||||
|
||||
## Features
|
||||
|
||||
- Components v2 in builders v1 (#10787) ([118e682](https://github.com/discordjs/discord.js/commit/118e6826821b3b90f5923e40f167747e0658cfd1))
|
||||
|
||||
# [@discordjs/builders@1.10.1](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.10.0...@discordjs/builders@1.10.1) - (2025-02-10)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **EmbedBuilder:** Allow empty `name` and `value` on fields (#10747) ([49ef3a8](https://github.com/discordjs/discord.js/commit/49ef3a833eab23d426d5c667e28aa493ddc9cb6c))
|
||||
|
||||
# [@discordjs/builders@1.9.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.8.2...@discordjs/builders@1.9.0) - (2024-09-01)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## Installation
|
||||
|
||||
**Node.js 18 or newer is required.**
|
||||
**Node.js 16.11.0 or newer is required.**
|
||||
|
||||
```sh
|
||||
npm install @discordjs/builders
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
type APIActionRowComponent,
|
||||
type APIMessageActionRowComponent,
|
||||
type APIComponentInMessageActionRow,
|
||||
} from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import {
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
StringSelectMenuOptionBuilder,
|
||||
} from '../../src/index.js';
|
||||
|
||||
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const rowWithButtonData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -25,7 +25,7 @@ const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
],
|
||||
};
|
||||
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -57,7 +57,7 @@ describe('Action Row Components', () => {
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const actionRowData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const actionRowData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -92,7 +92,7 @@ describe('Action Row Components', () => {
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const rowWithButtonData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -104,7 +104,7 @@ describe('Action Row Components', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ComponentType,
|
||||
TextInputStyle,
|
||||
type APIButtonComponent,
|
||||
type APIMessageActionRowComponent,
|
||||
type APIComponentInMessageActionRow,
|
||||
type APISelectMenuComponent,
|
||||
type APITextInputComponent,
|
||||
type APIActionRowComponent,
|
||||
@@ -27,7 +27,7 @@ describe('createComponentBuilder', () => {
|
||||
);
|
||||
|
||||
test('GIVEN an action row component THEN returns a ActionRowBuilder', () => {
|
||||
const actionRow: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
const actionRow: APIActionRowComponent<APIComponentInMessageActionRow> = {
|
||||
components: [],
|
||||
type: ComponentType.ActionRow,
|
||||
};
|
||||
|
||||
46
packages/builders/__tests__/components/fileUpload.test.ts
Normal file
46
packages/builders/__tests__/components/fileUpload.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { APIFileUploadComponent } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { FileUploadBuilder } from '../../src/components/fileUpload/FileUpload.js';
|
||||
|
||||
describe('File Upload Components', () => {
|
||||
test('Valid builder does not throw.', () => {
|
||||
expect(() => new FileUploadBuilder().setCustomId('file_upload').toJSON()).not.toThrowError();
|
||||
expect(() => new FileUploadBuilder().setCustomId('file_upload').setId(5).toJSON()).not.toThrowError();
|
||||
|
||||
expect(() =>
|
||||
new FileUploadBuilder().setCustomId('file_upload').setMaxValues(5).setMinValues(2).toJSON(),
|
||||
).not.toThrowError();
|
||||
|
||||
expect(() => new FileUploadBuilder().setCustomId('file_upload').setRequired(false).toJSON()).not.toThrowError();
|
||||
});
|
||||
|
||||
test('Invalid builder does throw.', () => {
|
||||
expect(() => new FileUploadBuilder().toJSON()).toThrowError();
|
||||
expect(() => new FileUploadBuilder().setCustomId('file_upload').setId(-3).toJSON()).toThrowError();
|
||||
expect(() => new FileUploadBuilder().setMaxValues(5).setMinValues(2).setId(10).toJSON()).toThrowError();
|
||||
expect(() => new FileUploadBuilder().setCustomId('file_upload').setMaxValues(500).toJSON()).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
new FileUploadBuilder().setCustomId('file_upload').setMinValues(500).setMaxValues(501).toJSON(),
|
||||
).toThrowError();
|
||||
|
||||
expect(() => new FileUploadBuilder().setRequired(false).toJSON()).toThrowError();
|
||||
});
|
||||
|
||||
test('API data equals toJSON().', () => {
|
||||
const fileUploadData = {
|
||||
type: ComponentType.FileUpload,
|
||||
custom_id: 'file_upload',
|
||||
min_values: 4,
|
||||
max_values: 9,
|
||||
required: false,
|
||||
} satisfies APIFileUploadComponent;
|
||||
|
||||
expect(new FileUploadBuilder(fileUploadData).toJSON()).toEqual(fileUploadData);
|
||||
|
||||
expect(
|
||||
new FileUploadBuilder().setCustomId('file_upload').setMinValues(4).setMaxValues(9).setRequired(false).toJSON(),
|
||||
).toEqual(fileUploadData);
|
||||
});
|
||||
});
|
||||
@@ -100,11 +100,11 @@ describe('Text Input Components', () => {
|
||||
.setPlaceholder('hello')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.toJSON();
|
||||
}).toThrowError();
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const textInputData: APITextInputComponent = {
|
||||
const textInputData = {
|
||||
type: ComponentType.TextInput,
|
||||
label: 'label',
|
||||
custom_id: 'custom id',
|
||||
@@ -114,7 +114,7 @@ describe('Text Input Components', () => {
|
||||
value: 'value',
|
||||
required: false,
|
||||
style: TextInputStyle.Paragraph,
|
||||
};
|
||||
} satisfies APITextInputComponent;
|
||||
|
||||
expect(new TextInputBuilder(textInputData).toJSON()).toEqual(textInputData);
|
||||
expect(
|
||||
|
||||
248
packages/builders/__tests__/components/v2/container.test.ts
Normal file
248
packages/builders/__tests__/components/v2/container.test.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { type APIContainerComponent, ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { ActionRowBuilder } from '../../../src/components/ActionRow.js';
|
||||
import { createComponentBuilder } from '../../../src/components/Components.js';
|
||||
import { ButtonBuilder } from '../../../src/components/button/Button.js';
|
||||
import { ContainerBuilder } from '../../../src/components/v2/Container.js';
|
||||
import { FileBuilder } from '../../../src/components/v2/File.js';
|
||||
import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery.js';
|
||||
import { SectionBuilder } from '../../../src/components/v2/Section.js';
|
||||
import { SeparatorBuilder } from '../../../src/components/v2/Separator.js';
|
||||
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay.js';
|
||||
|
||||
const containerWithTextDisplay: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
id: 123,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const containerWithSeparatorData: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Separator,
|
||||
id: 1_234,
|
||||
spacing: SeparatorSpacingSize.Small,
|
||||
divider: false,
|
||||
},
|
||||
],
|
||||
accent_color: 0x00ff00,
|
||||
};
|
||||
|
||||
const containerWithSeparatorDataNoColor: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Separator,
|
||||
id: 1_234,
|
||||
spacing: SeparatorSpacingSize.Small,
|
||||
divider: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Container Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid components THEN do not throw', () => {
|
||||
expect(() =>
|
||||
new ContainerBuilder().addActionRowComponents(
|
||||
new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder()),
|
||||
),
|
||||
).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addFileComponents(new FileBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addMediaGalleryComponents(new MediaGalleryBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addSectionComponents(new SectionBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addSeparatorComponents(new SeparatorBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addTextDisplayComponents(new TextDisplayBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().spliceComponents(0, 0, new SeparatorBuilder())).not.toThrowError();
|
||||
expect(() => new ContainerBuilder().addSeparatorComponents([new SeparatorBuilder()])).not.toThrowError();
|
||||
expect(() =>
|
||||
new ContainerBuilder().spliceComponents(0, 0, [{ type: ComponentType.Separator }]),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const containerData: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
id: 3,
|
||||
},
|
||||
{
|
||||
type: ComponentType.Separator,
|
||||
spacing: SeparatorSpacingSize.Large,
|
||||
divider: true,
|
||||
id: 4,
|
||||
},
|
||||
{
|
||||
type: ComponentType.File,
|
||||
file: {
|
||||
url: 'attachment://file.png',
|
||||
},
|
||||
spoiler: false,
|
||||
},
|
||||
],
|
||||
accent_color: 0xff00ff,
|
||||
spoiler: true,
|
||||
};
|
||||
|
||||
expect(new ContainerBuilder(containerData).toJSON()).toEqual(containerData);
|
||||
expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const containerWithTextDisplay: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
id: 123,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const containerWithSeparatorData: APIContainerComponent = {
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Separator,
|
||||
id: 1_234,
|
||||
spacing: SeparatorSpacingSize.Small,
|
||||
divider: false,
|
||||
},
|
||||
],
|
||||
accent_color: 0x00ff00,
|
||||
};
|
||||
|
||||
expect(new ContainerBuilder(containerWithTextDisplay).toJSON()).toEqual(containerWithTextDisplay);
|
||||
expect(new ContainerBuilder(containerWithSeparatorData).toJSON()).toEqual(containerWithSeparatorData);
|
||||
expect(() => createComponentBuilder({ type: ComponentType.Container, components: [] })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
||||
const textDisplay = new TextDisplayBuilder().setContent('test').setId(123);
|
||||
const separator = new SeparatorBuilder().setId(1_234).setSpacing(SeparatorSpacingSize.Small).setDivider(false);
|
||||
|
||||
expect(new ContainerBuilder().addTextDisplayComponents(textDisplay).toJSON()).toEqual(containerWithTextDisplay);
|
||||
expect(new ContainerBuilder().addSeparatorComponents(separator).toJSON()).toEqual(
|
||||
containerWithSeparatorDataNoColor,
|
||||
);
|
||||
expect(new ContainerBuilder().addTextDisplayComponents([textDisplay]).toJSON()).toEqual(containerWithTextDisplay);
|
||||
expect(new ContainerBuilder().addSeparatorComponents([separator]).toJSON()).toEqual(
|
||||
containerWithSeparatorDataNoColor,
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN valid accent color THEN valid JSON output is given', () => {
|
||||
expect(
|
||||
new ContainerBuilder({
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
})
|
||||
.setAccentColor([255, 0, 255])
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accent_color: 0xff00ff,
|
||||
});
|
||||
expect(
|
||||
new ContainerBuilder({
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
})
|
||||
.setAccentColor(0xff00ff)
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accent_color: 0xff00ff,
|
||||
});
|
||||
expect(
|
||||
new ContainerBuilder({
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
})
|
||||
.setAccentColor([255, 0, 255])
|
||||
.clearAccentColor()
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(new ContainerBuilder(containerWithSeparatorData).clearAccentColor().toJSON()).toEqual(
|
||||
containerWithSeparatorDataNoColor,
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN valid method parameters THEN valid JSON is given', () => {
|
||||
expect(
|
||||
new ContainerBuilder()
|
||||
.addTextDisplayComponents(new TextDisplayBuilder().setId(3).clearId().setContent('test'))
|
||||
.setSpoiler()
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
spoiler: true,
|
||||
});
|
||||
expect(
|
||||
new ContainerBuilder()
|
||||
.addTextDisplayComponents({ type: ComponentType.TextDisplay, content: 'test' })
|
||||
.setSpoiler(false)
|
||||
.setId(5)
|
||||
.toJSON(),
|
||||
).toEqual({
|
||||
type: ComponentType.Container,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
spoiler: false,
|
||||
id: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
44
packages/builders/__tests__/components/v2/file.test.ts
Normal file
44
packages/builders/__tests__/components/v2/file.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { FileBuilder } from '../../../src/components/v2/File';
|
||||
|
||||
const dummy = {
|
||||
type: ComponentType.File as const,
|
||||
file: { url: 'attachment://owo.png' },
|
||||
};
|
||||
|
||||
describe('File', () => {
|
||||
describe('File url', () => {
|
||||
test('GIVEN a file with a pre-defined url THEN return valid toJSON data', () => {
|
||||
const file = new FileBuilder({ file: { url: 'attachment://owo.png' } });
|
||||
expect(file.toJSON()).toEqual({ ...dummy, file: { url: 'attachment://owo.png' } });
|
||||
});
|
||||
|
||||
test('GIVEN a file using File#setURL THEN return valid toJSON data', () => {
|
||||
const file = new FileBuilder();
|
||||
file.setURL('attachment://uwu.png');
|
||||
|
||||
expect(file.toJSON()).toEqual({ ...dummy, file: { url: 'attachment://uwu.png' } });
|
||||
});
|
||||
|
||||
test('GIVEN a file with an invalid url THEN throws error', () => {
|
||||
const file = new FileBuilder();
|
||||
|
||||
expect(() => file.setURL('https://google.com')).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('File spoiler', () => {
|
||||
test('GIVEN a file with a pre-defined spoiler status THEN return valid toJSON data', () => {
|
||||
const file = new FileBuilder({ ...dummy, spoiler: true });
|
||||
expect(file.toJSON()).toEqual({ ...dummy, spoiler: true });
|
||||
});
|
||||
|
||||
test('GIVEN a file using File#setSpoiler THEN return valid toJSON data', () => {
|
||||
const file = new FileBuilder({ ...dummy });
|
||||
file.setSpoiler(false);
|
||||
|
||||
expect(file.toJSON()).toEqual({ ...dummy, spoiler: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
150
packages/builders/__tests__/components/v2/mediagallery.test.ts
Normal file
150
packages/builders/__tests__/components/v2/mediagallery.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { type APIMediaGalleryItem, type APIMediaGalleryComponent, ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { createComponentBuilder } from '../../../src/components/Components.js';
|
||||
import { MediaGalleryBuilder } from '../../../src/components/v2/MediaGallery.js';
|
||||
import { MediaGalleryItemBuilder } from '../../../src/components/v2/MediaGalleryItem.js';
|
||||
|
||||
const galleryHttpsDisplay: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
description: 'test',
|
||||
spoiler: false,
|
||||
media: { url: 'https://discord.com/logo.png' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const galleryAttachmentData: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
media: { url: 'attachment://file.png' },
|
||||
},
|
||||
],
|
||||
id: 123,
|
||||
};
|
||||
|
||||
describe('Media Gallery Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN an empty media gallery THEN throws error', () => {
|
||||
const gallery = new MediaGalleryBuilder();
|
||||
expect(() => gallery.toJSON()).toThrow();
|
||||
});
|
||||
|
||||
test('GIVEN valid items THEN do not throw', () => {
|
||||
expect(() => new MediaGalleryBuilder().addItems(new MediaGalleryItemBuilder())).not.toThrowError();
|
||||
expect(() => new MediaGalleryBuilder().spliceItems(0, 0, new MediaGalleryItemBuilder())).not.toThrowError();
|
||||
expect(() => new MediaGalleryBuilder().addItems([new MediaGalleryItemBuilder()])).not.toThrowError();
|
||||
expect(() => new MediaGalleryBuilder().spliceItems(0, 0, [new MediaGalleryItemBuilder()])).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const mediaGalleryData: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
media: { url: 'attachment://file.png' },
|
||||
description: 'test',
|
||||
spoiler: false,
|
||||
},
|
||||
{
|
||||
media: { url: 'https://discord.js.org/logo.jpg' },
|
||||
spoiler: true,
|
||||
},
|
||||
],
|
||||
id: 1_234,
|
||||
};
|
||||
|
||||
expect(new MediaGalleryBuilder(mediaGalleryData).toJSON()).toEqual(mediaGalleryData);
|
||||
expect(() => createComponentBuilder({ type: ComponentType.MediaGallery, items: [] })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const galleryHttpsDisplay: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
description: 'test',
|
||||
spoiler: false,
|
||||
media: { url: 'https://discord.com/logo.png' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const galleryAttachmentData: APIMediaGalleryComponent = {
|
||||
type: ComponentType.MediaGallery,
|
||||
items: [
|
||||
{
|
||||
media: { url: 'attachment://file.png' },
|
||||
},
|
||||
],
|
||||
id: 123,
|
||||
};
|
||||
|
||||
expect(new MediaGalleryBuilder(galleryHttpsDisplay).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder(galleryAttachmentData).toJSON()).toEqual(galleryAttachmentData);
|
||||
expect(() => createComponentBuilder({ type: ComponentType.MediaGallery, items: [] })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
||||
const item1 = new MediaGalleryItemBuilder()
|
||||
.setDescription('test')
|
||||
.setSpoiler(false)
|
||||
.setURL('https://discord.com/logo.png');
|
||||
const item2 = new MediaGalleryItemBuilder().setURL('attachment://file.png');
|
||||
|
||||
expect(new MediaGalleryBuilder().addItems(item1).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder().addItems(item2).setId(123).toJSON()).toEqual(galleryAttachmentData);
|
||||
expect(new MediaGalleryBuilder().addItems([item1]).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder().addItems([item2]).setId(123).toJSON()).toEqual(galleryAttachmentData);
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON options THEN valid JSON output is given 2', () => {
|
||||
const item1: APIMediaGalleryItem = {
|
||||
description: 'test',
|
||||
spoiler: false,
|
||||
media: { url: 'https://discord.com/logo.png' },
|
||||
};
|
||||
const item2 = {
|
||||
media: { url: 'attachment://file.png' },
|
||||
};
|
||||
|
||||
expect(new MediaGalleryBuilder().addItems(item1).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder().addItems(item2).setId(123).toJSON()).toEqual(galleryAttachmentData);
|
||||
expect(new MediaGalleryBuilder().addItems([item1]).toJSON()).toEqual(galleryHttpsDisplay);
|
||||
expect(new MediaGalleryBuilder().addItems([item2]).setId(123).toJSON()).toEqual(galleryAttachmentData);
|
||||
});
|
||||
|
||||
test('GIVEN valid builder callback THEN valid JSON output is given', () => {
|
||||
const item1 = new MediaGalleryItemBuilder()
|
||||
.setDescription('test')
|
||||
.setSpoiler(false)
|
||||
.setURL('https://discord.com/logo.png');
|
||||
const item2 = new MediaGalleryItemBuilder().setURL('attachment://file.png');
|
||||
|
||||
expect(
|
||||
new MediaGalleryBuilder()
|
||||
.addItems((item) => item.setDescription('test').setSpoiler(false).setURL('https://discord.com/logo.png'))
|
||||
.toJSON(),
|
||||
).toEqual(galleryHttpsDisplay);
|
||||
expect(
|
||||
new MediaGalleryBuilder()
|
||||
.spliceItems(0, 0, (item) => item.setURL('attachment://file.png'))
|
||||
.setId(123)
|
||||
.toJSON(),
|
||||
).toEqual(galleryAttachmentData);
|
||||
expect(
|
||||
new MediaGalleryBuilder()
|
||||
.addItems([(item) => item.setDescription('test').setSpoiler(false).setURL('https://discord.com/logo.png')])
|
||||
.toJSON(),
|
||||
).toEqual(galleryHttpsDisplay);
|
||||
expect(
|
||||
new MediaGalleryBuilder()
|
||||
.spliceItems(0, 0, [(item) => item.setDescription('test').clearDescription().setURL('attachment://file.png')])
|
||||
.setId(123)
|
||||
.toJSON(),
|
||||
).toEqual(galleryAttachmentData);
|
||||
});
|
||||
});
|
||||
});
|
||||
191
packages/builders/__tests__/components/v2/section.test.ts
Normal file
191
packages/builders/__tests__/components/v2/section.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { type APISectionComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { createComponentBuilder } from '../../../src/components/Components.js';
|
||||
import { ButtonBuilder } from '../../../src/components/button/Button.js';
|
||||
import { SectionBuilder } from '../../../src/components/v2/Section.js';
|
||||
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay.js';
|
||||
import { ThumbnailBuilder } from '../../../src/components/v2/Thumbnail.js';
|
||||
|
||||
const sectionWithButtonData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
custom_id: '123',
|
||||
style: ButtonStyle.Primary,
|
||||
},
|
||||
};
|
||||
|
||||
const sectionWithThumbnailData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Thumbnail,
|
||||
media: { url: 'attachment://file.png' },
|
||||
spoiler: true,
|
||||
description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Section Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid components THEN do not throw', () => {
|
||||
expect(() => new SectionBuilder().addTextDisplayComponents(new TextDisplayBuilder())).not.toThrowError();
|
||||
expect(() => new SectionBuilder().spliceTextDisplayComponents(0, 0, new TextDisplayBuilder())).not.toThrowError();
|
||||
expect(() => new SectionBuilder().addTextDisplayComponents([new TextDisplayBuilder()])).not.toThrowError();
|
||||
expect(() =>
|
||||
new SectionBuilder().spliceTextDisplayComponents(0, 0, [new TextDisplayBuilder()]),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const sectionData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
id: 123,
|
||||
},
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Thumbnail,
|
||||
media: { url: 'attachment://file.png' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(new SectionBuilder(sectionData).toJSON()).toEqual(sectionData);
|
||||
expect(() =>
|
||||
createComponentBuilder({
|
||||
type: ComponentType.Section,
|
||||
components: [],
|
||||
accessory: { type: ComponentType.Thumbnail, media: { url: 'https://discord.com/logo.png' } },
|
||||
}),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const sectionWithButtonData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
custom_id: '123',
|
||||
style: ButtonStyle.Primary,
|
||||
},
|
||||
};
|
||||
|
||||
const sectionWithThumbnailData: APISectionComponent = {
|
||||
type: ComponentType.Section,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextDisplay,
|
||||
content: 'test',
|
||||
},
|
||||
],
|
||||
accessory: {
|
||||
type: ComponentType.Thumbnail,
|
||||
media: { url: 'attachment://file.png' },
|
||||
spoiler: true,
|
||||
description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
expect(new SectionBuilder(sectionWithButtonData).toJSON()).toEqual(sectionWithButtonData);
|
||||
expect(new SectionBuilder(sectionWithThumbnailData).toJSON()).toEqual(sectionWithThumbnailData);
|
||||
expect(() =>
|
||||
createComponentBuilder({
|
||||
type: ComponentType.Section,
|
||||
components: [],
|
||||
accessory: {
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
custom_id: '123',
|
||||
style: ButtonStyle.Primary,
|
||||
},
|
||||
}),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
||||
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||
const thumbnail = new ThumbnailBuilder().setDescription('test').setSpoiler().setURL('attachment://file.png');
|
||||
const textDisplay = new TextDisplayBuilder().setContent('test');
|
||||
|
||||
expect(new SectionBuilder().addTextDisplayComponents(textDisplay).setButtonAccessory(button).toJSON()).toEqual(
|
||||
sectionWithButtonData,
|
||||
);
|
||||
expect(
|
||||
new SectionBuilder().addTextDisplayComponents(textDisplay).setThumbnailAccessory(thumbnail).toJSON(),
|
||||
).toEqual(sectionWithThumbnailData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.addTextDisplayComponents([textDisplay])
|
||||
.setButtonAccessory((button) => button.setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'))
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.addTextDisplayComponents([textDisplay])
|
||||
.setThumbnailAccessory((thumbnail) =>
|
||||
thumbnail.setDescription('test').setSpoiler().setURL('attachment://file.png'),
|
||||
)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithThumbnailData);
|
||||
});
|
||||
|
||||
test('GIVEN valid builder callback THEN valid JSON output is given', () => {
|
||||
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.addTextDisplayComponents((textDisplay) => textDisplay.setContent('test'))
|
||||
.setButtonAccessory(button)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.spliceTextDisplayComponents(0, 0, (textDisplay) => textDisplay.setContent('test'))
|
||||
.setButtonAccessory(button)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.addTextDisplayComponents([(textDisplay) => textDisplay.setContent('test')])
|
||||
.setButtonAccessory(button)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
expect(
|
||||
new SectionBuilder()
|
||||
.spliceTextDisplayComponents(0, 0, [(textDisplay) => textDisplay.setContent('test')])
|
||||
.setButtonAccessory(button)
|
||||
.toJSON(),
|
||||
).toEqual(sectionWithButtonData);
|
||||
});
|
||||
});
|
||||
});
|
||||
35
packages/builders/__tests__/components/v2/separator.test.ts
Normal file
35
packages/builders/__tests__/components/v2/separator.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { SeparatorBuilder } from '../../../src/components/v2/Separator';
|
||||
|
||||
describe('Separator', () => {
|
||||
describe('Divider', () => {
|
||||
test('GIVEN a separator with a pre-defined divider THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder({ divider: true });
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, divider: true });
|
||||
});
|
||||
|
||||
test('GIVEN a separator with a set divider THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder().setDivider(false);
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, divider: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Spacing', () => {
|
||||
test('GIVEN a separator with a pre-defined spacing THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder({ spacing: SeparatorSpacingSize.Small });
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, spacing: SeparatorSpacingSize.Small });
|
||||
});
|
||||
|
||||
test('GIVEN a separator with a set spacing THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Large);
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator, spacing: SeparatorSpacingSize.Large });
|
||||
});
|
||||
|
||||
test('GIVEN a separator with a set spacing THEN clear spacing THEN return valid toJSON data', () => {
|
||||
const separator = new SeparatorBuilder({ spacing: SeparatorSpacingSize.Small });
|
||||
separator.clearSpacing();
|
||||
expect(separator.toJSON()).toEqual({ type: ComponentType.Separator });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { TextDisplayBuilder } from '../../../src/components/v2/TextDisplay';
|
||||
|
||||
describe('TextDisplay', () => {
|
||||
describe('TextDisplay content', () => {
|
||||
test('GIVEN a text display with a pre-defined content THEN return valid toJSON data', () => {
|
||||
const textDisplay = new TextDisplayBuilder({ content: 'foo' });
|
||||
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a text display with a set content THEN return valid toJSON data', () => {
|
||||
const textDisplay = new TextDisplayBuilder().setContent('foo');
|
||||
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a text display with a pre-defined content THEN overwritten content THEN return valid toJSON data', () => {
|
||||
const textDisplay = new TextDisplayBuilder({ content: 'foo' });
|
||||
textDisplay.setContent('bar');
|
||||
expect(textDisplay.toJSON()).toEqual({ type: ComponentType.TextDisplay, content: 'bar' });
|
||||
});
|
||||
});
|
||||
});
|
||||
69
packages/builders/__tests__/components/v2/thumbnail.test.ts
Normal file
69
packages/builders/__tests__/components/v2/thumbnail.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { ThumbnailBuilder } from '../../../src/components/v2/Thumbnail';
|
||||
|
||||
const dummy = {
|
||||
type: ComponentType.Thumbnail as const,
|
||||
media: { url: 'https://google.com' },
|
||||
};
|
||||
|
||||
describe('Thumbnail', () => {
|
||||
describe('Thumbnail url', () => {
|
||||
test('GIVEN a thumbnail with a pre-defined url THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ media: { url: 'https://google.com' } });
|
||||
expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with a set url THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder().setURL('https://google.com');
|
||||
expect(thumbnail.toJSON()).toEqual({ type: ComponentType.Thumbnail, media: { url: 'https://google.com' } });
|
||||
});
|
||||
|
||||
test.each(['owo', 'discord://user'])('GIVEN a thumbnail with an invalid URL (%s) THEN throws error', (input) => {
|
||||
const thumbnail = new ThumbnailBuilder();
|
||||
|
||||
expect(() => thumbnail.setURL(input)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Thumbnail description', () => {
|
||||
test('GIVEN a thumbnail with a pre-defined description THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ ...dummy, description: 'foo' });
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy, description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with a set description THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ ...dummy });
|
||||
thumbnail.setDescription('foo');
|
||||
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy, description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ description: 'foo', ...dummy });
|
||||
thumbnail.clearDescription();
|
||||
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with an invalid description THEN throws error', () => {
|
||||
const thumbnail = new ThumbnailBuilder();
|
||||
|
||||
expect(() => thumbnail.setDescription('a'.repeat(1_025))).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Thumbnail spoiler', () => {
|
||||
test('GIVEN a thumbnail with a pre-defined spoiler status THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ ...dummy, spoiler: true });
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy, spoiler: true });
|
||||
});
|
||||
|
||||
test('GIVEN a thumbnail with a set spoiler status THEN return valid toJSON data', () => {
|
||||
const thumbnail = new ThumbnailBuilder({ ...dummy });
|
||||
thumbnail.setSpoiler(false);
|
||||
|
||||
expect(thumbnail.toJSON()).toEqual({ ...dummy, spoiler: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,8 @@ describe('Context Menu Commands', () => {
|
||||
// Too short of a name
|
||||
expect(() => ContextMenuCommandAssertions.validateName('')).toThrowError();
|
||||
|
||||
// Invalid characters used
|
||||
expect(() => ContextMenuCommandAssertions.validateName('ABC123$%^&')).toThrowError();
|
||||
// This should be fine, even with trailing and leading spaces (API trims it).
|
||||
expect(() => ContextMenuCommandAssertions.validateName(' 🩵 ABC 123 $%^& ')).not.toThrowError();
|
||||
|
||||
// Too long of a name
|
||||
expect(() =>
|
||||
@@ -60,8 +60,6 @@ describe('Context Menu Commands', () => {
|
||||
});
|
||||
|
||||
test('GIVEN invalid name THEN throw error', () => {
|
||||
expect(() => getBuilder().setName('$$$')).toThrowError();
|
||||
|
||||
expect(() => getBuilder().setName(' ')).toThrowError();
|
||||
});
|
||||
|
||||
@@ -166,7 +164,7 @@ describe('Context Menu Commands', () => {
|
||||
});
|
||||
|
||||
describe('integration types', () => {
|
||||
test('GIVEN a builder with valid integration types THEN does not throw an error', () => {
|
||||
test('GIVEN a builder with valid integraton types THEN does not throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().setIntegrationTypes([
|
||||
ApplicationIntegrationType.GuildInstall,
|
||||
|
||||
@@ -565,7 +565,7 @@ describe('Slash Commands', () => {
|
||||
});
|
||||
|
||||
describe('integration types', () => {
|
||||
test('GIVEN a builder with valid integration types THEN does not throw an error', () => {
|
||||
test('GIVEN a builder with valid integraton types THEN does not throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().setIntegrationTypes([
|
||||
ApplicationIntegrationType.GuildInstall,
|
||||
|
||||
@@ -324,12 +324,16 @@ describe('Embed', () => {
|
||||
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields({ name: 'foo', value: 'bar' });
|
||||
embed.addFields([{ name: 'foo', value: 'bar' }]);
|
||||
embed.addFields([
|
||||
{ name: 'foo', value: 'bar' },
|
||||
{ name: '', value: '' },
|
||||
]);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
fields: [
|
||||
{ name: 'foo', value: 'bar' },
|
||||
{ name: 'foo', value: 'bar' },
|
||||
{ name: '', value: '' },
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -381,38 +385,24 @@ describe('Embed', () => {
|
||||
expect(() => embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError();
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field amount THEN throws error', () => {
|
||||
test('1', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
test('GIVEN invalid field amount THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() =>
|
||||
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
).toThrowError();
|
||||
});
|
||||
expect(() =>
|
||||
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field name THEN throws error', () => {
|
||||
test('2', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
test('GIVEN invalid field name length THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
|
||||
});
|
||||
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field name length THEN throws error', () => {
|
||||
test('3', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
test('GIVEN invalid field value length THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field value length THEN throws error', () => {
|
||||
test('4', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1_025) })).toThrowError();
|
||||
});
|
||||
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1_025) })).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@discordjs/builders",
|
||||
"version": "1.9.0",
|
||||
"version": "1.13.0",
|
||||
"description": "A set of builders that you can use when creating your bot",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
@@ -68,7 +68,7 @@
|
||||
"@discordjs/formatters": "workspace:^",
|
||||
"@discordjs/util": "workspace:^",
|
||||
"@sapphire/shapeshift": "^4.0.0",
|
||||
"discord-api-types": "^0.37.119",
|
||||
"discord-api-types": "^0.38.32",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.6.3"
|
||||
@@ -91,7 +91,7 @@
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import {
|
||||
type APIActionRowComponent,
|
||||
ComponentType,
|
||||
type APIMessageActionRowComponent,
|
||||
type APIModalActionRowComponent,
|
||||
type APIActionRowComponentTypes,
|
||||
type APIComponentInMessageActionRow,
|
||||
type APIComponentInModalActionRow,
|
||||
type APIComponentInActionRow,
|
||||
} from 'discord-api-types/v10';
|
||||
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
||||
import { ComponentBuilder } from './Component.js';
|
||||
@@ -18,13 +18,6 @@ import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
||||
import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||
import type { TextInputBuilder } from './textInput/TextInput.js';
|
||||
|
||||
/**
|
||||
* The builders that may be used for messages.
|
||||
*/
|
||||
export type MessageComponentBuilder =
|
||||
| ActionRowBuilder<MessageActionRowComponentBuilder>
|
||||
| MessageActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* The builders that may be used for modals.
|
||||
*/
|
||||
@@ -57,7 +50,7 @@ export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalAction
|
||||
* @typeParam ComponentType - The types of components this action row holds
|
||||
*/
|
||||
export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends ComponentBuilder<
|
||||
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
||||
APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>
|
||||
> {
|
||||
/**
|
||||
* The components within this action row.
|
||||
@@ -98,7 +91,7 @@ export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends
|
||||
* .addComponents(button2, button3);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIComponentInActionRow>> = {}) {
|
||||
super({ type: ComponentType.ActionRow, ...data });
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentType[];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord
|
||||
import { isValidationEnabled } from '../util/validation.js';
|
||||
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';
|
||||
|
||||
export const idValidator = s
|
||||
.number()
|
||||
.safeInt()
|
||||
.greaterThanOrEqual(1)
|
||||
.lessThan(4_294_967_296) // 2^32 - 1
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const customIdValidator = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
APIComponentInActionRow,
|
||||
APIBaseComponent,
|
||||
ComponentType,
|
||||
APIMessageComponent,
|
||||
APIModalComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { idValidator } from './Assertions';
|
||||
|
||||
/**
|
||||
* Any action row component data represented as an object.
|
||||
*/
|
||||
export type AnyAPIActionRowComponent = APIActionRowComponent<APIActionRowComponentTypes> | APIActionRowComponentTypes;
|
||||
export type AnyAPIActionRowComponent =
|
||||
| APIActionRowComponent<APIComponentInActionRow>
|
||||
| APIComponentInActionRow
|
||||
| APIMessageComponent
|
||||
| APIModalComponent;
|
||||
|
||||
/**
|
||||
* The base component builder that contains common symbols for all sorts of components.
|
||||
@@ -42,4 +49,22 @@ export abstract class ComponentBuilder<
|
||||
public constructor(data: Partial<DataType>) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id (not the custom id) for this component.
|
||||
*
|
||||
* @param id - The id for this component
|
||||
*/
|
||||
public setId(id: number) {
|
||||
this.data.id = idValidator.parse(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the id of this component, defaulting to a default incremented id.
|
||||
*/
|
||||
public clearId() {
|
||||
this.data.id = undefined;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,42 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10';
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
type MessageActionRowComponentBuilder,
|
||||
type AnyComponentBuilder,
|
||||
type MessageComponentBuilder,
|
||||
type ModalComponentBuilder,
|
||||
} from './ActionRow.js';
|
||||
import { ComponentBuilder } from './Component.js';
|
||||
import { ButtonBuilder } from './button/Button.js';
|
||||
import { FileUploadBuilder } from './fileUpload/FileUpload.js';
|
||||
import { LabelBuilder } from './label/Label.js';
|
||||
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
||||
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
||||
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
||||
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
||||
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||
import { TextInputBuilder } from './textInput/TextInput.js';
|
||||
import { ContainerBuilder } from './v2/Container.js';
|
||||
import { FileBuilder } from './v2/File.js';
|
||||
import { MediaGalleryBuilder } from './v2/MediaGallery.js';
|
||||
import { SectionBuilder } from './v2/Section.js';
|
||||
import { SeparatorBuilder } from './v2/Separator.js';
|
||||
import { TextDisplayBuilder } from './v2/TextDisplay.js';
|
||||
import { ThumbnailBuilder } from './v2/Thumbnail.js';
|
||||
|
||||
/**
|
||||
* The builders that may be used for messages.
|
||||
*/
|
||||
export type MessageComponentBuilder =
|
||||
| ActionRowBuilder<MessageActionRowComponentBuilder>
|
||||
| ContainerBuilder
|
||||
| FileBuilder
|
||||
| MediaGalleryBuilder
|
||||
| MessageActionRowComponentBuilder
|
||||
| SectionBuilder
|
||||
| SeparatorBuilder
|
||||
| TextDisplayBuilder
|
||||
| ThumbnailBuilder;
|
||||
|
||||
/**
|
||||
* Components here are mapped to their respective builder.
|
||||
@@ -50,6 +74,42 @@ export interface MappedComponentTypes {
|
||||
* The channel select component type is associated with a {@link ChannelSelectMenuBuilder}.
|
||||
*/
|
||||
[ComponentType.ChannelSelect]: ChannelSelectMenuBuilder;
|
||||
/**
|
||||
* The file component type is associated with a {@link FileBuilder}.
|
||||
*/
|
||||
[ComponentType.File]: FileBuilder;
|
||||
/**
|
||||
* The separator component type is associated with a {@link SeparatorBuilder}.
|
||||
*/
|
||||
[ComponentType.Separator]: SeparatorBuilder;
|
||||
/**
|
||||
* The container component type is associated with a {@link ContainerBuilder}.
|
||||
*/
|
||||
[ComponentType.Container]: ContainerBuilder;
|
||||
/**
|
||||
* The text display component type is associated with a {@link TextDisplayBuilder}.
|
||||
*/
|
||||
[ComponentType.TextDisplay]: TextDisplayBuilder;
|
||||
/**
|
||||
* The thumbnail component type is associated with a {@link ThumbnailBuilder}.
|
||||
*/
|
||||
[ComponentType.Thumbnail]: ThumbnailBuilder;
|
||||
/**
|
||||
* The section component type is associated with a {@link SectionBuilder}.
|
||||
*/
|
||||
[ComponentType.Section]: SectionBuilder;
|
||||
/**
|
||||
* The media gallery component type is associated with a {@link MediaGalleryBuilder}.
|
||||
*/
|
||||
[ComponentType.MediaGallery]: MediaGalleryBuilder;
|
||||
/**
|
||||
* The label component type is associated with a {@link LabelBuilder}.
|
||||
*/
|
||||
[ComponentType.Label]: LabelBuilder;
|
||||
/**
|
||||
* The file upload component type is associated with a {@link FileUploadBuilder}.
|
||||
*/
|
||||
[ComponentType.FileUpload]: FileUploadBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,8 +157,48 @@ export function createComponentBuilder(
|
||||
return new MentionableSelectMenuBuilder(data);
|
||||
case ComponentType.ChannelSelect:
|
||||
return new ChannelSelectMenuBuilder(data);
|
||||
case ComponentType.File:
|
||||
return new FileBuilder(data);
|
||||
case ComponentType.Container:
|
||||
return new ContainerBuilder(data);
|
||||
case ComponentType.Section:
|
||||
return new SectionBuilder(data);
|
||||
case ComponentType.Separator:
|
||||
return new SeparatorBuilder(data);
|
||||
case ComponentType.TextDisplay:
|
||||
return new TextDisplayBuilder(data);
|
||||
case ComponentType.Thumbnail:
|
||||
return new ThumbnailBuilder(data);
|
||||
case ComponentType.MediaGallery:
|
||||
return new MediaGalleryBuilder(data);
|
||||
case ComponentType.Label:
|
||||
return new LabelBuilder(data);
|
||||
case ComponentType.FileUpload:
|
||||
return new FileUploadBuilder(data);
|
||||
default:
|
||||
// @ts-expect-error This case can still occur if we get a newer unsupported component type
|
||||
throw new Error(`Cannot properly serialize component type: ${data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isBuilder<Builder extends JSONEncodable<any>>(
|
||||
builder: unknown,
|
||||
Constructor: new () => Builder,
|
||||
): builder is Builder {
|
||||
return builder instanceof Constructor;
|
||||
}
|
||||
|
||||
export function resolveBuilder<ComponentType extends Record<PropertyKey, any>, Builder extends JSONEncodable<any>>(
|
||||
builder: Builder | ComponentType | ((builder: Builder) => Builder),
|
||||
Constructor: new (data?: ComponentType) => Builder,
|
||||
) {
|
||||
if (isBuilder(builder, Constructor)) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
if (typeof builder === 'function') {
|
||||
return builder(new Constructor());
|
||||
}
|
||||
|
||||
return new Constructor(builder);
|
||||
}
|
||||
|
||||
12
packages/builders/src/components/fileUpload/Assertions.ts
Normal file
12
packages/builders/src/components/fileUpload/Assertions.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { customIdValidator, idValidator } from '../Assertions.js';
|
||||
|
||||
export const fileUploadPredicate = s.object({
|
||||
type: s.literal(ComponentType.FileUpload),
|
||||
id: idValidator.optional(),
|
||||
custom_id: customIdValidator,
|
||||
min_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(10).optional(),
|
||||
max_values: s.number().greaterThanOrEqual(1).lessThanOrEqual(10).optional(),
|
||||
required: s.boolean().optional(),
|
||||
});
|
||||
99
packages/builders/src/components/fileUpload/FileUpload.ts
Normal file
99
packages/builders/src/components/fileUpload/FileUpload.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { type APIFileUploadComponent, ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { fileUploadPredicate } from './Assertions.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for file uploads.
|
||||
*/
|
||||
export class FileUploadBuilder extends ComponentBuilder<APIFileUploadComponent> {
|
||||
/**
|
||||
* Creates a new file upload.
|
||||
*
|
||||
* @param data - The API data to create this file upload with
|
||||
* @example
|
||||
* Creating a file upload from an API data object:
|
||||
* ```ts
|
||||
* const fileUpload = new FileUploadBuilder({
|
||||
* custom_id: "file_upload",
|
||||
* min_values: 2,
|
||||
* max_values: 5,
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a file upload using setters and API data:
|
||||
* ```ts
|
||||
* const fileUpload = new FileUploadBuilder({
|
||||
* custom_id: "file_upload",
|
||||
* min_values: 2,
|
||||
* max_values: 5,
|
||||
* }).setRequired();
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APIFileUploadComponent> = {}) {
|
||||
super({ type: ComponentType.FileUpload, ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id for this file upload.
|
||||
*
|
||||
* @param customId - The custom id to use
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum number of file uploads required.
|
||||
*
|
||||
* @param minValues - The minimum values that must be uploaded
|
||||
*/
|
||||
public setMinValues(minValues: number) {
|
||||
this.data.min_values = minValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the minimum values.
|
||||
*/
|
||||
public clearMinValues() {
|
||||
this.data.min_values = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of file uploads required.
|
||||
*
|
||||
* @param maxValues - The maximum values that can be uploaded
|
||||
*/
|
||||
public setMaxValues(maxValues: number) {
|
||||
this.data.max_values = maxValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the maximum values.
|
||||
*/
|
||||
public clearMaxValues() {
|
||||
this.data.max_values = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this file upload is required.
|
||||
*
|
||||
* @param required - Whether this file upload is required
|
||||
*/
|
||||
public setRequired(required = true) {
|
||||
this.data.required = required;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APIFileUploadComponent {
|
||||
fileUploadPredicate.parse(this.data);
|
||||
return this.data as APIFileUploadComponent;
|
||||
}
|
||||
}
|
||||
31
packages/builders/src/components/label/Assertions.ts
Normal file
31
packages/builders/src/components/label/Assertions.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { idValidator } from '../Assertions.js';
|
||||
import { fileUploadPredicate } from '../fileUpload/Assertions.js';
|
||||
import {
|
||||
selectMenuChannelPredicate,
|
||||
selectMenuMentionablePredicate,
|
||||
selectMenuRolePredicate,
|
||||
selectMenuStringPredicate,
|
||||
selectMenuUserPredicate,
|
||||
} from '../selectMenu/Assertions.js';
|
||||
import { textInputPredicate } from '../textInput/Assertions.js';
|
||||
|
||||
export const labelPredicate = s
|
||||
.object({
|
||||
id: idValidator.optional(),
|
||||
type: s.literal(ComponentType.Label),
|
||||
label: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(45),
|
||||
description: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100).optional(),
|
||||
component: s.union([
|
||||
textInputPredicate,
|
||||
selectMenuUserPredicate,
|
||||
selectMenuRolePredicate,
|
||||
selectMenuMentionablePredicate,
|
||||
selectMenuChannelPredicate,
|
||||
selectMenuStringPredicate,
|
||||
fileUploadPredicate,
|
||||
]),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
213
packages/builders/src/components/label/Label.ts
Normal file
213
packages/builders/src/components/label/Label.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import type {
|
||||
APIChannelSelectComponent,
|
||||
APIFileUploadComponent,
|
||||
APILabelComponent,
|
||||
APIMentionableSelectComponent,
|
||||
APIRoleSelectComponent,
|
||||
APIStringSelectComponent,
|
||||
APITextInputComponent,
|
||||
APIUserSelectComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { createComponentBuilder, resolveBuilder } from '../Components.js';
|
||||
import { FileUploadBuilder } from '../fileUpload/FileUpload.js';
|
||||
import { ChannelSelectMenuBuilder } from '../selectMenu/ChannelSelectMenu.js';
|
||||
import { MentionableSelectMenuBuilder } from '../selectMenu/MentionableSelectMenu.js';
|
||||
import { RoleSelectMenuBuilder } from '../selectMenu/RoleSelectMenu.js';
|
||||
import { StringSelectMenuBuilder } from '../selectMenu/StringSelectMenu.js';
|
||||
import { UserSelectMenuBuilder } from '../selectMenu/UserSelectMenu.js';
|
||||
import { TextInputBuilder } from '../textInput/TextInput.js';
|
||||
import { labelPredicate } from './Assertions.js';
|
||||
|
||||
export interface LabelBuilderData extends Partial<Omit<APILabelComponent, 'component'>> {
|
||||
component?:
|
||||
| ChannelSelectMenuBuilder
|
||||
| FileUploadBuilder
|
||||
| MentionableSelectMenuBuilder
|
||||
| RoleSelectMenuBuilder
|
||||
| StringSelectMenuBuilder
|
||||
| TextInputBuilder
|
||||
| UserSelectMenuBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for labels.
|
||||
*/
|
||||
export class LabelBuilder extends ComponentBuilder<LabelBuilderData> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public override readonly data: LabelBuilderData;
|
||||
|
||||
/**
|
||||
* Creates a new label.
|
||||
*
|
||||
* @param data - The API data to create this label with
|
||||
* @example
|
||||
* Creating a label from an API data object:
|
||||
* ```ts
|
||||
* const label = new LabelBuilder({
|
||||
* label: "label",
|
||||
* component,
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a label using setters and API data:
|
||||
* ```ts
|
||||
* const label = new LabelBuilder({
|
||||
* label: 'label',
|
||||
* component,
|
||||
* }).setLabel('new text');
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APILabelComponent> = {}) {
|
||||
super({ type: ComponentType.Label });
|
||||
|
||||
const { component, ...rest } = data;
|
||||
|
||||
this.data = {
|
||||
...rest,
|
||||
component: component ? createComponentBuilder(component) : undefined,
|
||||
type: ComponentType.Label,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this label.
|
||||
*
|
||||
* @param label - The label to use
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description for this label.
|
||||
*
|
||||
* @param description - The description to use
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the description for this label.
|
||||
*/
|
||||
public clearDescription() {
|
||||
this.data.description = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a string select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setStringSelectMenuComponent(
|
||||
input:
|
||||
| APIStringSelectComponent
|
||||
| StringSelectMenuBuilder
|
||||
| ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, StringSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a user select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setUserSelectMenuComponent(
|
||||
input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, UserSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a role select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setRoleSelectMenuComponent(
|
||||
input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, RoleSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a mentionable select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setMentionableSelectMenuComponent(
|
||||
input:
|
||||
| APIMentionableSelectComponent
|
||||
| MentionableSelectMenuBuilder
|
||||
| ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, MentionableSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a channel select menu component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setChannelSelectMenuComponent(
|
||||
input:
|
||||
| APIChannelSelectComponent
|
||||
| ChannelSelectMenuBuilder
|
||||
| ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, ChannelSelectMenuBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a text input component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setTextInputComponent(
|
||||
input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, TextInputBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a file upload component to this label.
|
||||
*
|
||||
* @param input - A function that returns a component builder or an already built builder
|
||||
*/
|
||||
public setFileUploadComponent(
|
||||
input: APIFileUploadComponent | FileUploadBuilder | ((builder: FileUploadBuilder) => FileUploadBuilder),
|
||||
): this {
|
||||
this.data.component = resolveBuilder(input, FileUploadBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APILabelComponent {
|
||||
const { component, ...rest } = this.data;
|
||||
|
||||
const data = {
|
||||
...rest,
|
||||
// The label predicate validates the component.
|
||||
component: component?.toJSON(),
|
||||
};
|
||||
|
||||
labelPredicate.parse(data);
|
||||
|
||||
return data as APILabelComponent;
|
||||
}
|
||||
}
|
||||
92
packages/builders/src/components/selectMenu/Assertions.ts
Normal file
92
packages/builders/src/components/selectMenu/Assertions.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Result, s } from '@sapphire/shapeshift';
|
||||
import { ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { customIdValidator, emojiValidator, idValidator } from '../Assertions.js';
|
||||
import { labelValidator } from '../textInput/Assertions.js';
|
||||
|
||||
const selectMenuBasePredicate = s.object({
|
||||
id: idValidator.optional(),
|
||||
placeholder: s.string().lengthLessThanOrEqual(150).optional(),
|
||||
min_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(25).optional(),
|
||||
max_values: s.number().greaterThanOrEqual(0).lessThanOrEqual(25).optional(),
|
||||
custom_id: customIdValidator,
|
||||
disabled: s.boolean().optional(),
|
||||
});
|
||||
|
||||
export const selectMenuChannelPredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.ChannelSelect),
|
||||
channel_types: s.nativeEnum(ChannelType).array().optional(),
|
||||
default_values: s
|
||||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.Channel) })
|
||||
.array()
|
||||
.lengthLessThanOrEqual(25)
|
||||
.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuMentionablePredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.MentionableSelect),
|
||||
default_values: s
|
||||
.object({
|
||||
id: s.string(),
|
||||
type: s.union([s.literal(SelectMenuDefaultValueType.Role), s.literal(SelectMenuDefaultValueType.User)]),
|
||||
})
|
||||
.array()
|
||||
.lengthLessThanOrEqual(25)
|
||||
.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuRolePredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.RoleSelect),
|
||||
default_values: s
|
||||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.Role) })
|
||||
.array()
|
||||
.lengthLessThanOrEqual(25)
|
||||
.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuUserPredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.UserSelect),
|
||||
default_values: s
|
||||
.object({ id: s.string(), type: s.literal(SelectMenuDefaultValueType.User) })
|
||||
.array()
|
||||
.lengthLessThanOrEqual(25)
|
||||
.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuStringOptionPredicate = s
|
||||
.object({
|
||||
label: labelValidator,
|
||||
value: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100),
|
||||
description: s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100).optional(),
|
||||
emoji: emojiValidator.optional(),
|
||||
default: s.boolean().optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const selectMenuStringPredicate = selectMenuBasePredicate
|
||||
.extend({
|
||||
type: s.literal(ComponentType.StringSelect),
|
||||
options: selectMenuStringOptionPredicate.array().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(25),
|
||||
})
|
||||
.reshape((value) => {
|
||||
if (value.min_values !== undefined && value.options.length < value.min_values) {
|
||||
return Result.err(new RangeError(`The number of options must be greater than or equal to min_values`));
|
||||
}
|
||||
|
||||
if (value.min_values !== undefined && value.max_values !== undefined && value.min_values > value.max_values) {
|
||||
return Result.err(
|
||||
new RangeError(`The maximum amount of options must be greater than or equal to the minimum amount of options`),
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(value);
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { APISelectMenuComponent } from 'discord-api-types/v10';
|
||||
import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { requiredValidator } from '../textInput/Assertions.js';
|
||||
|
||||
/**
|
||||
* The base select menu builder that contains common symbols for select menu builders.
|
||||
@@ -31,9 +32,9 @@ export abstract class BaseSelectMenuBuilder<
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum values that must be selected in the select menu.
|
||||
* Sets the maximum values that can be selected in the select menu.
|
||||
*
|
||||
* @param maxValues - The maximum values that must be selected
|
||||
* @param maxValues - The maximum values that can be selected
|
||||
*/
|
||||
public setMaxValues(maxValues: number) {
|
||||
this.data.max_values = minMaxValidator.parse(maxValues);
|
||||
@@ -60,6 +61,17 @@ export abstract class BaseSelectMenuBuilder<
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this select menu is required.
|
||||
*
|
||||
* @remarks Only for use in modals.
|
||||
* @param required - Whether this select menu is required
|
||||
*/
|
||||
public setRequired(required = true) {
|
||||
this.data.required = requiredValidator.parse(required);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
|
||||
@@ -83,7 +83,7 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/slice | Array.prototype.splice()}.
|
||||
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice | Array.prototype.splice()}.
|
||||
* It's useful for modifying and adjusting the order of existing options.
|
||||
* @example
|
||||
* Remove the first option:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { TextInputStyle } from 'discord-api-types/v10';
|
||||
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
import { customIdValidator } from '../Assertions.js';
|
||||
import { customIdValidator, idValidator } from '../Assertions.js';
|
||||
|
||||
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
|
||||
export const textInputStyleValidator = s.nativeEnum(TextInputStyle).setValidationEnabled(isValidationEnabled);
|
||||
export const minLengthValidator = s
|
||||
.number()
|
||||
.int()
|
||||
@@ -16,7 +16,7 @@ export const maxLengthValidator = s
|
||||
.greaterThanOrEqual(1)
|
||||
.lessThanOrEqual(4_000)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const requiredValidator = s.boolean();
|
||||
export const requiredValidator = s.boolean().setValidationEnabled(isValidationEnabled);
|
||||
export const valueValidator = s.string().lengthLessThanOrEqual(4_000).setValidationEnabled(isValidationEnabled);
|
||||
export const placeholderValidator = s.string().lengthLessThanOrEqual(100).setValidationEnabled(isValidationEnabled);
|
||||
export const labelValidator = s
|
||||
@@ -25,8 +25,21 @@ export const labelValidator = s
|
||||
.lengthLessThanOrEqual(45)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
|
||||
export const textInputPredicate = s
|
||||
.object({
|
||||
type: s.literal(ComponentType.TextInput),
|
||||
custom_id: customIdValidator,
|
||||
style: textInputStyleValidator,
|
||||
id: idValidator.optional(),
|
||||
min_length: minLengthValidator.optional(),
|
||||
max_length: maxLengthValidator.optional(),
|
||||
placeholder: placeholderValidator.optional(),
|
||||
value: valueValidator.optional(),
|
||||
required: requiredValidator.optional(),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export function validateRequiredParameters(customId?: string, style?: TextInputStyle) {
|
||||
customIdValidator.parse(customId);
|
||||
textInputStyleValidator.parse(style);
|
||||
labelValidator.parse(label);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class TextInputBuilder
|
||||
* ```ts
|
||||
* const textInput = new TextInputBuilder({
|
||||
* custom_id: 'a cool text input',
|
||||
* label: 'Type something',
|
||||
* placeholder: 'Type something',
|
||||
* style: TextInputStyle.Short,
|
||||
* });
|
||||
* ```
|
||||
@@ -38,7 +38,7 @@ export class TextInputBuilder
|
||||
* Creating a text input using setters and API data:
|
||||
* ```ts
|
||||
* const textInput = new TextInputBuilder({
|
||||
* label: 'Type something else',
|
||||
* placeholder: 'Type something else',
|
||||
* })
|
||||
* .setCustomId('woah')
|
||||
* .setStyle(TextInputStyle.Paragraph);
|
||||
@@ -62,6 +62,7 @@ export class TextInputBuilder
|
||||
* Sets the label for this text input.
|
||||
*
|
||||
* @param label - The label to use
|
||||
* @deprecated Use a label builder to create a label (and optionally a description) instead.
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = labelValidator.parse(label);
|
||||
@@ -132,7 +133,7 @@ export class TextInputBuilder
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APITextInputComponent {
|
||||
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
|
||||
validateRequiredParameters(this.data.custom_id, this.data.style);
|
||||
|
||||
return {
|
||||
...this.data,
|
||||
|
||||
72
packages/builders/src/components/v2/Assertions.ts
Normal file
72
packages/builders/src/components/v2/Assertions.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||
import { colorPredicate } from '../../messages/embed/Assertions';
|
||||
import { isValidationEnabled } from '../../util/validation';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { ButtonBuilder } from '../button/Button';
|
||||
import type { ContainerComponentBuilder } from './Container';
|
||||
import type { MediaGalleryItemBuilder } from './MediaGalleryItem';
|
||||
import type { TextDisplayBuilder } from './TextDisplay';
|
||||
import { ThumbnailBuilder } from './Thumbnail';
|
||||
|
||||
export const unfurledMediaItemPredicate = s
|
||||
.object({
|
||||
url: s
|
||||
.string()
|
||||
.url(
|
||||
{ allowedProtocols: ['http:', 'https:', 'attachment:'] },
|
||||
{ message: 'Invalid protocol for media URL. Must be http:, https:, or attachment:' },
|
||||
),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const descriptionPredicate = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(1_024)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const filePredicate = s
|
||||
.object({
|
||||
url: s
|
||||
.string()
|
||||
.url({ allowedProtocols: ['attachment:'] }, { message: 'Invalid protocol for file URL. Must be attachment:' }),
|
||||
})
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const spoilerPredicate = s.boolean();
|
||||
|
||||
export const dividerPredicate = s.boolean();
|
||||
|
||||
export const spacingPredicate = s.nativeEnum(SeparatorSpacingSize);
|
||||
|
||||
export const textDisplayContentPredicate = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(4_000)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const accessoryPredicate = s
|
||||
.instance(ButtonBuilder)
|
||||
.or(s.instance(ThumbnailBuilder))
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const containerColorPredicate = colorPredicate.nullish();
|
||||
|
||||
export function assertReturnOfBuilder<ReturnType extends MediaGalleryItemBuilder | TextDisplayBuilder>(
|
||||
input: unknown,
|
||||
ExpectedInstanceOf: new () => ReturnType,
|
||||
): asserts input is ReturnType {
|
||||
s.instance(ExpectedInstanceOf).setValidationEnabled(isValidationEnabled).parse(input);
|
||||
}
|
||||
|
||||
export function validateComponentArray<
|
||||
ReturnType extends ContainerComponentBuilder | MediaGalleryItemBuilder = ContainerComponentBuilder,
|
||||
>(input: unknown, min: number, max: number, ExpectedInstanceOf?: new () => ReturnType): asserts input is ReturnType[] {
|
||||
(ExpectedInstanceOf ? s.instance(ExpectedInstanceOf) : s.instance(ComponentBuilder))
|
||||
.array()
|
||||
.lengthGreaterThanOrEqual(min)
|
||||
.lengthLessThanOrEqual(max)
|
||||
.setValidationEnabled(isValidationEnabled)
|
||||
.parse(input);
|
||||
}
|
||||
239
packages/builders/src/components/v2/Container.ts
Normal file
239
packages/builders/src/components/v2/Container.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIComponentInContainer,
|
||||
APIComponentInMessageActionRow,
|
||||
APIContainerComponent,
|
||||
APIFileComponent,
|
||||
APIMediaGalleryComponent,
|
||||
APISectionComponent,
|
||||
APISeparatorComponent,
|
||||
APITextDisplayComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import type { RGBTuple } from '../../index.js';
|
||||
import { MediaGalleryBuilder, SectionBuilder } from '../../index.js';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import type { AnyComponentBuilder, MessageActionRowComponentBuilder } from '../ActionRow.js';
|
||||
import { ActionRowBuilder } from '../ActionRow.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { createComponentBuilder, resolveBuilder } from '../Components.js';
|
||||
import { containerColorPredicate, spoilerPredicate } from './Assertions.js';
|
||||
import { FileBuilder } from './File.js';
|
||||
import { SeparatorBuilder } from './Separator.js';
|
||||
import { TextDisplayBuilder } from './TextDisplay.js';
|
||||
|
||||
/**
|
||||
* The builders that may be used within a container.
|
||||
*/
|
||||
export type ContainerComponentBuilder =
|
||||
| ActionRowBuilder<AnyComponentBuilder>
|
||||
| FileBuilder
|
||||
| MediaGalleryBuilder
|
||||
| SectionBuilder
|
||||
| SeparatorBuilder
|
||||
| TextDisplayBuilder;
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for a container.
|
||||
*/
|
||||
export class ContainerBuilder extends ComponentBuilder<APIContainerComponent> {
|
||||
/**
|
||||
* The components within this container.
|
||||
*/
|
||||
public readonly components: ContainerComponentBuilder[];
|
||||
|
||||
/**
|
||||
* Creates a new container from API data.
|
||||
*
|
||||
* @param data - The API data to create this container with
|
||||
* @example
|
||||
* Creating a container from an API data object:
|
||||
* ```ts
|
||||
* const container = new ContainerBuilder({
|
||||
* components: [
|
||||
* {
|
||||
* content: "Some text here",
|
||||
* type: ComponentType.TextDisplay,
|
||||
* },
|
||||
* ],
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a container using setters and API data:
|
||||
* ```ts
|
||||
* const container = new ContainerBuilder({
|
||||
* components: [
|
||||
* {
|
||||
* content: "# Heading",
|
||||
* type: ComponentType.TextDisplay,
|
||||
* },
|
||||
* ],
|
||||
* })
|
||||
* .addComponents(separator, section);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ components, ...data }: Partial<APIContainerComponent> = {}) {
|
||||
super({ type: ComponentType.Container, ...data });
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ??
|
||||
[]) as ContainerComponentBuilder[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accent color of this container.
|
||||
*
|
||||
* @param color - The color to use
|
||||
*/
|
||||
public setAccentColor(color?: RGBTuple | number): this {
|
||||
// Data assertions
|
||||
containerColorPredicate.parse(color);
|
||||
|
||||
if (Array.isArray(color)) {
|
||||
const [red, green, blue] = color;
|
||||
this.data.accent_color = (red << 16) + (green << 8) + blue;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.accent_color = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the accent color of this container.
|
||||
*/
|
||||
public clearAccentColor() {
|
||||
this.data.accent_color = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds action row components to this container.
|
||||
*
|
||||
* @param components - The action row components to add
|
||||
*/
|
||||
public addActionRowComponents<ComponentType extends MessageActionRowComponentBuilder>(
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder<ComponentType>
|
||||
| APIActionRowComponent<APIComponentInMessageActionRow>
|
||||
| ((builder: ActionRowBuilder<ComponentType>) => ActionRowBuilder<ComponentType>)
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((component) => resolveBuilder(component, ActionRowBuilder<ComponentType>)),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds file components to this container.
|
||||
*
|
||||
* @param components - The file components to add
|
||||
*/
|
||||
public addFileComponents(
|
||||
...components: RestOrArray<APIFileComponent | FileBuilder | ((builder: FileBuilder) => FileBuilder)>
|
||||
) {
|
||||
this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, FileBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds media gallery components to this container.
|
||||
*
|
||||
* @param components - The media gallery components to add
|
||||
*/
|
||||
public addMediaGalleryComponents(
|
||||
...components: RestOrArray<
|
||||
APIMediaGalleryComponent | MediaGalleryBuilder | ((builder: MediaGalleryBuilder) => MediaGalleryBuilder)
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((component) => resolveBuilder(component, MediaGalleryBuilder)),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds section components to this container.
|
||||
*
|
||||
* @param components - The section components to add
|
||||
*/
|
||||
public addSectionComponents(
|
||||
...components: RestOrArray<APISectionComponent | SectionBuilder | ((builder: SectionBuilder) => SectionBuilder)>
|
||||
) {
|
||||
this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, SectionBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds separator components to this container.
|
||||
*
|
||||
* @param components - The separator components to add
|
||||
*/
|
||||
public addSeparatorComponents(
|
||||
...components: RestOrArray<
|
||||
APISeparatorComponent | SeparatorBuilder | ((builder: SeparatorBuilder) => SeparatorBuilder)
|
||||
>
|
||||
) {
|
||||
this.components.push(...normalizeArray(components).map((component) => resolveBuilder(component, SeparatorBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text display components to this container.
|
||||
*
|
||||
* @param components - The text display components to add
|
||||
*/
|
||||
public addTextDisplayComponents(
|
||||
...components: RestOrArray<
|
||||
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((component) => resolveBuilder(component, TextDisplayBuilder)),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts components for this container.
|
||||
*
|
||||
* @param index - The index to start removing, replacing or inserting components
|
||||
* @param deleteCount - The amount of components to remove
|
||||
* @param components - The components to set
|
||||
*/
|
||||
public spliceComponents(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...components: RestOrArray<APIComponentInContainer | ContainerComponentBuilder>
|
||||
) {
|
||||
this.components.splice(
|
||||
index,
|
||||
deleteCount,
|
||||
...normalizeArray(components).map((component) =>
|
||||
component instanceof ComponentBuilder ? component : createComponentBuilder(component),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spoiler status of this container.
|
||||
*
|
||||
* @param spoiler - The spoiler status to use
|
||||
*/
|
||||
public setSpoiler(spoiler = true) {
|
||||
this.data.spoiler = spoilerPredicate.parse(spoiler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APIContainerComponent {
|
||||
return {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
} as APIContainerComponent;
|
||||
}
|
||||
}
|
||||
63
packages/builders/src/components/v2/File.ts
Normal file
63
packages/builders/src/components/v2/File.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ComponentType, type APIFileComponent } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { filePredicate, spoilerPredicate } from './Assertions';
|
||||
|
||||
export class FileBuilder extends ComponentBuilder<APIFileComponent> {
|
||||
/**
|
||||
* Creates a new file from API data.
|
||||
*
|
||||
* @param data - The API data to create this file with
|
||||
* @example
|
||||
* Creating a file from an API data object:
|
||||
* ```ts
|
||||
* const file = new FileBuilder({
|
||||
* spoiler: true,
|
||||
* file: {
|
||||
* url: 'attachment://file.png',
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a file using setters and API data:
|
||||
* ```ts
|
||||
* const file = new FileBuilder({
|
||||
* file: {
|
||||
* url: 'attachment://image.jpg',
|
||||
* },
|
||||
* })
|
||||
* .setSpoiler(false);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APIFileComponent> = {}) {
|
||||
super({ type: ComponentType.File, ...data, file: data.file ? { url: data.file.url } : undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spoiler status of this file.
|
||||
*
|
||||
* @param spoiler - The spoiler status to use
|
||||
*/
|
||||
public setSpoiler(spoiler = true) {
|
||||
this.data.spoiler = spoilerPredicate.parse(spoiler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media URL of this file.
|
||||
*
|
||||
* @param url - The URL to use
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
this.data.file = filePredicate.parse({ url });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APIFileComponent {
|
||||
filePredicate.parse(this.data.file);
|
||||
|
||||
return { ...this.data, file: { ...this.data.file } } as APIFileComponent;
|
||||
}
|
||||
}
|
||||
117
packages/builders/src/components/v2/MediaGallery.ts
Normal file
117
packages/builders/src/components/v2/MediaGallery.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type { APIMediaGalleryComponent, APIMediaGalleryItem } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { resolveBuilder } from '../Components.js';
|
||||
import { assertReturnOfBuilder, validateComponentArray } from './Assertions.js';
|
||||
import { MediaGalleryItemBuilder } from './MediaGalleryItem.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for a container.
|
||||
*/
|
||||
export class MediaGalleryBuilder extends ComponentBuilder<APIMediaGalleryComponent> {
|
||||
/**
|
||||
* The components within this container.
|
||||
*/
|
||||
public readonly items: MediaGalleryItemBuilder[];
|
||||
|
||||
/**
|
||||
* Creates a new media gallery from API data.
|
||||
*
|
||||
* @param data - The API data to create this media gallery with
|
||||
* @example
|
||||
* Creating a media gallery from an API data object:
|
||||
* ```ts
|
||||
* const mediaGallery = new MediaGalleryBuilder({
|
||||
* items: [
|
||||
* {
|
||||
* description: "Some text here",
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/2.png',
|
||||
* },
|
||||
* },
|
||||
* ],
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a media gallery using setters and API data:
|
||||
* ```ts
|
||||
* const mediaGallery = new MediaGalleryBuilder({
|
||||
* items: [
|
||||
* {
|
||||
* description: "alt text",
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/5.png',
|
||||
* },
|
||||
* },
|
||||
* ],
|
||||
* })
|
||||
* .addItems(item2, item3);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ items, ...data }: Partial<APIMediaGalleryComponent> = {}) {
|
||||
super({ type: ComponentType.MediaGallery, ...data });
|
||||
this.items = items?.map((item) => new MediaGalleryItemBuilder(item)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to this media gallery.
|
||||
*
|
||||
* @param items - The items to add
|
||||
*/
|
||||
public addItems(
|
||||
...items: RestOrArray<
|
||||
APIMediaGalleryItem | MediaGalleryItemBuilder | ((builder: MediaGalleryItemBuilder) => MediaGalleryItemBuilder)
|
||||
>
|
||||
) {
|
||||
this.items.push(
|
||||
...normalizeArray(items).map((input) => {
|
||||
const result = resolveBuilder(input, MediaGalleryItemBuilder);
|
||||
|
||||
assertReturnOfBuilder(result, MediaGalleryItemBuilder);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts media gallery items for this media gallery.
|
||||
*
|
||||
* @param index - The index to start removing, replacing or inserting items
|
||||
* @param deleteCount - The amount of items to remove
|
||||
* @param items - The items to insert
|
||||
*/
|
||||
public spliceItems(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...items: RestOrArray<
|
||||
APIMediaGalleryItem | MediaGalleryItemBuilder | ((builder: MediaGalleryItemBuilder) => MediaGalleryItemBuilder)
|
||||
>
|
||||
) {
|
||||
this.items.splice(
|
||||
index,
|
||||
deleteCount,
|
||||
...normalizeArray(items).map((input) => {
|
||||
const result = resolveBuilder(input, MediaGalleryItemBuilder);
|
||||
|
||||
assertReturnOfBuilder(result, MediaGalleryItemBuilder);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APIMediaGalleryComponent {
|
||||
validateComponentArray(this.items, 1, 10, MediaGalleryItemBuilder);
|
||||
return {
|
||||
...this.data,
|
||||
items: this.items.map((item) => item.toJSON()),
|
||||
} as APIMediaGalleryComponent;
|
||||
}
|
||||
}
|
||||
90
packages/builders/src/components/v2/MediaGalleryItem.ts
Normal file
90
packages/builders/src/components/v2/MediaGalleryItem.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type { APIMediaGalleryItem } from 'discord-api-types/v10';
|
||||
import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions';
|
||||
|
||||
export class MediaGalleryItemBuilder implements JSONEncodable<APIMediaGalleryItem> {
|
||||
/**
|
||||
* The API data associated with this media gallery item.
|
||||
*/
|
||||
public readonly data: Partial<APIMediaGalleryItem>;
|
||||
|
||||
/**
|
||||
* Creates a new media gallery item from API data.
|
||||
*
|
||||
* @param data - The API data to create this media gallery item with
|
||||
* @example
|
||||
* Creating a media gallery item from an API data object:
|
||||
* ```ts
|
||||
* const item = new MediaGalleryItemBuilder({
|
||||
* description: "Some text here",
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/2.png',
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a media gallery item using setters and API data:
|
||||
* ```ts
|
||||
* const item = new MediaGalleryItemBuilder({
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/5.png',
|
||||
* },
|
||||
* })
|
||||
* .setDescription("alt text");
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APIMediaGalleryItem> = {}) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this media gallery item.
|
||||
*
|
||||
* @param description - The description to use
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = descriptionPredicate.parse(description);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the description of this media gallery item.
|
||||
*/
|
||||
public clearDescription() {
|
||||
this.data.description = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spoiler status of this media gallery item.
|
||||
*
|
||||
* @param spoiler - The spoiler status to use
|
||||
*/
|
||||
public setSpoiler(spoiler = true) {
|
||||
this.data.spoiler = spoilerPredicate.parse(spoiler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media URL of this media gallery item.
|
||||
*
|
||||
* @param url - The URL to use
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
this.data.media = unfurledMediaItemPredicate.parse({ url });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this builder to API-compatible JSON data.
|
||||
*
|
||||
* @remarks
|
||||
* This method runs validations on the data before serializing it.
|
||||
* As such, it may throw an error if the data is invalid.
|
||||
*/
|
||||
public toJSON(): APIMediaGalleryItem {
|
||||
unfurledMediaItemPredicate.parse(this.data.media);
|
||||
|
||||
return { ...this.data } as APIMediaGalleryItem;
|
||||
}
|
||||
}
|
||||
153
packages/builders/src/components/v2/Section.ts
Normal file
153
packages/builders/src/components/v2/Section.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type {
|
||||
APIButtonComponent,
|
||||
APISectionComponent,
|
||||
APITextDisplayComponent,
|
||||
APIThumbnailComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ButtonBuilder, ThumbnailBuilder } from '../../index.js';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { ComponentBuilder } from '../Component.js';
|
||||
import { createComponentBuilder, resolveBuilder } from '../Components.js';
|
||||
import { accessoryPredicate, assertReturnOfBuilder, validateComponentArray } from './Assertions.js';
|
||||
import { TextDisplayBuilder } from './TextDisplay.js';
|
||||
|
||||
/**
|
||||
* A builder that creates API-compatible JSON data for a section.
|
||||
*/
|
||||
export class SectionBuilder extends ComponentBuilder<APISectionComponent> {
|
||||
/**
|
||||
* The components within this section.
|
||||
*/
|
||||
public readonly components: ComponentBuilder[];
|
||||
|
||||
/**
|
||||
* The accessory of this section.
|
||||
*/
|
||||
public readonly accessory?: ButtonBuilder | ThumbnailBuilder;
|
||||
|
||||
/**
|
||||
* Creates a new section from API data.
|
||||
*
|
||||
* @param data - The API data to create this section with
|
||||
* @example
|
||||
* Creating a section from an API data object:
|
||||
* ```ts
|
||||
* const section = new SectionBuilder({
|
||||
* components: [
|
||||
* {
|
||||
* content: "Some text here",
|
||||
* type: ComponentType.TextDisplay,
|
||||
* },
|
||||
* ],
|
||||
* accessory: {
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/3.png',
|
||||
* },
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a section using setters and API data:
|
||||
* ```ts
|
||||
* const section = new SectionBuilder({
|
||||
* components: [
|
||||
* {
|
||||
* content: "# Heading",
|
||||
* type: ComponentType.TextDisplay,
|
||||
* },
|
||||
* ],
|
||||
* })
|
||||
* .setPrimaryButtonAccessory(button);
|
||||
* ```
|
||||
*/
|
||||
public constructor({ components, accessory, ...data }: Partial<APISectionComponent> = {}) {
|
||||
super({ type: ComponentType.Section, ...data });
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentBuilder[];
|
||||
this.accessory = accessory ? createComponentBuilder(accessory) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accessory of this section to a button.
|
||||
*
|
||||
* @param accessory - The accessory to use
|
||||
*/
|
||||
public setButtonAccessory(
|
||||
accessory: APIButtonComponent | ButtonBuilder | ((builder: ButtonBuilder) => ButtonBuilder),
|
||||
): this {
|
||||
Reflect.set(this, 'accessory', accessoryPredicate.parse(resolveBuilder(accessory, ButtonBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accessory of this section to a thumbnail.
|
||||
*
|
||||
* @param accessory - The accessory to use
|
||||
*/
|
||||
public setThumbnailAccessory(
|
||||
accessory: APIThumbnailComponent | ThumbnailBuilder | ((builder: ThumbnailBuilder) => ThumbnailBuilder),
|
||||
): this {
|
||||
Reflect.set(this, 'accessory', accessoryPredicate.parse(resolveBuilder(accessory, ThumbnailBuilder)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text display components to this section.
|
||||
*
|
||||
* @param components - The text display components to add
|
||||
*/
|
||||
public addTextDisplayComponents(
|
||||
...components: RestOrArray<TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((input) => {
|
||||
const result = resolveBuilder(input, TextDisplayBuilder);
|
||||
|
||||
assertReturnOfBuilder(result, TextDisplayBuilder);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts text display components for this section.
|
||||
*
|
||||
* @param index - The index to start removing, replacing or inserting text display components
|
||||
* @param deleteCount - The amount of text display components to remove
|
||||
* @param components - The text display components to insert
|
||||
*/
|
||||
public spliceTextDisplayComponents(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...components: RestOrArray<
|
||||
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
|
||||
>
|
||||
) {
|
||||
this.components.splice(
|
||||
index,
|
||||
deleteCount,
|
||||
...normalizeArray(components).map((input) => {
|
||||
const result = resolveBuilder(input, TextDisplayBuilder);
|
||||
|
||||
assertReturnOfBuilder(result, TextDisplayBuilder);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public toJSON(): APISectionComponent {
|
||||
validateComponentArray(this.components, 1, 3, TextDisplayBuilder);
|
||||
return {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
accessory: accessoryPredicate.parse(this.accessory).toJSON(),
|
||||
} as APISectionComponent;
|
||||
}
|
||||
}
|
||||
69
packages/builders/src/components/v2/Separator.ts
Normal file
69
packages/builders/src/components/v2/Separator.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { SeparatorSpacingSize, APISeparatorComponent } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { dividerPredicate, spacingPredicate } from './Assertions';
|
||||
|
||||
export class SeparatorBuilder extends ComponentBuilder<APISeparatorComponent> {
|
||||
/**
|
||||
* Creates a new separator from API data.
|
||||
*
|
||||
* @param data - The API data to create this separator with
|
||||
* @example
|
||||
* Creating a separator from an API data object:
|
||||
* ```ts
|
||||
* const separator = new SeparatorBuilder({
|
||||
* spacing: SeparatorSpacingSize.Small,
|
||||
* divider: true,
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a separator using setters and API data:
|
||||
* ```ts
|
||||
* const separator = new SeparatorBuilder({
|
||||
* spacing: SeparatorSpacingSize.Large,
|
||||
* })
|
||||
* .setDivider(false);
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APISeparatorComponent> = {}) {
|
||||
super({
|
||||
type: ComponentType.Separator,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this separator should show a divider line.
|
||||
*
|
||||
* @param divider - Whether to show a divider line
|
||||
*/
|
||||
public setDivider(divider = true) {
|
||||
this.data.divider = dividerPredicate.parse(divider);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spacing of this separator.
|
||||
*
|
||||
* @param spacing - The spacing to use
|
||||
*/
|
||||
public setSpacing(spacing: SeparatorSpacingSize) {
|
||||
this.data.spacing = spacingPredicate.parse(spacing);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the spacing of this separator.
|
||||
*/
|
||||
public clearSpacing() {
|
||||
this.data.spacing = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APISeparatorComponent {
|
||||
return { ...this.data } as APISeparatorComponent;
|
||||
}
|
||||
}
|
||||
52
packages/builders/src/components/v2/TextDisplay.ts
Normal file
52
packages/builders/src/components/v2/TextDisplay.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { APITextDisplayComponent } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { textDisplayContentPredicate } from './Assertions';
|
||||
|
||||
export class TextDisplayBuilder extends ComponentBuilder<APITextDisplayComponent> {
|
||||
/**
|
||||
* Creates a new text display from API data.
|
||||
*
|
||||
* @param data - The API data to create this text display with
|
||||
* @example
|
||||
* Creating a text display from an API data object:
|
||||
* ```ts
|
||||
* const textDisplay = new TextDisplayBuilder({
|
||||
* content: 'some text',
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a text display using setters and API data:
|
||||
* ```ts
|
||||
* const textDisplay = new TextDisplayBuilder({
|
||||
* content: 'old text',
|
||||
* })
|
||||
* .setContent('new text');
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APITextDisplayComponent> = {}) {
|
||||
super({
|
||||
type: ComponentType.TextDisplay,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text of this text display.
|
||||
*
|
||||
* @param content - The text to use
|
||||
*/
|
||||
public setContent(content: string) {
|
||||
this.data.content = textDisplayContentPredicate.parse(content);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APITextDisplayComponent {
|
||||
textDisplayContentPredicate.parse(this.data.content);
|
||||
|
||||
return { ...this.data } as APITextDisplayComponent;
|
||||
}
|
||||
}
|
||||
86
packages/builders/src/components/v2/Thumbnail.ts
Normal file
86
packages/builders/src/components/v2/Thumbnail.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { APIThumbnailComponent } from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
import { descriptionPredicate, spoilerPredicate, unfurledMediaItemPredicate } from './Assertions';
|
||||
|
||||
export class ThumbnailBuilder extends ComponentBuilder<APIThumbnailComponent> {
|
||||
/**
|
||||
* Creates a new thumbnail from API data.
|
||||
*
|
||||
* @param data - The API data to create this thumbnail with
|
||||
* @example
|
||||
* Creating a thumbnail from an API data object:
|
||||
* ```ts
|
||||
* const thumbnail = new ThumbnailBuilder({
|
||||
* description: 'some text',
|
||||
* media: {
|
||||
* url: 'https://cdn.discordapp.com/embed/avatars/4.png',
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
* @example
|
||||
* Creating a thumbnail using setters and API data:
|
||||
* ```ts
|
||||
* const thumbnail = new ThumbnailBuilder({
|
||||
* media: {
|
||||
* url: 'attachment://image.png',
|
||||
* },
|
||||
* })
|
||||
* .setDescription('alt text');
|
||||
* ```
|
||||
*/
|
||||
public constructor(data: Partial<APIThumbnailComponent> = {}) {
|
||||
super({
|
||||
type: ComponentType.Thumbnail,
|
||||
...data,
|
||||
media: data.media ? { url: data.media.url } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this thumbnail.
|
||||
*
|
||||
* @param description - The description to use
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = descriptionPredicate.parse(description);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the description of this thumbnail.
|
||||
*/
|
||||
public clearDescription() {
|
||||
this.data.description = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the spoiler status of this thumbnail.
|
||||
*
|
||||
* @param spoiler - The spoiler status to use
|
||||
*/
|
||||
public setSpoiler(spoiler = true) {
|
||||
this.data.spoiler = spoilerPredicate.parse(spoiler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media URL of this thumbnail.
|
||||
*
|
||||
* @param url - The URL to use
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
this.data.media = unfurledMediaItemPredicate.parse({ url });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc ComponentBuilder.toJSON}
|
||||
*/
|
||||
public override toJSON(): APIThumbnailComponent {
|
||||
unfurledMediaItemPredicate.parse(this.data.media);
|
||||
|
||||
return { ...this.data } as APIThumbnailComponent;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,22 @@ export {
|
||||
export * from './components/selectMenu/StringSelectMenuOption.js';
|
||||
export * from './components/selectMenu/UserSelectMenu.js';
|
||||
|
||||
export * from './components/fileUpload/FileUpload.js';
|
||||
export * as FileUploadAssertions from './components/fileUpload/Assertions.js';
|
||||
|
||||
export * from './components/label/Label.js';
|
||||
export * as LabelAssertions from './components/label/Assertions.js';
|
||||
|
||||
export * as ComponentsV2Assertions from './components/v2/Assertions.js';
|
||||
export * from './components/v2/Container.js';
|
||||
export * from './components/v2/File.js';
|
||||
export * from './components/v2/MediaGallery.js';
|
||||
export * from './components/v2/MediaGalleryItem.js';
|
||||
export * from './components/v2/Section.js';
|
||||
export * from './components/v2/Separator.js';
|
||||
export * from './components/v2/TextDisplay.js';
|
||||
export * from './components/v2/Thumbnail.js';
|
||||
|
||||
export * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js';
|
||||
export * from './interactions/slashCommands/SlashCommandBuilder.js';
|
||||
export * from './interactions/slashCommands/SlashCommandSubcommands.js';
|
||||
|
||||
@@ -7,8 +7,7 @@ const namePredicate = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(32)
|
||||
// eslint-disable-next-line prefer-named-capture-group
|
||||
.regex(/^( *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]+ *)+$/u)
|
||||
.regex(/\S/)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
const typePredicate = s
|
||||
.union([s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message)])
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
|
||||
import { customIdValidator } from '../../components/Assertions.js';
|
||||
import { LabelBuilder } from '../../components/label/Label.js';
|
||||
import { TextDisplayBuilder } from '../../components/v2/TextDisplay.js';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
|
||||
export const titleValidator = s
|
||||
@@ -9,7 +11,7 @@ export const titleValidator = s
|
||||
.lengthLessThanOrEqual(45)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const componentsValidator = s
|
||||
.instance(ActionRowBuilder)
|
||||
.union([s.instance(ActionRowBuilder), s.instance(LabelBuilder), s.instance(TextDisplayBuilder)])
|
||||
.array()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
@@ -17,7 +19,7 @@ export const componentsValidator = s
|
||||
export function validateRequiredParameters(
|
||||
customId?: string,
|
||||
title?: string,
|
||||
components?: ActionRowBuilder<ModalActionRowComponentBuilder>[],
|
||||
components?: (ActionRowBuilder<ModalActionRowComponentBuilder> | LabelBuilder | TextDisplayBuilder)[],
|
||||
) {
|
||||
customIdValidator.parse(customId);
|
||||
titleValidator.parse(title);
|
||||
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
import type { JSONEncodable } from '@discordjs/util';
|
||||
import type {
|
||||
APITextInputComponent,
|
||||
APIActionRowComponent,
|
||||
APIModalActionRowComponent,
|
||||
APIComponentInModalActionRow,
|
||||
APILabelComponent,
|
||||
APIModalInteractionResponseCallbackData,
|
||||
APITextDisplayComponent,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
|
||||
import { customIdValidator } from '../../components/Assertions.js';
|
||||
import { createComponentBuilder } from '../../components/Components.js';
|
||||
import { createComponentBuilder, resolveBuilder } from '../../components/Components.js';
|
||||
import { LabelBuilder } from '../../components/label/Label.js';
|
||||
import { TextInputBuilder } from '../../components/textInput/TextInput.js';
|
||||
import { TextDisplayBuilder } from '../../components/v2/TextDisplay.js';
|
||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||
import { titleValidator, validateRequiredParameters } from './Assertions.js';
|
||||
|
||||
@@ -24,7 +31,8 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
/**
|
||||
* The components within this modal.
|
||||
*/
|
||||
public readonly components: ActionRowBuilder<ModalActionRowComponentBuilder>[] = [];
|
||||
public readonly components: (ActionRowBuilder<ModalActionRowComponentBuilder> | LabelBuilder | TextDisplayBuilder)[] =
|
||||
[];
|
||||
|
||||
/**
|
||||
* Creates a new modal from API data.
|
||||
@@ -33,8 +41,10 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
*/
|
||||
public constructor({ components, ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
|
||||
this.data = { ...data };
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ??
|
||||
[]) as ActionRowBuilder<ModalActionRowComponentBuilder>[];
|
||||
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as (
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| LabelBuilder
|
||||
)[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,28 +71,182 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
||||
* Adds components to this modal.
|
||||
*
|
||||
* @param components - The components to add
|
||||
* @deprecated Use {@link ModalBuilder.addLabelComponents} or {@link ModalBuilder.addTextDisplayComponents} instead
|
||||
*/
|
||||
public addComponents(
|
||||
...components: RestOrArray<
|
||||
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIModalActionRowComponent>
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIComponentInModalActionRow>
|
||||
| APILabelComponent
|
||||
| APITextDisplayComponent
|
||||
| APITextInputComponent
|
||||
| LabelBuilder
|
||||
| TextDisplayBuilder
|
||||
| TextInputBuilder
|
||||
>
|
||||
) {
|
||||
this.components.push(
|
||||
...normalizeArray(components).map((component) =>
|
||||
component instanceof ActionRowBuilder
|
||||
? component
|
||||
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
|
||||
),
|
||||
...normalizeArray(components).map((component, idx) => {
|
||||
if (
|
||||
component instanceof ActionRowBuilder ||
|
||||
component instanceof LabelBuilder ||
|
||||
component instanceof TextDisplayBuilder
|
||||
) {
|
||||
return component;
|
||||
}
|
||||
|
||||
// Deprecated support
|
||||
if (component instanceof TextInputBuilder) {
|
||||
return new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(component);
|
||||
}
|
||||
|
||||
if ('type' in component) {
|
||||
if (component.type === ComponentType.ActionRow) {
|
||||
return new ActionRowBuilder<ModalActionRowComponentBuilder>(component);
|
||||
}
|
||||
|
||||
if (component.type === ComponentType.Label) {
|
||||
return new LabelBuilder(component);
|
||||
}
|
||||
|
||||
if (component.type === ComponentType.TextDisplay) {
|
||||
return new TextDisplayBuilder(component);
|
||||
}
|
||||
|
||||
// Deprecated, should go in a label component
|
||||
if (component.type === ComponentType.TextInput) {
|
||||
return new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
|
||||
new TextInputBuilder(component),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(`Invalid component passed in ModalBuilder.addComponents at index ${idx}!`);
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds label components to this modal.
|
||||
*
|
||||
* @param components - The components to add
|
||||
*/
|
||||
public addLabelComponents(
|
||||
...components: RestOrArray<APILabelComponent | LabelBuilder | ((builder: LabelBuilder) => LabelBuilder)>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
const resolved = normalized.map((label) => resolveBuilder(label, LabelBuilder));
|
||||
|
||||
this.components.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text display components to this modal.
|
||||
*
|
||||
* @param components - The components to add
|
||||
*/
|
||||
public addTextDisplayComponents(
|
||||
...components: RestOrArray<
|
||||
APITextDisplayComponent | TextDisplayBuilder | ((builder: TextDisplayBuilder) => TextDisplayBuilder)
|
||||
>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
const resolved = normalized.map((row) => resolveBuilder(row, TextDisplayBuilder));
|
||||
|
||||
this.components.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds action rows to this modal.
|
||||
*
|
||||
* @param components - The components to add
|
||||
* @deprecated Use {@link ModalBuilder.addLabelComponents} instead
|
||||
*/
|
||||
public addActionRowComponents(
|
||||
...components: RestOrArray<
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIComponentInModalActionRow>
|
||||
| ((
|
||||
builder: ActionRowBuilder<ModalActionRowComponentBuilder>,
|
||||
) => ActionRowBuilder<ModalActionRowComponentBuilder>)
|
||||
>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
const resolved = normalized.map((row) => resolveBuilder(row, ActionRowBuilder<ModalActionRowComponentBuilder>));
|
||||
|
||||
this.components.push(...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the labels for this modal.
|
||||
*
|
||||
* @param components - The components to set
|
||||
*/
|
||||
public setLabelComponents(
|
||||
...components: RestOrArray<APILabelComponent | LabelBuilder | ((builder: LabelBuilder) => LabelBuilder)>
|
||||
) {
|
||||
const normalized = normalizeArray(components);
|
||||
this.spliceLabelComponents(0, this.components.length, ...normalized);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts labels for this modal.
|
||||
*
|
||||
* @remarks
|
||||
* This method behaves similarly
|
||||
* to {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||
* The maximum amount of labels that can be added is 5.
|
||||
*
|
||||
* It's useful for modifying and adjusting order of the already-existing labels of a modal.
|
||||
* @example
|
||||
* Remove the first label:
|
||||
* ```ts
|
||||
* modal.spliceLabelComponents(0, 1);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the first n labels:
|
||||
* ```ts
|
||||
* const n = 4;
|
||||
* modal.spliceLabelComponents(0, n);
|
||||
* ```
|
||||
* @example
|
||||
* Remove the last label:
|
||||
* ```ts
|
||||
* modal.spliceLabelComponents(-1, 1);
|
||||
* ```
|
||||
* @param index - The index to start at
|
||||
* @param deleteCount - The number of labels to remove
|
||||
* @param labels - The replacing label objects
|
||||
*/
|
||||
public spliceLabelComponents(
|
||||
index: number,
|
||||
deleteCount: number,
|
||||
...labels: (APILabelComponent | LabelBuilder | ((builder: LabelBuilder) => LabelBuilder))[]
|
||||
): this {
|
||||
const resolved = labels.map((label) => resolveBuilder(label, LabelBuilder));
|
||||
this.components.splice(index, deleteCount, ...resolved);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets components for this modal.
|
||||
*
|
||||
* @param components - The components to set
|
||||
* @deprecated Use {@link ModalBuilder.setLabelComponents} instead
|
||||
*/
|
||||
public setComponents(...components: RestOrArray<ActionRowBuilder<ModalActionRowComponentBuilder>>) {
|
||||
public setComponents(
|
||||
...components: RestOrArray<ActionRowBuilder<ModalActionRowComponentBuilder> | LabelBuilder | TextDisplayBuilder>
|
||||
) {
|
||||
this.components.splice(0, this.components.length, ...normalizeArray(components));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -2,17 +2,9 @@ import { s } from '@sapphire/shapeshift';
|
||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||
import { isValidationEnabled } from '../../util/validation.js';
|
||||
|
||||
export const fieldNamePredicate = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(256)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const fieldNamePredicate = s.string().lengthLessThanOrEqual(256).setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const fieldValuePredicate = s
|
||||
.string()
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(1_024)
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
export const fieldValuePredicate = s.string().lengthLessThanOrEqual(1_024).setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const fieldInlinePredicate = s.boolean().optional();
|
||||
|
||||
@@ -32,7 +24,10 @@ export function validateFieldLength(amountAdding: number, fields?: APIEmbedField
|
||||
fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding);
|
||||
}
|
||||
|
||||
export const authorNamePredicate = fieldNamePredicate.nullable().setValidationEnabled(isValidationEnabled);
|
||||
export const authorNamePredicate = fieldNamePredicate
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.nullable()
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const imageURLPredicate = s
|
||||
.string()
|
||||
@@ -96,4 +91,7 @@ export const embedFooterPredicate = s
|
||||
|
||||
export const timestampPredicate = s.union([s.number(), s.date()]).nullable().setValidationEnabled(isValidationEnabled);
|
||||
|
||||
export const titlePredicate = fieldNamePredicate.nullable().setValidationEnabled(isValidationEnabled);
|
||||
export const titlePredicate = fieldNamePredicate
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.nullable()
|
||||
.setValidationEnabled(isValidationEnabled);
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# [@discordjs/core@2.3.0](https://github.com/discordjs/discord.js/compare/@discordjs/core@2.2.2...@discordjs/core@2.3.0) - (2025-10-08)
|
||||
|
||||
## Features
|
||||
|
||||
- Add `{add,remove}GroupDMRecipient` methods (#11135) ([72771b7](https://github.com/discordjs/discord.js/commit/72771b79aa3a78967be92ea2e4c523755d0d2ec0))
|
||||
- **guild:** Support incident actions (#11131) ([63dbe48](https://github.com/discordjs/discord.js/commit/63dbe48055347413ec70f36bce4f645688776413))
|
||||
- Add gateway endpoints (#11130) ([a041723](https://github.com/discordjs/discord.js/commit/a04172325af5a3a9880253bb8dc7c057a0426d83))
|
||||
|
||||
# [@discordjs/core@2.2.2](https://github.com/discordjs/discord.js/compare/@discordjs/core@2.2.1...@discordjs/core@2.2.2) - (2025-09-10)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@discordjs/core",
|
||||
"version": "2.2.2",
|
||||
"version": "2.3.0",
|
||||
"description": "A thinly abstracted wrapper around the rest API, and gateway.",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
@@ -70,7 +70,7 @@
|
||||
"@discordjs/ws": "workspace:^",
|
||||
"@sapphire/snowflake": "^3.5.3",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.24"
|
||||
"discord-api-types": "^0.38.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
|
||||
34
packages/core/scripts/check-routes.mjs
Normal file
34
packages/core/scripts/check-routes.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Routes } from 'discord-api-types/v10';
|
||||
import { glob, readFile } from 'node:fs/promises';
|
||||
|
||||
const usedRoutes = new Set();
|
||||
|
||||
const ignoredRoutes = new Set([
|
||||
// Deprecated
|
||||
'channelPins',
|
||||
'channelPin',
|
||||
'guilds',
|
||||
'guildCurrentMemberNickname',
|
||||
'guildMFA',
|
||||
'nitroStickerPacks',
|
||||
]);
|
||||
|
||||
for await (const file of glob('src/api/*.ts')) {
|
||||
const content = await readFile(file, 'utf-8');
|
||||
|
||||
const routes = content.matchAll(/Routes\.([\w\d_]+)/g);
|
||||
for (const route of routes) {
|
||||
usedRoutes.add(route[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const unusedRoutes = Object.keys(Routes).filter((route) => !usedRoutes.has(route) && !ignoredRoutes.has(route));
|
||||
|
||||
if (unusedRoutes.length > 0) {
|
||||
console.warn('The following routes are not implemented:');
|
||||
for (const route of unusedRoutes) {
|
||||
console.warn(` - ${route}`);
|
||||
}
|
||||
} else {
|
||||
console.log('No missing routes.');
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import { makeURLSearchParams, type RawFile, type REST, type RequestData } from '@discordjs/rest';
|
||||
import { makeURLSearchParams, type RawFile, type RequestData, type REST } from '@discordjs/rest';
|
||||
import {
|
||||
Routes,
|
||||
type RESTPostAPIChannelWebhookJSONBody,
|
||||
type RESTPostAPIChannelWebhookResult,
|
||||
type APIThreadChannel,
|
||||
type RESTDeleteAPIChannelResult,
|
||||
type RESTGetAPIChannelInvitesResult,
|
||||
type RESTGetAPIChannelMessageReactionUsersQuery,
|
||||
@@ -17,8 +16,8 @@ import {
|
||||
type RESTGetAPIChannelThreadsArchivedQuery,
|
||||
type RESTGetAPIChannelUsersThreadsArchivedResult,
|
||||
type RESTGetAPIChannelWebhooksResult,
|
||||
type RESTPatchAPIChannelMessageJSONBody,
|
||||
type RESTPatchAPIChannelJSONBody,
|
||||
type RESTPatchAPIChannelMessageJSONBody,
|
||||
type RESTPatchAPIChannelMessageResult,
|
||||
type RESTPatchAPIChannelResult,
|
||||
type RESTPostAPIChannelFollowersResult,
|
||||
@@ -27,12 +26,14 @@ import {
|
||||
type RESTPostAPIChannelMessageCrosspostResult,
|
||||
type RESTPostAPIChannelMessageJSONBody,
|
||||
type RESTPostAPIChannelMessageResult,
|
||||
type RESTPutAPIChannelPermissionJSONBody,
|
||||
type Snowflake,
|
||||
type RESTPostAPIChannelThreadsJSONBody,
|
||||
type RESTPostAPIChannelThreadsResult,
|
||||
type APIThreadChannel,
|
||||
type RESTPostAPIChannelWebhookJSONBody,
|
||||
type RESTPostAPIChannelWebhookResult,
|
||||
type RESTPostAPIGuildForumThreadsJSONBody,
|
||||
type RESTPutAPIChannelPermissionJSONBody,
|
||||
type RESTPutAPIChannelRecipientJSONBody,
|
||||
type Snowflake,
|
||||
} from 'discord-api-types/v10';
|
||||
|
||||
export interface StartForumThreadOptions extends RESTPostAPIGuildForumThreadsJSONBody {
|
||||
@@ -593,4 +594,43 @@ export class ChannelsAPI {
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a recipient to a group DM channel
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#group-dm-add-recipient}
|
||||
* @param channelId - The id of the channel to add the recipient to
|
||||
* @param userId - The id of the user to add as a recipient
|
||||
* @param body - The data for adding the recipient
|
||||
* @param options - The options for adding the recipient
|
||||
*/
|
||||
public async addGroupDMRecipient(
|
||||
channelId: Snowflake,
|
||||
userId: Snowflake,
|
||||
body: RESTPutAPIChannelRecipientJSONBody,
|
||||
{ signal }: Pick<RequestData, 'signal'> = {},
|
||||
) {
|
||||
await this.rest.put(Routes.channelRecipient(channelId, userId), {
|
||||
body,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a recipient from a group DM channel
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/resources/channel#group-dm-remove-recipient}
|
||||
* @param channelId - The id of the channel to remove the recipient from
|
||||
* @param userId - The id of the user to remove as a recipient
|
||||
* @param options - The options for removing the recipient
|
||||
*/
|
||||
public async removeGroupDMRecipient(
|
||||
channelId: Snowflake,
|
||||
userId: Snowflake,
|
||||
{ signal }: Pick<RequestData, 'signal'> = {},
|
||||
) {
|
||||
await this.rest.delete(Routes.channelRecipient(channelId, userId), {
|
||||
signal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
31
packages/core/src/api/gateway.ts
Normal file
31
packages/core/src/api/gateway.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
|
||||
import type { RequestData, REST } from '@discordjs/rest';
|
||||
import { Routes, type RESTGetAPIGatewayBotResult, type RESTGetAPIGatewayResult } from 'discord-api-types/v10';
|
||||
|
||||
export class GatewayAPI {
|
||||
public constructor(private readonly rest: REST) {}
|
||||
|
||||
/**
|
||||
* Gets gateway information.
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/events/gateway#get-gateway}
|
||||
* @param options - The options for fetching the gateway information
|
||||
*/
|
||||
public async get({ signal }: Pick<RequestData, 'signal'> = {}) {
|
||||
return this.rest.get(Routes.gateway(), {
|
||||
auth: false,
|
||||
signal,
|
||||
}) as Promise<RESTGetAPIGatewayResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets gateway information with additional metadata.
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/events/gateway#get-gateway-bot}
|
||||
* @param options - The options for fetching the gateway information
|
||||
*/
|
||||
public async getBot({ signal }: Pick<RequestData, 'signal'> = {}) {
|
||||
return this.rest.get(Routes.gatewayBot(), { signal }) as Promise<RESTGetAPIGatewayBotResult>;
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,8 @@ import {
|
||||
type RESTPostAPIGuildsMFAResult,
|
||||
type RESTPostAPIGuildsResult,
|
||||
type RESTPutAPIGuildBanJSONBody,
|
||||
type RESTPutAPIGuildIncidentActionsJSONBody,
|
||||
type RESTPutAPIGuildIncidentActionsResult,
|
||||
type RESTPutAPIGuildMemberJSONBody,
|
||||
type RESTPutAPIGuildMemberResult,
|
||||
type RESTPutAPIGuildOnboardingJSONBody,
|
||||
@@ -1359,4 +1361,23 @@ export class GuildsAPI {
|
||||
signal,
|
||||
}) as Promise<RESTPutAPIGuildOnboardingResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies incident actions for a guild.
|
||||
*
|
||||
* @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-incident-actions}
|
||||
* @param guildId - The id of the guild
|
||||
* @param body - The data for modifying guild incident actions
|
||||
* @param options - The options for modifying guild incident actions
|
||||
*/
|
||||
public async editIncidentActions(
|
||||
guildId: Snowflake,
|
||||
body: RESTPutAPIGuildIncidentActionsJSONBody,
|
||||
{ signal }: Pick<RequestData, 'signal'> = {},
|
||||
) {
|
||||
return this.rest.put(Routes.guildIncidentActions(guildId), {
|
||||
body,
|
||||
signal,
|
||||
}) as Promise<RESTPutAPIGuildIncidentActionsResult>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { REST } from '@discordjs/rest';
|
||||
import { ApplicationCommandsAPI } from './applicationCommands.js';
|
||||
import { ApplicationsAPI } from './applications.js';
|
||||
import { ChannelsAPI } from './channel.js';
|
||||
import { GatewayAPI } from './gateway.js';
|
||||
import { GuildsAPI } from './guild.js';
|
||||
import { InteractionsAPI } from './interactions.js';
|
||||
import { InvitesAPI } from './invite.js';
|
||||
@@ -19,6 +20,7 @@ import { WebhooksAPI } from './webhook.js';
|
||||
export * from './applicationCommands.js';
|
||||
export * from './applications.js';
|
||||
export * from './channel.js';
|
||||
export * from './gateway.js';
|
||||
export * from './guild.js';
|
||||
export * from './interactions.js';
|
||||
export * from './invite.js';
|
||||
@@ -40,6 +42,8 @@ export class API {
|
||||
|
||||
public readonly channels: ChannelsAPI;
|
||||
|
||||
public readonly gateway: GatewayAPI;
|
||||
|
||||
public readonly guilds: GuildsAPI;
|
||||
|
||||
public readonly interactions: InteractionsAPI;
|
||||
@@ -70,6 +74,7 @@ export class API {
|
||||
this.applicationCommands = new ApplicationCommandsAPI(rest);
|
||||
this.applications = new ApplicationsAPI(rest);
|
||||
this.channels = new ChannelsAPI(rest);
|
||||
this.gateway = new GatewayAPI(rest);
|
||||
this.guilds = new GuildsAPI(rest);
|
||||
this.invites = new InvitesAPI(rest);
|
||||
this.monetization = new MonetizationAPI(rest);
|
||||
|
||||
@@ -2,6 +2,84 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# [14.24.2](https://github.com/discordjs/discord.js/compare/14.24.1...14.24.2) - (2025-10-30)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **GuildMember:** JoinedAt possibly being NaN ([fb2b728](https://github.com/discordjs/discord.js/commit/fb2b7281e019de9dbd1eb307d9a2ed655c165187))
|
||||
|
||||
# [14.24.1](https://github.com/discordjs/discord.js/compare/14.24.0...14.24.1) - (2025-10-28)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Message:** Check if in voice based channel for `pinnable` (#11215) ([c2c8cce](https://github.com/discordjs/discord.js/commit/c2c8cce1d77d7afb9da3b0c6a1ee5787e922ec3c))
|
||||
|
||||
## Documentation
|
||||
|
||||
- **GuildMemberFlagsBitField:** Remove duplicate word ([abb84ce](https://github.com/discordjs/discord.js/commit/abb84ce88f7b9586740855085bb5abc6f0a6282c))
|
||||
|
||||
## Typings
|
||||
|
||||
- **FileUploadModalData:** Correct fields (#11209) ([d317ca1](https://github.com/discordjs/discord.js/commit/d317ca1053734d6fed651e1e8600750e4d8d16d4))
|
||||
- **LabelModalData:** Singular `ModalData` (#11207) ([072fbb2](https://github.com/discordjs/discord.js/commit/072fbb228a096e8cfb2a1f55c6170f68bc84345d))
|
||||
- **FileUploadComponentData:** `boolean` ([548c254](https://github.com/discordjs/discord.js/commit/548c25488a832f8aa274e7834ac57ad9c3e23890))
|
||||
|
||||
# [14.24.0](https://github.com/discordjs/discord.js/compare/14.23.2...14.24.0) - (2025-10-24)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Message:** Update `pinnable` to check for migrated guilds (#11189) ([ee988e3](https://github.com/discordjs/discord.js/commit/ee988e3e75d39e91a98a572e72a5981e0ef87dbc))
|
||||
|
||||
## Features
|
||||
|
||||
- Handle file upload component for v14 (#11179) ([104ad75](https://github.com/discordjs/discord.js/commit/104ad754f36933276f3acfd4164f7f19d50dfe2e))
|
||||
|
||||
# [14.23.2](https://github.com/discordjs/discord.js/compare/14.23.1...14.23.2) - (2025-10-09)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **ModalSubmitInteraction:** Better resolving of components (#11162) ([5cc13b7](https://github.com/discordjs/discord.js/commit/5cc13b735c78384a3488da527985cded92f67d41))
|
||||
- Handle DM modals ([1e4d1dc](https://github.com/discordjs/discord.js/commit/1e4d1dc04f7dabfb0575441957a6278675f02871))
|
||||
|
||||
# [14.23.1](https://github.com/discordjs/discord.js/compare/14.23.0...14.23.1) - (2025-10-08)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **ModalSubmitInteraction:** Resolve crash on handling populated select menus (#11158) ([11b236f](https://github.com/discordjs/discord.js/commit/11b236ff6539f91f11caa3d5a2cc7ae23070aaec))
|
||||
- Ending uncached polls (#11157) ([1d5b983](https://github.com/discordjs/discord.js/commit/1d5b9837de4036ca6f07f22f714f534463cc35ec))
|
||||
|
||||
# [14.23.0](https://github.com/discordjs/discord.js/compare/14.22.1...14.23.0) - (2025-10-08)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **ThreadMemberFlagsBitField:** Use `ThreadMemberFlags` enum in `Flags` (#11118) ([154c00d](https://github.com/discordjs/discord.js/commit/154c00ded932109c59ff0759609424fcb95140a0))
|
||||
- Backport in operator fix from main (#11127) ([fcce0d9](https://github.com/discordjs/discord.js/commit/fcce0d95bb6cd415f40f9f7a052e01ddcf625ed0))
|
||||
- Ensure discriminator detection respects webhooks too (#11062) ([d8ad181](https://github.com/discordjs/discord.js/commit/d8ad181c191e3a908e3c8e133ccb1d961d9d79e0))
|
||||
|
||||
## Documentation
|
||||
|
||||
- Use LocalizationMap where applicable (#11117) ([3b92744](https://github.com/discordjs/discord.js/commit/3b927449ae728175f04d67376642b20ba4a93069))
|
||||
- **GuildEditOptions:** Deprecate owner property ([fe025c0](https://github.com/discordjs/discord.js/commit/fe025c0a9f722c6225fff6501e9b3981cfe134ba))
|
||||
- Deprecate API related to guild ownership (#11054) ([3dd57c2](https://github.com/discordjs/discord.js/commit/3dd57c2eaf220b08f2b6f6562c34acf8524b5b17))
|
||||
- Deprecate setting owner ([740da4c](https://github.com/discordjs/discord.js/commit/740da4ce5e189391c7a0904da32a96fe1c8534e6))
|
||||
|
||||
## Features
|
||||
|
||||
- Bump builders in v14 (and fix runtime crashes) (#11153) ([67c8953](https://github.com/discordjs/discord.js/commit/67c8953a10d150074ba848cd8bfb30961d46b662))
|
||||
- **GuildMemberManager:** Add new modify self fields (#11112) ([9b821e5](https://github.com/discordjs/discord.js/commit/9b821e5dfcfb92a9d23ef96dd947c0bd11ee7b86))
|
||||
- Text display and more selects in modal for v14 (#11096) ([93e0f4c](https://github.com/discordjs/discord.js/commit/93e0f4cd10af6d85ccdcb6a6aeae3e1a9f14a8fe))
|
||||
- Guest invites (#11079) ([79d999e](https://github.com/discordjs/discord.js/commit/79d999e4c10e36330ee897065987ad99d558edca))
|
||||
- Polls overhaul (#11058) ([4a8aeb6](https://github.com/discordjs/discord.js/commit/4a8aeb6aee78b23a25e8d5be1309cc7c64b066fb))
|
||||
|
||||
## Refactor
|
||||
|
||||
- **ActionsManager:** Register actions without using class name (#11080) ([0dff969](https://github.com/discordjs/discord.js/commit/0dff969e16a8879a0fc889567bd540cb1b82a682))
|
||||
|
||||
## Typings
|
||||
|
||||
- **ClientEventTypes:** Fix `messageDeleteBulk` event arg (#11122) ([30e35d9](https://github.com/discordjs/discord.js/commit/30e35d909e0058db701c82744b13da26ddefcf0e))
|
||||
- **Webhook:** Specify message type (#11142) ([6a5707c](https://github.com/discordjs/discord.js/commit/6a5707c78669bb65d03ae76ab591e053787891f1))
|
||||
|
||||
# [14.22.1](https://github.com/discordjs/discord.js/compare/14.22.0...14.22.1) - (2025-08-22)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "discord.js",
|
||||
"version": "14.22.1",
|
||||
"version": "14.24.2",
|
||||
"description": "A powerful library for interacting with the Discord API",
|
||||
"scripts": {
|
||||
"test": "pnpm run docs:test && pnpm run test:typescript",
|
||||
@@ -66,14 +66,14 @@
|
||||
"homepage": "https://discord.js.org",
|
||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.11.2",
|
||||
"@discordjs/builders": "workspace:^",
|
||||
"@discordjs/collection": "1.5.3",
|
||||
"@discordjs/formatters": "^0.6.1",
|
||||
"@discordjs/formatters": "workspace:^",
|
||||
"@discordjs/rest": "workspace:^",
|
||||
"@discordjs/util": "workspace:^",
|
||||
"@discordjs/ws": "^1.2.3",
|
||||
"@sapphire/snowflake": "3.5.3",
|
||||
"discord-api-types": "^0.38.24",
|
||||
"discord-api-types": "^0.38.32",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
|
||||
@@ -168,6 +168,8 @@
|
||||
|
||||
* @property {'ModalSubmitInteractionFieldNotFound'} ModalSubmitInteractionFieldNotFound
|
||||
* @property {'ModalSubmitInteractionFieldType'} ModalSubmitInteractionFieldType
|
||||
* @property {'ModalSubmitInteractionFieldEmpty'} ModalSubmitInteractionFieldEmpty
|
||||
* @property {'ModalSubmitInteractionFieldInvalidChannelType'} ModalSubmitInteractionFieldInvalidChannelType
|
||||
|
||||
* @property {'InvalidMissingScopes'} InvalidMissingScopes
|
||||
* @property {'InvalidScopesWithPermissions'} InvalidScopesWithPermissions
|
||||
@@ -327,6 +329,8 @@ const keys = [
|
||||
|
||||
'ModalSubmitInteractionFieldNotFound',
|
||||
'ModalSubmitInteractionFieldType',
|
||||
'ModalSubmitInteractionFieldEmpty',
|
||||
'ModalSubmitInteractionFieldInvalidChannelType',
|
||||
|
||||
'InvalidMissingScopes',
|
||||
'InvalidScopesWithPermissions',
|
||||
|
||||
@@ -161,6 +161,10 @@ const Messages = {
|
||||
`Required field with custom id "${customId}" not found.`,
|
||||
[DjsErrorCodes.ModalSubmitInteractionFieldType]: (customId, type, expected) =>
|
||||
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
|
||||
[DjsErrorCodes.ModalSubmitInteractionFieldEmpty]: (customId, type) =>
|
||||
`Required field with custom id "${customId}" is of type: ${type}; expected a non-empty value.`,
|
||||
[DjsErrorCodes.ModalSubmitInteractionFieldInvalidChannelType]: (customId, type, expected) =>
|
||||
`The type of channel of the field with custom id "${customId}" is: ${type}; expected ${expected}.`,
|
||||
|
||||
[DjsErrorCodes.InvalidMissingScopes]: 'At least one valid scope must be provided for the invite',
|
||||
[DjsErrorCodes.InvalidScopesWithPermissions]: 'Permissions cannot be set without the bot scope.',
|
||||
|
||||
@@ -162,6 +162,7 @@ exports.InteractionWebhook = require('./structures/InteractionWebhook');
|
||||
exports.Invite = require('./structures/Invite');
|
||||
exports.InviteStageInstance = require('./structures/InviteStageInstance');
|
||||
exports.InviteGuild = require('./structures/InviteGuild');
|
||||
exports.LabelComponent = require('./structures/LabelComponent');
|
||||
exports.Message = require('./structures/Message').Message;
|
||||
exports.Attachment = require('./structures/Attachment');
|
||||
exports.AttachmentBuilder = require('./structures/AttachmentBuilder');
|
||||
|
||||
@@ -120,12 +120,12 @@ class GuildBanManager extends CachedManager {
|
||||
return this._add(data, cache);
|
||||
}
|
||||
|
||||
async _fetchMany(options = {}) {
|
||||
async _fetchMany({ cache, ...apiOptions } = {}) {
|
||||
const data = await this.client.rest.get(Routes.guildBans(this.guild.id), {
|
||||
query: makeURLSearchParams(options),
|
||||
query: makeURLSearchParams(apiOptions),
|
||||
});
|
||||
|
||||
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, options.cache)), new Collection());
|
||||
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, cache)), new Collection());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,15 @@ class GuildEmojiRoleManager extends DataManager {
|
||||
* @readonly
|
||||
*/
|
||||
get cache() {
|
||||
return this.guild.roles.cache.filter(role => this.emoji._roles.includes(role.id));
|
||||
const cache = new Collection();
|
||||
for (const roleId of this.emoji._roles) {
|
||||
const role = this.guild.roles.cache.get(roleId);
|
||||
if (role !== undefined) {
|
||||
cache.set(roleId, role);
|
||||
}
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { process } = require('node:process');
|
||||
const { setTimeout, clearTimeout } = require('node:timers');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
@@ -10,10 +11,13 @@ const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } =
|
||||
const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel');
|
||||
const { GuildMember } = require('../structures/GuildMember');
|
||||
const { Role } = require('../structures/Role');
|
||||
const { resolveImage } = require('../util/DataResolver');
|
||||
const Events = require('../util/Events');
|
||||
const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField');
|
||||
const Partials = require('../util/Partials');
|
||||
|
||||
let deprecatedEmittedForEditSoleNickname = false;
|
||||
|
||||
/**
|
||||
* Manages API methods for GuildMembers and stores their cache.
|
||||
* @extends {CachedManager}
|
||||
@@ -336,8 +340,8 @@ class GuildMemberManager extends CachedManager {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits a member of the guild.
|
||||
* <info>The user must be a member of the guild</info>
|
||||
* Edits a member of a guild.
|
||||
*
|
||||
* @param {UserResolvable} user The member to edit
|
||||
* @param {GuildMemberEditOptions} options The options to provide
|
||||
* @returns {Promise<GuildMember>}
|
||||
@@ -372,13 +376,30 @@ class GuildMemberManager extends CachedManager {
|
||||
}
|
||||
|
||||
let endpoint;
|
||||
|
||||
if (id === this.client.user.id) {
|
||||
const keys = Object.keys(options);
|
||||
if (keys.length === 1 && keys[0] === 'nick') endpoint = Routes.guildMember(this.guild.id);
|
||||
else endpoint = Routes.guildMember(this.guild.id, id);
|
||||
} else {
|
||||
endpoint = Routes.guildMember(this.guild.id, id);
|
||||
|
||||
if (keys.length === 1 && keys[0] === 'nick') {
|
||||
// For modifying the current application's nickname only, we use the /guilds/{guild.id}/members/@me endpoint.
|
||||
// This endpoint only requires the CHANGE_NICKNAME permission.
|
||||
// The other endpoint would require the MANAGE_NICKNAMES permission.
|
||||
// In v15, this will be split out, so emit a deprecation.
|
||||
endpoint = Routes.guildMember(this.guild.id, '@me');
|
||||
|
||||
if (!deprecatedEmittedForEditSoleNickname) {
|
||||
process.emitWarning(
|
||||
// eslint-disable-next-line max-len
|
||||
"You should use GuildMemberManager#editMe() when changing your nickname. Due to Discord's API changes, GuildMemberManager#edit() will end up requiring MANAGE_NICKNAMES in v15.",
|
||||
'DeprecationWarning',
|
||||
);
|
||||
|
||||
deprecatedEmittedForEditSoleNickname = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpoint ??= Routes.guildMember(this.guild.id, id);
|
||||
const d = await this.client.rest.patch(endpoint, { body: options, reason });
|
||||
|
||||
const clone = this.cache.get(id)?._clone();
|
||||
@@ -386,6 +407,38 @@ class GuildMemberManager extends CachedManager {
|
||||
return clone ?? this._add(d, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* The data for editing the current application's guild member.
|
||||
*
|
||||
* @typedef {Object} GuildMemberEditMeOptions
|
||||
* @property {?string} [nick] The nickname to set
|
||||
* @property {?(BufferResolvable|Base64Resolvable)} [banner] The banner to set
|
||||
* @property {?(BufferResolvable|Base64Resolvable)} [avatar] The avatar to set
|
||||
* @property {?string} [bio] The bio to set
|
||||
* @property {string} [reason] The reason to use
|
||||
*/
|
||||
|
||||
/**
|
||||
* Edits the current application's guild member in a guild.
|
||||
*
|
||||
* @param {GuildMemberEditMeOptions} options The options to provide
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
async editMe({ reason, ...options }) {
|
||||
const data = await this.client.rest.patch(Routes.guildMember(this.guild.id, '@me'), {
|
||||
body: {
|
||||
...options,
|
||||
banner: options.banner && (await resolveImage(options.banner)),
|
||||
avatar: options.avatar && (await resolveImage(options.avatar)),
|
||||
},
|
||||
reason,
|
||||
});
|
||||
|
||||
const clone = this.me?._clone();
|
||||
clone?._patch(data);
|
||||
return clone ?? this._add(data, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options used for pruning guild members.
|
||||
* <info>It's recommended to set {@link GuildPruneMembersOptions#count options.count}
|
||||
|
||||
@@ -33,8 +33,17 @@ class GuildMemberRoleManager extends DataManager {
|
||||
* @readonly
|
||||
*/
|
||||
get cache() {
|
||||
const everyone = this.guild.roles.everyone;
|
||||
return this.guild.roles.cache.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone);
|
||||
const cache = new Collection();
|
||||
cache.set(this.guild.id, this.guild.roles.everyone);
|
||||
|
||||
for (const roleId of this.member._roles) {
|
||||
const role = this.guild.roles.cache.get(roleId);
|
||||
if (role !== undefined) {
|
||||
cache.set(roleId, role);
|
||||
}
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,12 +111,12 @@ class MessageManager extends CachedManager {
|
||||
return this._add(data, cache);
|
||||
}
|
||||
|
||||
async _fetchMany(options = {}) {
|
||||
async _fetchMany({ cache, ...apiOptions } = {}) {
|
||||
const data = await this.client.rest.get(Routes.channelMessages(this.channel.id), {
|
||||
query: makeURLSearchParams(options),
|
||||
query: makeURLSearchParams(apiOptions),
|
||||
});
|
||||
|
||||
return data.reduce((_data, message) => _data.set(message.id, this._add(message, options.cache)), new Collection());
|
||||
return data.reduce((_data, message) => _data.set(message.id, this._add(message, cache)), new Collection());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,11 +158,11 @@ class MessageManager extends CachedManager {
|
||||
* .then(messages => console.log(`Received ${messages.items.length} messages`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async fetchPins(options = {}) {
|
||||
async fetchPins({ cache, ...apiOptions } = {}) {
|
||||
const data = await this.client.rest.get(Routes.channelMessagesPins(this.channel.id), {
|
||||
query: makeURLSearchParams({
|
||||
...options,
|
||||
before: options.before && new Date(options.before).toISOString(),
|
||||
...apiOptions,
|
||||
before: apiOptions.before && new Date(apiOptions.before).toISOString(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -172,7 +172,7 @@ class MessageManager extends CachedManager {
|
||||
get pinnedAt() {
|
||||
return new Date(this.pinnedTimestamp);
|
||||
},
|
||||
message: this._add(item.message, options.cache),
|
||||
message: this._add(item.message, cache),
|
||||
})),
|
||||
hasMore: data.has_more,
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ class ApplicationCommand extends Base {
|
||||
if ('name_localizations' in data) {
|
||||
/**
|
||||
* The name localizations for this command
|
||||
* @type {?Object<Locale, string>}
|
||||
* @type {?LocalizationMap}
|
||||
*/
|
||||
this.nameLocalizations = data.name_localizations;
|
||||
} else {
|
||||
@@ -101,7 +101,7 @@ class ApplicationCommand extends Base {
|
||||
if ('description_localizations' in data) {
|
||||
/**
|
||||
* The description localizations for this command
|
||||
* @type {?Object<Locale, string>}
|
||||
* @type {?LocalizationMap}
|
||||
*/
|
||||
this.descriptionLocalizations = data.description_localizations;
|
||||
} else {
|
||||
@@ -227,11 +227,11 @@ class ApplicationCommand extends Base {
|
||||
* @typedef {Object} ApplicationCommandData
|
||||
* @property {string} name The name of the command, must be in all lowercase if type is
|
||||
* {@link ApplicationCommandType.ChatInput}
|
||||
* @property {Object<Locale, string>} [nameLocalizations] The localizations for the command name
|
||||
* @property {LocalizationMap} [nameLocalizations] The localizations for the command name
|
||||
* @property {string} description The description of the command,
|
||||
* if type is {@link ApplicationCommandType.ChatInput} or {@link ApplicationCommandType.PrimaryEntryPoint}
|
||||
* @property {boolean} [nsfw] Whether the command is age-restricted
|
||||
* @property {Object<Locale, string>} [descriptionLocalizations] The localizations for the command description,
|
||||
* @property {LocalizationMap} [descriptionLocalizations] The localizations for the command description,
|
||||
* if type is {@link ApplicationCommandType.ChatInput} or {@link ApplicationCommandType.PrimaryEntryPoint}
|
||||
* @property {ApplicationCommandType} [type=ApplicationCommandType.ChatInput] The type of the command
|
||||
* @property {ApplicationCommandOptionData[]} [options] Options for the command
|
||||
@@ -253,9 +253,9 @@ class ApplicationCommand extends Base {
|
||||
* @typedef {Object} ApplicationCommandOptionData
|
||||
* @property {ApplicationCommandOptionType} type The type of the option
|
||||
* @property {string} name The name of the option
|
||||
* @property {Object<Locale, string>} [nameLocalizations] The name localizations for the option
|
||||
* @property {LocalizationMap} [nameLocalizations] The name localizations for the option
|
||||
* @property {string} description The description of the option
|
||||
* @property {Object<Locale, string>} [descriptionLocalizations] The description localizations for the option
|
||||
* @property {LocalizationMap} [descriptionLocalizations] The description localizations for the option
|
||||
* @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a
|
||||
* {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or
|
||||
* {@link ApplicationCommandOptionType.Number} option
|
||||
@@ -277,7 +277,7 @@ class ApplicationCommand extends Base {
|
||||
/**
|
||||
* @typedef {Object} ApplicationCommandOptionChoiceData
|
||||
* @property {string} name The name of the choice
|
||||
* @property {Object<Locale, string>} [nameLocalizations] The localized names for this choice
|
||||
* @property {LocalizationMap} [nameLocalizations] The localized names for this choice
|
||||
* @property {string|number} value The value of the choice
|
||||
*/
|
||||
|
||||
@@ -308,7 +308,7 @@ class ApplicationCommand extends Base {
|
||||
|
||||
/**
|
||||
* Edits the localized names of this ApplicationCommand
|
||||
* @param {Object<Locale, string>} nameLocalizations The new localized names for the command
|
||||
* @param {LocalizationMap} nameLocalizations The new localized names for the command
|
||||
* @returns {Promise<ApplicationCommand>}
|
||||
* @example
|
||||
* // Edit the name localizations of this command
|
||||
@@ -334,7 +334,7 @@ class ApplicationCommand extends Base {
|
||||
|
||||
/**
|
||||
* Edits the localized descriptions of this ApplicationCommand
|
||||
* @param {Object<Locale, string>} descriptionLocalizations The new localized descriptions for the command
|
||||
* @param {LocalizationMap} descriptionLocalizations The new localized descriptions for the command
|
||||
* @returns {Promise<ApplicationCommand>}
|
||||
* @example
|
||||
* // Edit the description localizations of this command
|
||||
@@ -550,10 +550,10 @@ class ApplicationCommand extends Base {
|
||||
* @typedef {Object} ApplicationCommandOption
|
||||
* @property {ApplicationCommandOptionType} type The type of the option
|
||||
* @property {string} name The name of the option
|
||||
* @property {Object<Locale, string>} [nameLocalizations] The localizations for the option name
|
||||
* @property {LocalizationMap} [nameLocalizations] The localizations for the option name
|
||||
* @property {string} [nameLocalized] The localized name for this option
|
||||
* @property {string} description The description of the option
|
||||
* @property {Object<Locale, string>} [descriptionLocalizations] The localizations for the option description
|
||||
* @property {LocalizationMap} [descriptionLocalizations] The localizations for the option description
|
||||
* @property {string} [descriptionLocalized] The localized description for this option
|
||||
* @property {boolean} [required] Whether the option is required
|
||||
* @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a
|
||||
|
||||
@@ -13,7 +13,7 @@ class ApplicationRoleConnectionMetadata {
|
||||
|
||||
/**
|
||||
* The name localizations for this metadata field
|
||||
* @type {?Object<Locale, string>}
|
||||
* @type {?LocalizationMap}
|
||||
*/
|
||||
this.nameLocalizations = data.name_localizations ?? null;
|
||||
|
||||
@@ -25,7 +25,7 @@ class ApplicationRoleConnectionMetadata {
|
||||
|
||||
/**
|
||||
* The description localizations for this metadata field
|
||||
* @type {?Object<Locale, string>}
|
||||
* @type {?LocalizationMap}
|
||||
*/
|
||||
this.descriptionLocalizations = data.description_localizations ?? null;
|
||||
|
||||
|
||||
@@ -375,9 +375,9 @@ class ClientApplication extends Application {
|
||||
* Data for creating or editing an application role connection metadata.
|
||||
* @typedef {Object} ApplicationRoleConnectionMetadataEditOptions
|
||||
* @property {string} name The name of the metadata field
|
||||
* @property {?Object<Locale, string>} [nameLocalizations] The name localizations for the metadata field
|
||||
* @property {?LocalizationMap} [nameLocalizations] The name localizations for the metadata field
|
||||
* @property {string} description The description of the metadata field
|
||||
* @property {?Object<Locale, string>} [descriptionLocalizations] The description localizations for the metadata field
|
||||
* @property {?LocalizationMap} [descriptionLocalizations] The description localizations for the metadata field
|
||||
* @property {string} key The dictionary key of the metadata field
|
||||
* @property {ApplicationRoleConnectionMetadataType} type The type of the metadata field
|
||||
*/
|
||||
|
||||
@@ -80,16 +80,21 @@ class CommandInteraction extends BaseInteraction {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the resolved data of a received command interaction.
|
||||
* @typedef {Object} CommandInteractionResolvedData
|
||||
* @typedef {Object} BaseInteractionResolvedData
|
||||
* @property {Collection<Snowflake, User>} [users] The resolved users
|
||||
* @property {Collection<Snowflake, GuildMember|APIGuildMember>} [members] The resolved guild members
|
||||
* @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles
|
||||
* @property {Collection<Snowflake, BaseChannel|APIChannel>} [channels] The resolved channels
|
||||
* @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages
|
||||
* @property {Collection<Snowflake, Attachment>} [attachments] The resolved attachments
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the resolved data of a received command interaction.
|
||||
*
|
||||
* @typedef {BaseInteractionResolvedData} CommandInteractionResolvedData
|
||||
* @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents an option of a received command interaction.
|
||||
* @typedef {Object} CommandInteractionOption
|
||||
|
||||
@@ -24,12 +24,6 @@ class GuildMember extends Base {
|
||||
*/
|
||||
this.guild = guild;
|
||||
|
||||
/**
|
||||
* The timestamp the member joined the guild at
|
||||
* @type {?number}
|
||||
*/
|
||||
this.joinedTimestamp = null;
|
||||
|
||||
/**
|
||||
* The last timestamp this member started boosting the guild
|
||||
* @type {?number}
|
||||
@@ -95,7 +89,17 @@ class GuildMember extends Base {
|
||||
this.banner ??= null;
|
||||
}
|
||||
|
||||
if ('joined_at' in data) this.joinedTimestamp = Date.parse(data.joined_at);
|
||||
if ('joined_at' in data) {
|
||||
/**
|
||||
* The timestamp the member joined the guild at
|
||||
*
|
||||
* @type {?number}
|
||||
*/
|
||||
this.joinedTimestamp = data.joined_at && Date.parse(data.joined_at);
|
||||
} else {
|
||||
this.joinedTimestamp ??= null;
|
||||
}
|
||||
|
||||
if ('premium_since' in data) {
|
||||
this.premiumSinceTimestamp = data.premium_since ? Date.parse(data.premium_since) : null;
|
||||
}
|
||||
@@ -422,7 +426,9 @@ class GuildMember extends Base {
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setNickname(nick, reason) {
|
||||
return this.edit({ nick, reason });
|
||||
return this.user.id === this.client.user.id
|
||||
? this.guild.members.editMe({ nick, reason })
|
||||
: this.edit({ nick, reason });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
54
packages/discord.js/src/structures/LabelComponent.js
Normal file
54
packages/discord.js/src/structures/LabelComponent.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
const Component = require('./Component');
|
||||
const { createComponent } = require('../util/Components');
|
||||
|
||||
/**
|
||||
* Represents a label component
|
||||
*
|
||||
* @extends {Component}
|
||||
*/
|
||||
class LabelComponent extends Component {
|
||||
constructor({ component, ...data }) {
|
||||
super(data);
|
||||
|
||||
/**
|
||||
* The component in this label
|
||||
*
|
||||
* @type {Component}
|
||||
* @readonly
|
||||
*/
|
||||
this.component = createComponent(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* The label of the component
|
||||
*
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
get label() {
|
||||
return this.data.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* The description of this component
|
||||
*
|
||||
* @type {?string}
|
||||
* @readonly
|
||||
*/
|
||||
get description() {
|
||||
return this.data.description ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the API-compatible JSON for this component
|
||||
*
|
||||
* @returns {APILabelComponent}
|
||||
*/
|
||||
toJSON() {
|
||||
return { ...this.data, component: this.component.toJSON() };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LabelComponent;
|
||||
@@ -623,7 +623,7 @@ class Message extends Base {
|
||||
* Similar to createReactionCollector but in promise form.
|
||||
* Resolves with a collection of reactions that pass the specified filter.
|
||||
* @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector
|
||||
* @returns {Promise<Collection<string | Snowflake, MessageReaction>>}
|
||||
* @returns {Promise<Collection<string|Snowflake, MessageReaction>>}
|
||||
* @example
|
||||
* // Create a reaction collector
|
||||
* const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId'
|
||||
@@ -781,11 +781,16 @@ class Message extends Base {
|
||||
*/
|
||||
get pinnable() {
|
||||
const { channel } = this;
|
||||
return Boolean(
|
||||
!this.system &&
|
||||
(!this.guild ||
|
||||
(channel?.viewable &&
|
||||
channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))),
|
||||
if (this.system) return false;
|
||||
if (!this.guild) return true;
|
||||
if (!channel || channel.isVoiceBased() || !channel.viewable) return false;
|
||||
|
||||
const permissions = channel?.permissionsFor(this.client.user);
|
||||
if (!permissions) return false;
|
||||
|
||||
return (
|
||||
permissions.has(PermissionFlagsBits.ReadMessageHistory | PermissionFlagsBits.PinMessages) ||
|
||||
permissions.has(PermissionFlagsBits.ReadMessageHistory | PermissionFlagsBits.ManageMessages)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,48 @@ const { Collection } = require('@discordjs/collection');
|
||||
const { ComponentType } = require('discord-api-types/v10');
|
||||
const { DiscordjsTypeError, ErrorCodes } = require('../errors');
|
||||
|
||||
/**
|
||||
* @typedef {Object} ModalSelectedMentionables
|
||||
* @property {Collection<Snowflake, User>} users The selected users
|
||||
* @property {Collection<Snowflake, GuildMember | APIGuildMember>} members The selected members
|
||||
* @property {Collection<Snowflake, Role | APIRole>} roles The selected roles
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the serialized fields from a modal submit interaction
|
||||
*/
|
||||
class ModalSubmitFields {
|
||||
constructor(components) {
|
||||
constructor(components, resolved) {
|
||||
/**
|
||||
* The components within the modal
|
||||
* @type {ActionRowModalData[]}
|
||||
*
|
||||
* @type {Array<ActionRowModalData|LabelModalData|TextDisplayModalData>}
|
||||
*/
|
||||
this.components = components;
|
||||
|
||||
/**
|
||||
* The interaction resolved data
|
||||
*
|
||||
* @name ModalSubmitFields#resolved
|
||||
* @type {?Readonly<BaseInteractionResolvedData>}
|
||||
*/
|
||||
Object.defineProperty(this, 'resolved', { value: resolved ? Object.freeze(resolved) : null });
|
||||
|
||||
/**
|
||||
* The extracted fields from the modal
|
||||
* @type {Collection<string, ModalData>}
|
||||
*/
|
||||
this.fields = components.reduce((accumulator, next) => {
|
||||
next.components.forEach(component => accumulator.set(component.customId, component));
|
||||
// For legacy support of action rows
|
||||
if ('components' in next) {
|
||||
for (const component of next.components) accumulator.set(component.customId, component);
|
||||
}
|
||||
|
||||
// For label components
|
||||
if ('component' in next) {
|
||||
accumulator.set(next.component.customId, next.component);
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}, new Collection());
|
||||
}
|
||||
@@ -42,13 +67,165 @@ class ModalSubmitFields {
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a component by custom id and property and checks its type.
|
||||
*
|
||||
* @param {string} customId The custom id of the component.
|
||||
* @param {ComponentType[]} allowedTypes The allowed types of the component.
|
||||
* @param {string[]} properties The properties to check for for `required`.
|
||||
* @param {boolean} required Whether to throw an error if the component value(s) are not found.
|
||||
* @returns {ModalData} The option, if found.
|
||||
* @private
|
||||
*/
|
||||
_getTypedComponent(customId, allowedTypes, properties, required) {
|
||||
const component = this.getField(customId);
|
||||
if (!allowedTypes.includes(component.type)) {
|
||||
throw new DiscordjsTypeError(
|
||||
ErrorCodes.ModalSubmitInteractionFieldNotFound,
|
||||
customId,
|
||||
component.type,
|
||||
allowedTypes.join(', '),
|
||||
);
|
||||
} else if (required && properties.every(prop => component[prop] === null || component[prop] === undefined)) {
|
||||
throw new DiscordjsTypeError(ErrorCodes.ModalSubmitInteractionFieldEmpty, customId, component.type);
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a text input component given a custom id
|
||||
* @param {string} customId The custom id of the text input component
|
||||
* @returns {string}
|
||||
*/
|
||||
getTextInputValue(customId) {
|
||||
return this.getField(customId, ComponentType.TextInput).value;
|
||||
return this._getTypedComponent(customId, [ComponentType.TextInput]).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the values of a string select component given a custom id
|
||||
*
|
||||
* @param {string} customId The custom id of the string select component
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getStringSelectValues(customId) {
|
||||
return this._getTypedComponent(customId, [ComponentType.StringSelect]).values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets users component
|
||||
*
|
||||
* @param {string} customId The custom id of the component
|
||||
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
|
||||
* @returns {?Collection<Snowflake, User>} The selected users, or null if none were selected and not required
|
||||
*/
|
||||
getSelectedUsers(customId, required = false) {
|
||||
const component = this._getTypedComponent(
|
||||
customId,
|
||||
[ComponentType.UserSelect, ComponentType.MentionableSelect],
|
||||
['users'],
|
||||
required,
|
||||
);
|
||||
return component.users ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets roles component
|
||||
*
|
||||
* @param {string} customId The custom id of the component
|
||||
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
|
||||
* @returns {?Collection<Snowflake, Role|APIRole>} The selected roles, or null if none were selected and not required
|
||||
*/
|
||||
getSelectedRoles(customId, required = false) {
|
||||
const component = this._getTypedComponent(
|
||||
customId,
|
||||
[ComponentType.RoleSelect, ComponentType.MentionableSelect],
|
||||
['roles'],
|
||||
required,
|
||||
);
|
||||
return component.roles ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets channels component
|
||||
*
|
||||
* @param {string} customId The custom id of the component
|
||||
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
|
||||
* @param {ChannelType[]} [channelTypes=[]] The allowed types of channels. If empty, all channel types are allowed.
|
||||
* @returns {?Collection<Snowflake, GuildChannel|ThreadChannel|APIChannel>} The selected channels,
|
||||
* or null if none were selected and not required
|
||||
*/
|
||||
getSelectedChannels(customId, required = false, channelTypes = []) {
|
||||
const component = this._getTypedComponent(customId, [ComponentType.ChannelSelect], ['channels'], required);
|
||||
const channels = component.channels;
|
||||
if (channels && channelTypes.length > 0) {
|
||||
for (const channel of channels.values()) {
|
||||
if (!channelTypes.includes(channel.type)) {
|
||||
throw new DiscordjsTypeError(
|
||||
ErrorCodes.ModalSubmitInteractionComponentInvalidChannelType,
|
||||
customId,
|
||||
channel.type,
|
||||
channelTypes.join(', '),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channels ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets members component
|
||||
*
|
||||
* @param {string} customId The custom id of the component
|
||||
* @returns {?Collection<Snowflake, GuildMember|APIGuildMember>} The selected members,
|
||||
* or null if none were selected or the users were not present in the guild
|
||||
*/
|
||||
getSelectedMembers(customId) {
|
||||
const component = this._getTypedComponent(
|
||||
customId,
|
||||
[ComponentType.UserSelect, ComponentType.MentionableSelect],
|
||||
['members'],
|
||||
false,
|
||||
);
|
||||
return component.members ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets mentionables component
|
||||
*
|
||||
* @param {string} customId The custom id of the component
|
||||
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
|
||||
* @returns {?ModalSelectedMentionables} The selected mentionables, or null if none were selected and not required
|
||||
*/
|
||||
getSelectedMentionables(customId, required = false) {
|
||||
const component = this._getTypedComponent(
|
||||
customId,
|
||||
[ComponentType.MentionableSelect],
|
||||
['users', 'members', 'roles'],
|
||||
required,
|
||||
);
|
||||
|
||||
if (component.users || component.members || component.roles) {
|
||||
return {
|
||||
users: component.users ?? new Collection(),
|
||||
members: component.members ?? new Collection(),
|
||||
roles: component.roles ?? new Collection(),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets file upload component
|
||||
*
|
||||
* @param {string} customId The custom id of the component
|
||||
* @param {boolean} [required=false] Whether to throw an error if the component value is not found or empty
|
||||
* @returns {?Collection<Snowflake, Attachment>} The uploaded files, or null if none were uploaded and not required
|
||||
*/
|
||||
getUploadedFiles(customId, required = false) {
|
||||
return this._getTypedComponent(customId, [ComponentType.FileUpload], ['attachments'], required).attachments ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { lazy } = require('@discordjs/util');
|
||||
const BaseInteraction = require('./BaseInteraction');
|
||||
const InteractionWebhook = require('./InteractionWebhook');
|
||||
const ModalSubmitFields = require('./ModalSubmitFields');
|
||||
const InteractionResponses = require('./interfaces/InteractionResponses');
|
||||
const { transformResolved } = require('../util/Util');
|
||||
|
||||
const getMessage = lazy(() => require('./Message').Message);
|
||||
const getAttachment = lazy(() => require('./Attachment'));
|
||||
|
||||
/**
|
||||
* @typedef {Object} ModalData
|
||||
* @property {string} value The value of the field
|
||||
* @typedef {Object} BaseModalData
|
||||
* @property {ComponentType} type The component type of the field
|
||||
* @property {string} customId The custom id of the field
|
||||
* @property {number} id The id of the field
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ActionRowModalData
|
||||
* @property {ModalData[]} components The components of this action row
|
||||
* @property {ComponentType} type The component type of the action row
|
||||
* @typedef {BaseModalData} FileUploadModalData
|
||||
* @property {string} customId The custom id of the file upload
|
||||
* @property {Snowflake[]} values The values of the file upload
|
||||
* @property {Collection<Snowflake, Attachment>} [attachments] The resolved attachments
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseModalData} TextInputModalData
|
||||
* @property {string} customId The custom id of the field
|
||||
* @property {string} value The value of the field
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseModalData} SelectMenuModalData
|
||||
* @property {string} customId The custom id of the field
|
||||
* @property {string[]} values The values of the field
|
||||
* @property {Collection<Snowflake, GuildMember|APIGuildMember>} [members] The resolved members
|
||||
* @property {Collection<Snowflake, User|APIUser>} [users] The resolved users
|
||||
* @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles
|
||||
* @property {Collection<Snowflake, BaseChannel|APIChannel>} [channels] The resolved channels
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseModalData} TextDisplayModalData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {SelectMenuModalData|TextInputModalData|FileUploadModalData} ModalData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseModalData} LabelModalData
|
||||
* @property {ModalData} component The component within the label
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseModalData} ActionRowModalData
|
||||
* @property {TextInputModalData[]} components The components of this action row
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -47,15 +84,24 @@ class ModalSubmitInteraction extends BaseInteraction {
|
||||
|
||||
/**
|
||||
* The components within the modal
|
||||
* @type {ActionRowModalData[]}
|
||||
*
|
||||
* @type {Array<ActionRowModalData | LabelModalData | TextDisplayModalData>}
|
||||
*/
|
||||
this.components = data.data.components?.map(component => ModalSubmitInteraction.transformComponent(component));
|
||||
this.components = data.data.components?.map(component =>
|
||||
ModalSubmitInteraction.transformComponent(component, data.data.resolved, {
|
||||
client: this.client,
|
||||
guild: this.guild,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* The fields within the modal
|
||||
* @type {ModalSubmitFields}
|
||||
*/
|
||||
this.fields = new ModalSubmitFields(this.components);
|
||||
this.fields = new ModalSubmitFields(
|
||||
this.components,
|
||||
transformResolved({ client: this.client, guild: this.guild, channel: this.channel }, data.data.resolved),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether the reply to this interaction has been deferred
|
||||
@@ -85,19 +131,102 @@ class ModalSubmitInteraction extends BaseInteraction {
|
||||
/**
|
||||
* Transforms component data to discord.js-compatible data
|
||||
* @param {*} rawComponent The data to transform
|
||||
* @param {APIInteractionDataResolved} [resolved] The resolved data for the interaction
|
||||
* @param {*} [extra] Extra data required for the transformation
|
||||
* @returns {ModalData[]}
|
||||
*/
|
||||
static transformComponent(rawComponent) {
|
||||
return rawComponent.components
|
||||
? {
|
||||
type: rawComponent.type,
|
||||
components: rawComponent.components.map(component => this.transformComponent(component)),
|
||||
static transformComponent(rawComponent, resolved, { client, guild } = {}) {
|
||||
if ('components' in rawComponent) {
|
||||
return {
|
||||
type: rawComponent.type,
|
||||
id: rawComponent.id,
|
||||
components: rawComponent.components.map(component =>
|
||||
this.transformComponent(component, resolved, { client, guild }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if ('component' in rawComponent) {
|
||||
return {
|
||||
type: rawComponent.type,
|
||||
id: rawComponent.id,
|
||||
component: this.transformComponent(rawComponent.component, resolved, { client, guild }),
|
||||
};
|
||||
}
|
||||
|
||||
const data = {
|
||||
type: rawComponent.type,
|
||||
id: rawComponent.id,
|
||||
};
|
||||
|
||||
// Text display components do not have custom ids.
|
||||
if ('custom_id' in rawComponent) data.customId = rawComponent.custom_id;
|
||||
|
||||
if ('value' in rawComponent) data.value = rawComponent.value;
|
||||
|
||||
if (rawComponent.values) {
|
||||
data.values = rawComponent.values;
|
||||
|
||||
/* eslint-disable max-depth */
|
||||
if (resolved) {
|
||||
const { members, users, channels, roles, attachments } = resolved;
|
||||
const valueSet = new Set(rawComponent.values);
|
||||
|
||||
if (users) {
|
||||
data.users = new Collection();
|
||||
|
||||
for (const [id, user] of Object.entries(users)) {
|
||||
if (valueSet.has(id)) {
|
||||
data.users.set(id, client.users._add(user));
|
||||
}
|
||||
}
|
||||
}
|
||||
: {
|
||||
value: rawComponent.value,
|
||||
type: rawComponent.type,
|
||||
customId: rawComponent.custom_id,
|
||||
};
|
||||
|
||||
if (channels) {
|
||||
data.channels = new Collection();
|
||||
|
||||
for (const [id, apiChannel] of Object.entries(channels)) {
|
||||
if (valueSet.has(id)) {
|
||||
data.channels.set(id, client.channels._add(apiChannel, guild) ?? apiChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (members) {
|
||||
data.members = new Collection();
|
||||
|
||||
for (const [id, member] of Object.entries(members)) {
|
||||
if (valueSet.has(id)) {
|
||||
const user = users?.[id];
|
||||
data.members.set(id, guild?.members._add({ user, ...member }) ?? member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (roles) {
|
||||
data.roles = new Collection();
|
||||
|
||||
for (const [id, role] of Object.entries(roles)) {
|
||||
if (valueSet.has(id)) {
|
||||
data.roles.set(id, guild?.roles._add(role) ?? role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attachments) {
|
||||
data.attachments = new Collection();
|
||||
for (const [id, attachment] of Object.entries(attachments)) {
|
||||
if (valueSet.has(id)) {
|
||||
data.attachments.set(id, new (getAttachment())(attachment));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable max-depth */
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -164,7 +164,7 @@ class Poll extends Base {
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
async end() {
|
||||
if (Date.now() > this.expiresTimestamp) {
|
||||
if (this.expiresTimestamp !== null && Date.now() > this.expiresTimestamp) {
|
||||
throw new DiscordjsError(ErrorCodes.PollAlreadyExpired);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,6 @@ const MessagePayload = require('../MessagePayload');
|
||||
let deprecationEmittedForEphemeralOption = false;
|
||||
let deprecationEmittedForFetchReplyOption = false;
|
||||
|
||||
/**
|
||||
* @typedef {Object} ModalComponentData
|
||||
* @property {string} title The title of the modal
|
||||
* @property {string} customId The custom id of the modal
|
||||
* @property {ActionRow[]} components The components within this modal
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface for classes that support shared interaction response types.
|
||||
* @interface
|
||||
|
||||
@@ -245,6 +245,11 @@
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuOption}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external APISelectMenuDefaultValue
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuDefaultValue}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external APISticker
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISticker}
|
||||
@@ -535,6 +540,11 @@
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/Locale}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external LocalizationMap
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#LocalizationMap}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external MessageActivityType
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/MessageActivityType}
|
||||
@@ -650,6 +660,11 @@
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ThreadAutoArchiveDuration}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external ThreadMemberFlags
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ThreadMemberFlags}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external UserFlags
|
||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/UserFlags}
|
||||
|
||||
@@ -14,6 +14,26 @@ const { ComponentType } = require('discord-api-types/v10');
|
||||
* @property {ComponentData[]} components The components in this action row
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ModalComponentData
|
||||
* @property {string} title The title of the modal
|
||||
* @property {string} customId The custom id of the modal
|
||||
* @property {Array<ActionRow|TextDisplayComponentData|LabelData>} components The components within this modal
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {StringSelectMenuComponentData|TextInputComponentData|UserSelectMenuComponentData|
|
||||
* RoleSelectMenuComponentData|MentionableSelectMenuComponentData|ChannelSelectMenuComponentData|
|
||||
* FileUploadComponentData} ComponentInLabelData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseComponentData} LabelData
|
||||
* @property {string} label The label to use
|
||||
* @property {string} [description] The optional description for the label
|
||||
* @property {ComponentInLabelData} component The component within the label
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseComponentData} ButtonComponentData
|
||||
* @property {ButtonStyle} style The style of the button
|
||||
@@ -24,6 +44,50 @@ const { ComponentType } = require('discord-api-types/v10');
|
||||
* @property {string} [url] The URL of the button
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseComponentData} FileUploadComponentData
|
||||
* @property {string} customId The custom id of the file upload
|
||||
* @property {number} [minValues] The minimum number of files that can be uploaded (0-10)
|
||||
* @property {number} [maxValues] The maximum number of files that can be uploaded (1-10)
|
||||
* @property {boolean} [required] Whether this component is required in modals
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseComponentData} BaseSelectMenuComponentData
|
||||
* @property {string} customId The custom id of the select menu
|
||||
* @property {boolean} [disabled] Whether the select menu is disabled or not
|
||||
* @property {number} [maxValues] The maximum amount of options that can be selected
|
||||
* @property {number} [minValues] The minimum amount of options that can be selected
|
||||
* @property {string} [placeholder] The placeholder of the select menu
|
||||
* @property {boolean} [required] Whether this component is required in modals
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseSelectMenuComponentData} StringSelectMenuComponentData
|
||||
* @property {SelectMenuComponentOptionData[]} [options] The options in this select menu
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseSelectMenuComponentData} UserSelectMenuComponentData
|
||||
* @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseSelectMenuComponentData} RoleSelectMenuComponentData
|
||||
* @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseSelectMenuComponentData} MentionableSelectMenuComponentData
|
||||
* @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseSelectMenuComponentData} ChannelSelectMenuComponentData
|
||||
* @property {APISelectMenuDefaultValue[]} [defaultValues] The default selected values in this select menu
|
||||
* @property {ChannelType[]} [channelTypes] The types of channels that can be selected
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SelectMenuComponentOptionData
|
||||
* @property {string} label The label of the option
|
||||
@@ -199,6 +263,7 @@ const ChannelSelectMenuComponent = require('../structures/ChannelSelectMenuCompo
|
||||
const Component = require('../structures/Component');
|
||||
const ContainerComponent = require('../structures/ContainerComponent');
|
||||
const FileComponent = require('../structures/FileComponent');
|
||||
const LabelComponent = require('../structures/LabelComponent');
|
||||
const MediaGalleryComponent = require('../structures/MediaGalleryComponent');
|
||||
const MentionableSelectMenuBuilder = require('../structures/MentionableSelectMenuBuilder');
|
||||
const MentionableSelectMenuComponent = require('../structures/MentionableSelectMenuComponent');
|
||||
@@ -231,6 +296,7 @@ const ComponentTypeToComponent = {
|
||||
[ComponentType.Section]: SectionComponent,
|
||||
[ComponentType.Separator]: SeparatorComponent,
|
||||
[ComponentType.Thumbnail]: ThumbnailComponent,
|
||||
[ComponentType.Label]: LabelComponent,
|
||||
};
|
||||
|
||||
const ComponentTypeToBuilder = {
|
||||
|
||||
@@ -9,7 +9,8 @@ const BitField = require('./BitField');
|
||||
*/
|
||||
class GuildMemberFlagsBitField extends BitField {
|
||||
/**
|
||||
* Numeric guild guild member flags.
|
||||
* Numeric guild member flags.
|
||||
*
|
||||
* @type {GuildMemberFlags}
|
||||
* @memberof GuildMemberFlagsBitField
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { InviteFlags } = require('discord-api-types/v10');
|
||||
const { BitField } = require('./BitField.js');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a {@link GuildInvite#flags} bit field.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { ThreadMemberFlags } = require('discord-api-types/v10');
|
||||
const BitField = require('./BitField');
|
||||
|
||||
/**
|
||||
@@ -9,10 +10,10 @@ const BitField = require('./BitField');
|
||||
class ThreadMemberFlagsBitField extends BitField {
|
||||
/**
|
||||
* Numeric thread member flags. There are currently no bitflags relevant to bots for this.
|
||||
* @type {Object<string, number>}
|
||||
* @type {ThreadMemberFlags}
|
||||
* @memberof ThreadMemberFlagsBitField
|
||||
*/
|
||||
static Flags = {};
|
||||
static Flags = ThreadMemberFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
208
packages/discord.js/typings/index.d.ts
vendored
208
packages/discord.js/typings/index.d.ts
vendored
@@ -37,7 +37,15 @@ import {
|
||||
} from '@discordjs/formatters';
|
||||
import { Awaitable, JSONEncodable } from '@discordjs/util';
|
||||
import { Collection, ReadonlyCollection } from '@discordjs/collection';
|
||||
import { BaseImageURLOptions, EmojiURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
|
||||
import {
|
||||
BaseImageURLOptions,
|
||||
EmojiURLOptions,
|
||||
ImageURLOptions,
|
||||
RawFile,
|
||||
REST,
|
||||
RESTOptions,
|
||||
ImageSize,
|
||||
} from '@discordjs/rest';
|
||||
import {
|
||||
WebSocketManager as WSWebSocketManager,
|
||||
IShardingStrategy,
|
||||
@@ -55,6 +63,7 @@ import {
|
||||
APIInteractionDataResolvedChannel,
|
||||
APIInteractionDataResolvedGuildMember,
|
||||
APIInteractionGuildMember,
|
||||
APILabelComponent,
|
||||
APIMessage,
|
||||
APIMessageComponent,
|
||||
APIOverwrite,
|
||||
@@ -352,6 +361,22 @@ export class ActionRowBuilder<
|
||||
): ActionRowBuilder<ComponentType>;
|
||||
}
|
||||
|
||||
export type ComponentInLabelData =
|
||||
| StringSelectMenuComponentData
|
||||
| TextInputComponentData
|
||||
| UserSelectMenuComponentData
|
||||
| ChannelSelectMenuComponentData
|
||||
| RoleSelectMenuComponentData
|
||||
| MentionableSelectMenuComponentData
|
||||
| FileUploadComponentData;
|
||||
|
||||
export interface LabelComponentData extends BaseComponentData {
|
||||
type: ComponentType.Label;
|
||||
component: ComponentInLabelData;
|
||||
description?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type MessageActionRowComponent =
|
||||
| ButtonComponent
|
||||
| StringSelectMenuComponent
|
||||
@@ -912,6 +937,12 @@ export class TextInputComponent extends Component<APITextInputComponent> {
|
||||
public get value(): string;
|
||||
}
|
||||
|
||||
export class LabelComponent extends Component<APILabelComponent> {
|
||||
public component: StringSelectMenuComponent | TextInputComponent;
|
||||
public get label(): string;
|
||||
public get description(): string | null;
|
||||
}
|
||||
|
||||
export class BaseSelectMenuComponent<Data extends APISelectMenuComponent> extends Component<Data> {
|
||||
protected constructor(data: Data);
|
||||
public get placeholder(): string | null;
|
||||
@@ -2757,33 +2788,127 @@ export interface ModalComponentData {
|
||||
customId: string;
|
||||
title: string;
|
||||
components: readonly (
|
||||
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow>>
|
||||
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow> | APILabelComponent>
|
||||
| ActionRowData<ModalActionRowComponentData>
|
||||
| LabelComponentData
|
||||
| TextDisplayComponentData
|
||||
)[];
|
||||
}
|
||||
|
||||
export interface BaseModalData {
|
||||
customId: string;
|
||||
type: ComponentType;
|
||||
export interface BaseModalData<Type extends ComponentType> {
|
||||
id: number;
|
||||
type: Type;
|
||||
}
|
||||
|
||||
export interface TextInputModalData extends BaseModalData {
|
||||
type: ComponentType.TextInput;
|
||||
export interface TextInputModalData extends BaseModalData<ComponentType.TextInput> {
|
||||
customId: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ActionRowModalData {
|
||||
type: ComponentType.ActionRow;
|
||||
export interface SelectMenuModalData<Cached extends CacheType = CacheType>
|
||||
extends BaseModalData<
|
||||
| ComponentType.ChannelSelect
|
||||
| ComponentType.MentionableSelect
|
||||
| ComponentType.RoleSelect
|
||||
| ComponentType.StringSelect
|
||||
| ComponentType.UserSelect
|
||||
> {
|
||||
channels?: ReadonlyCollection<
|
||||
Snowflake,
|
||||
CacheTypeReducer<Cached, GuildBasedChannel, APIInteractionDataResolvedChannel>
|
||||
>;
|
||||
customId: string;
|
||||
members?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, GuildMember, APIInteractionDataResolvedGuildMember>>;
|
||||
roles?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Role, APIRole>>;
|
||||
users?: ReadonlyCollection<Snowflake, User>;
|
||||
values: readonly string[];
|
||||
}
|
||||
|
||||
export interface FileUploadModalData extends BaseModalData<ComponentType.FileUpload> {
|
||||
customId: string;
|
||||
values: readonly Snowflake[];
|
||||
attachments: ReadonlyCollection<Snowflake, Attachment>;
|
||||
}
|
||||
|
||||
export type ModalData = FileUploadModalData | SelectMenuModalData | TextInputModalData;
|
||||
|
||||
export interface LabelModalData extends BaseModalData<ComponentType.Label> {
|
||||
component: ModalData;
|
||||
}
|
||||
export interface ActionRowModalData extends BaseModalData<ComponentType.ActionRow> {
|
||||
components: readonly TextInputModalData[];
|
||||
}
|
||||
|
||||
export class ModalSubmitFields {
|
||||
private constructor(components: readonly (readonly ModalActionRowComponent[])[]);
|
||||
public components: ActionRowModalData[];
|
||||
public fields: Collection<string, TextInputModalData>;
|
||||
public getField<Type extends ComponentType>(customId: string, type: Type): { type: Type } & TextInputModalData;
|
||||
public getField(customId: string, type?: ComponentType): TextInputModalData;
|
||||
export interface TextDisplayModalData extends BaseModalData<ComponentType.TextDisplay> {}
|
||||
|
||||
export interface ModalSelectedMentionables<Cached extends CacheType = CacheType> {
|
||||
members: NonNullable<SelectMenuModalData<Cached>['members']>;
|
||||
roles: NonNullable<SelectMenuModalData<Cached>['roles']>;
|
||||
users: NonNullable<SelectMenuModalData<Cached>['users']>;
|
||||
}
|
||||
|
||||
export class ModalSubmitFields<Cached extends CacheType = CacheType> {
|
||||
private constructor(
|
||||
components: readonly (ActionRowModalData | LabelModalData | TextDisplayModalData)[],
|
||||
resolved?: BaseInteractionResolvedData,
|
||||
);
|
||||
public components: (ActionRowModalData | LabelModalData | TextDisplayModalData)[];
|
||||
public resolved: Readonly<BaseInteractionResolvedData<Cached>> | null;
|
||||
public fields: Collection<string, ModalData>;
|
||||
public getField<Type extends ComponentType>(customId: string, type: Type): Extract<ModalData, { type: Type }>;
|
||||
public getField(customId: string, type?: ComponentType): ModalData;
|
||||
private _getTypedComponent(
|
||||
customId: string,
|
||||
allowedTypes: readonly ComponentType[],
|
||||
properties: string,
|
||||
required: boolean,
|
||||
): ModalData;
|
||||
public getTextInputValue(customId: string): string;
|
||||
public getStringSelectValues(customId: string): readonly string[];
|
||||
public getSelectedUsers(customId: string, required: true): ReadonlyCollection<Snowflake, User>;
|
||||
public getSelectedUsers(customId: string, required?: boolean): ReadonlyCollection<Snowflake, User> | null;
|
||||
public getSelectedMembers(customId: string): NonNullable<SelectMenuModalData<Cached>['members']> | null;
|
||||
public getSelectedChannels<const Type extends ChannelType = ChannelType>(
|
||||
customId: string,
|
||||
required: true,
|
||||
channelTypes?: readonly Type[],
|
||||
): ReadonlyCollection<
|
||||
Snowflake,
|
||||
Extract<
|
||||
NonNullable<CommandInteractionOption<Cached>['channel']>,
|
||||
{
|
||||
type: Type extends ChannelType.AnnouncementThread | ChannelType.PublicThread
|
||||
? ChannelType.AnnouncementThread | ChannelType.PublicThread
|
||||
: Type;
|
||||
}
|
||||
>
|
||||
>;
|
||||
public getSelectedChannels<const Type extends ChannelType = ChannelType>(
|
||||
customId: string,
|
||||
required?: boolean,
|
||||
channelTypes?: readonly Type[],
|
||||
): ReadonlyCollection<
|
||||
Snowflake,
|
||||
Extract<
|
||||
NonNullable<CommandInteractionOption<Cached>['channel']>,
|
||||
{
|
||||
type: Type extends ChannelType.AnnouncementThread | ChannelType.PublicThread
|
||||
? ChannelType.AnnouncementThread | ChannelType.PublicThread
|
||||
: Type;
|
||||
}
|
||||
>
|
||||
> | null;
|
||||
|
||||
public getSelectedRoles(customId: string, required: true): NonNullable<SelectMenuModalData<Cached>['roles']>;
|
||||
public getSelectedRoles(
|
||||
customId: string,
|
||||
required?: boolean,
|
||||
): NonNullable<SelectMenuModalData<Cached>['roles']> | null;
|
||||
|
||||
public getSelectedMentionables(customId: string, required: true): ModalSelectedMentionables<Cached>;
|
||||
public getSelectedMentionables(customId: string, required?: boolean): ModalSelectedMentionables<Cached> | null;
|
||||
public getUploadedFiles(customId: string, required: true): ReadonlyCollection<Snowflake, Attachment>;
|
||||
public getUploadedFiles(customId: string, required?: boolean): ReadonlyCollection<Snowflake, Attachment> | null;
|
||||
}
|
||||
|
||||
export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = CacheType>
|
||||
@@ -2807,8 +2932,8 @@ export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extend
|
||||
private constructor(client: Client<true>, data: APIModalSubmitInteraction);
|
||||
public type: InteractionType.ModalSubmit;
|
||||
public readonly customId: string;
|
||||
public readonly components: ActionRowModalData[];
|
||||
public readonly fields: ModalSubmitFields;
|
||||
public readonly components: (ActionRowModalData | LabelModalData)[];
|
||||
public readonly fields: ModalSubmitFields<Cached>;
|
||||
public deferred: boolean;
|
||||
public ephemeral: boolean | null;
|
||||
public message: Message<BooleanCache<Cached>> | null;
|
||||
@@ -4036,6 +4161,8 @@ export class Formatters extends null {
|
||||
export type ComponentData =
|
||||
| MessageActionRowComponentData
|
||||
| ModalActionRowComponentData
|
||||
| LabelComponentData
|
||||
| ComponentInLabelData
|
||||
| ComponentInContainerData
|
||||
| ContainerComponentData
|
||||
| ThumbnailComponentData;
|
||||
@@ -4135,9 +4262,9 @@ export class Webhook<Type extends WebhookType = WebhookType> {
|
||||
public editMessage(
|
||||
message: MessageResolvable,
|
||||
options: string | MessagePayload | WebhookMessageEditOptions,
|
||||
): Promise<Message>;
|
||||
public fetchMessage(message: Snowflake, options?: WebhookFetchMessageOptions): Promise<Message>;
|
||||
public send(options: string | MessagePayload | WebhookMessageCreateOptions): Promise<Message>;
|
||||
): Promise<Message<true>>;
|
||||
public fetchMessage(message: Snowflake, options?: WebhookFetchMessageOptions): Promise<Message<true>>;
|
||||
public send(options: string | MessagePayload | WebhookMessageCreateOptions): Promise<Message<true>>;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-empty-interface
|
||||
@@ -4523,6 +4650,8 @@ export enum DiscordjsErrorCodes {
|
||||
|
||||
ModalSubmitInteractionFieldNotFound = 'ModalSubmitInteractionFieldNotFound',
|
||||
ModalSubmitInteractionFieldType = 'ModalSubmitInteractionFieldType',
|
||||
ModalSubmitInteractionFieldEmpty = 'ModalSubmitInteractionComponentEmpty',
|
||||
ModalSubmitInteractionFieldInvalidChannelType = 'ModalSubmitInteractionFieldInvalidChannelType',
|
||||
|
||||
InvalidMissingScopes = 'InvalidMissingScopes',
|
||||
InvalidScopesWithPermissions = 'InvalidScopesWithPermissions',
|
||||
@@ -4906,6 +5035,7 @@ export class GuildMemberManager extends CachedManager<Snowflake, GuildMember, Gu
|
||||
options?: BulkBanOptions,
|
||||
): Promise<BulkBanResult>;
|
||||
public edit(user: UserResolvable, options: GuildMemberEditOptions): Promise<GuildMember>;
|
||||
public editMe(options: GuildMemberEditMeOptions): Promise<GuildMember>;
|
||||
public fetch(
|
||||
options: UserResolvable | FetchMemberOptions | (FetchMembersOptions & { user: UserResolvable }),
|
||||
): Promise<GuildMember>;
|
||||
@@ -5915,7 +6045,7 @@ export interface ClientEvents {
|
||||
];
|
||||
messageReactionRemoveEmoji: [reaction: MessageReaction | PartialMessageReaction];
|
||||
messageDeleteBulk: [
|
||||
messages: ReadonlyCollection<Snowflake, OmitPartialGroupDMChannel<Message | PartialMessage>>,
|
||||
messages: ReadonlyCollection<Snowflake, Message<true> | PartialMessage<true>>,
|
||||
channel: GuildTextBasedChannel,
|
||||
];
|
||||
messageReactionAdd: [
|
||||
@@ -6062,13 +6192,17 @@ export interface CommandInteractionOption<Cached extends CacheType = CacheType>
|
||||
message?: Message<BooleanCache<Cached>>;
|
||||
}
|
||||
|
||||
export interface CommandInteractionResolvedData<Cached extends CacheType = CacheType> {
|
||||
users?: ReadonlyCollection<Snowflake, User>;
|
||||
export interface BaseInteractionResolvedData<Cached extends CacheType = CacheType> {
|
||||
attachments?: ReadonlyCollection<Snowflake, Attachment>;
|
||||
channels?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Channel, APIInteractionDataResolvedChannel>>;
|
||||
members?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, GuildMember, APIInteractionDataResolvedGuildMember>>;
|
||||
roles?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Role, APIRole>>;
|
||||
channels?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Channel, APIInteractionDataResolvedChannel>>;
|
||||
users?: ReadonlyCollection<Snowflake, User>;
|
||||
}
|
||||
|
||||
export interface CommandInteractionResolvedData<Cached extends CacheType = CacheType>
|
||||
extends BaseInteractionResolvedData<Cached> {
|
||||
messages?: ReadonlyCollection<Snowflake, CacheTypeReducer<Cached, Message, APIMessage>>;
|
||||
attachments?: ReadonlyCollection<Snowflake, Attachment>;
|
||||
}
|
||||
|
||||
export interface AutocompleteFocusedOption extends Pick<CommandInteractionOption, 'name'> {
|
||||
@@ -6760,6 +6894,14 @@ export interface GuildMemberEditOptions {
|
||||
|
||||
export type GuildMemberResolvable = GuildMember | UserResolvable;
|
||||
|
||||
export interface GuildMemberEditMeOptions {
|
||||
avatar?: Base64Resolvable | BufferResolvable | null;
|
||||
banner?: Base64Resolvable | BufferResolvable | null;
|
||||
bio?: string | null;
|
||||
nick?: string | null;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export type GuildResolvable = Guild | NonThreadGuildBasedChannel | GuildMember | GuildEmoji | Invite | Role | Snowflake;
|
||||
|
||||
export interface GuildPruneMembersOptions {
|
||||
@@ -7233,6 +7375,7 @@ export interface BaseSelectMenuComponentData extends BaseComponentData {
|
||||
maxValues?: number;
|
||||
minValues?: number;
|
||||
placeholder?: string;
|
||||
required?: true;
|
||||
}
|
||||
|
||||
export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData {
|
||||
@@ -7296,6 +7439,14 @@ export interface TextInputComponentData extends BaseComponentData {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface FileUploadComponentData extends BaseComponentData {
|
||||
customId: string;
|
||||
maxValues?: number;
|
||||
minValues?: number;
|
||||
required?: boolean;
|
||||
type: ComponentType.FileUpload;
|
||||
}
|
||||
|
||||
export type MessageTarget =
|
||||
| Interaction
|
||||
| InteractionWebhook
|
||||
@@ -7398,8 +7549,8 @@ export interface PartialDMChannel extends Partialize<DMChannel, null, null, 'las
|
||||
|
||||
export interface PartialGuildMember extends Partialize<GuildMember, 'joinedAt' | 'joinedTimestamp' | 'pending'> {}
|
||||
|
||||
export interface PartialMessage
|
||||
extends Partialize<Message, 'type' | 'system' | 'pinned' | 'tts', 'content' | 'cleanContent' | 'author'> {}
|
||||
export interface PartialMessage<InGuild extends boolean = boolean>
|
||||
extends Partialize<Message<InGuild>, 'type' | 'system' | 'pinned' | 'tts', 'content' | 'cleanContent' | 'author'> {}
|
||||
|
||||
export interface PartialMessageReaction extends Partialize<MessageReaction, 'count'> {}
|
||||
|
||||
@@ -7878,3 +8029,6 @@ export * from '@discordjs/formatters';
|
||||
export * from '@discordjs/rest';
|
||||
export * from '@discordjs/util';
|
||||
export * from '@discordjs/ws';
|
||||
|
||||
// Solve TS compile error
|
||||
export type { ImageSize };
|
||||
|
||||
@@ -2566,6 +2566,59 @@ chatInputInteraction.showModal({
|
||||
],
|
||||
});
|
||||
|
||||
chatInputInteraction.showModal({
|
||||
title: 'abc',
|
||||
customId: 'abc',
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Label,
|
||||
label: 'label',
|
||||
component: {
|
||||
customId: 'aa',
|
||||
type: ComponentType.TextInput,
|
||||
style: TextInputStyle.Short,
|
||||
label: 'label',
|
||||
},
|
||||
},
|
||||
{
|
||||
components: [
|
||||
{
|
||||
customId: 'aa',
|
||||
label: 'label',
|
||||
style: TextInputStyle.Short,
|
||||
type: ComponentType.TextInput,
|
||||
},
|
||||
],
|
||||
type: ComponentType.ActionRow,
|
||||
},
|
||||
{
|
||||
type: ComponentType.Label,
|
||||
label: 'Lll',
|
||||
component: {
|
||||
customId: 'aa',
|
||||
type: ComponentType.UserSelect,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ComponentType.Label,
|
||||
label: 'Lll',
|
||||
component: {
|
||||
customId: 'aa',
|
||||
type: ComponentType.ChannelSelect,
|
||||
channelTypes: [ChannelType.GuildText, ChannelType.GuildVoice],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ComponentType.Label,
|
||||
label: 'Lll',
|
||||
component: {
|
||||
customId: 'aa',
|
||||
type: ComponentType.RoleSelect,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
declare const stringSelectMenuData: APIStringSelectComponent;
|
||||
StringSelectMenuBuilder.from(stringSelectMenuData);
|
||||
|
||||
@@ -2649,9 +2702,9 @@ declare const webhookClient: WebhookClient;
|
||||
declare const interactionWebhook: InteractionWebhook;
|
||||
declare const snowflake: Snowflake;
|
||||
|
||||
expectType<Promise<Message>>(webhook.send('content'));
|
||||
expectType<Promise<Message>>(webhook.editMessage(snowflake, 'content'));
|
||||
expectType<Promise<Message>>(webhook.fetchMessage(snowflake));
|
||||
expectType<Promise<Message<true>>>(webhook.send('content'));
|
||||
expectType<Promise<Message<true>>>(webhook.editMessage(snowflake, 'content'));
|
||||
expectType<Promise<Message<true>>>(webhook.fetchMessage(snowflake));
|
||||
expectType<Promise<Webhook>>(webhook.edit({ name: 'name' }));
|
||||
|
||||
expectType<Promise<APIMessage>>(webhookClient.send('content'));
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## Installation
|
||||
|
||||
**Node.js 18 or newer is required.**
|
||||
**Node.js 16.11.0 or newer is required.**
|
||||
|
||||
```sh
|
||||
npm install @discordjs/formatters
|
||||
|
||||
@@ -23,6 +23,8 @@ const testURLs = [
|
||||
'https://example.com/name_with_underscores',
|
||||
'https://example.com/name__with__underscores',
|
||||
'https://example.com/name_with_underscores_and__double__underscores',
|
||||
'https://*.example.com/globbed/*',
|
||||
'https://example.com/*before*/>/*after*',
|
||||
];
|
||||
|
||||
describe('Markdown escapers', () => {
|
||||
@@ -87,6 +89,16 @@ describe('Markdown escapers', () => {
|
||||
test('url', () => {
|
||||
for (const url of testURLs) expect(escapeItalic(url)).toBe(url);
|
||||
});
|
||||
|
||||
test('after-url', () => {
|
||||
const url = '<https://example.com>';
|
||||
for (const [input, output] of [
|
||||
['*hi*', '\\*hi\\*'],
|
||||
['_hi_', '\\_hi\\_'],
|
||||
]) {
|
||||
expect(escapeItalic(url + input)).toBe(url + output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeUnderline', () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
hyperlink,
|
||||
inlineCode,
|
||||
italic,
|
||||
linkedRoleMention,
|
||||
messageLink,
|
||||
orderedList,
|
||||
quote,
|
||||
@@ -29,6 +30,8 @@ import {
|
||||
underline,
|
||||
unorderedList,
|
||||
userMention,
|
||||
email,
|
||||
phoneNumber,
|
||||
} from '../src/index.js';
|
||||
|
||||
describe('Message formatters', () => {
|
||||
@@ -145,6 +148,12 @@ describe('Message formatters', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('linkedRoleMention', () => {
|
||||
test('GIVEN roleId THEN returns "<id:linked-roles:[roleId]>"', () => {
|
||||
expect(linkedRoleMention('815434166602170409')).toEqual('<id:linked-roles:815434166602170409>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chatInputApplicationCommandMention', () => {
|
||||
test('GIVEN commandName and commandId THEN returns "</[commandName]:[commandId]>"', () => {
|
||||
expect(chatInputApplicationCommandMention('airhorn', '815434166602170409')).toEqual(
|
||||
@@ -167,31 +176,37 @@ describe('Message formatters', () => {
|
||||
});
|
||||
|
||||
describe('formatEmoji', () => {
|
||||
test('GIVEN static emojiId THEN returns "<:_:${emojiId}>"', () => {
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji('851461487498493952')).toEqual('<:_:851461487498493952>');
|
||||
test('GIVEN static emojiId THEN returns "<:emoji:${emojiId}>"', () => {
|
||||
expect<`<:emoji:851461487498493952>`>(formatEmoji('851461487498493952')).toEqual('<:emoji:851461487498493952>');
|
||||
});
|
||||
|
||||
test('GIVEN static emojiId WITH animated explicitly false THEN returns "<:_:[emojiId]>"', () => {
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji('851461487498493952', false)).toEqual('<:_:851461487498493952>');
|
||||
});
|
||||
|
||||
test('GIVEN animated emojiId THEN returns "<a:_:${emojiId}>"', () => {
|
||||
expect<`<a:_:827220205352255549>`>(formatEmoji('827220205352255549', true)).toEqual('<a:_:827220205352255549>');
|
||||
});
|
||||
|
||||
test('GIVEN static id in options object THEN returns "<:_:${id}>"', () => {
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji({ id: '851461487498493952' })).toEqual('<:_:851461487498493952>');
|
||||
});
|
||||
|
||||
test('GIVEN static id in options object WITH animated explicitly false THEN returns "<:_:${id}>"', () => {
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji({ animated: false, id: '851461487498493952' })).toEqual(
|
||||
'<:_:851461487498493952>',
|
||||
test('GIVEN static emojiId WITH animated explicitly false THEN returns "<:emoji:[emojiId]>"', () => {
|
||||
expect<`<:emoji:851461487498493952>`>(formatEmoji('851461487498493952', false)).toEqual(
|
||||
'<:emoji:851461487498493952>',
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN animated id in options object THEN returns "<a:_:${id}>"', () => {
|
||||
expect<`<a:_:827220205352255549>`>(formatEmoji({ animated: true, id: '827220205352255549' })).toEqual(
|
||||
'<a:_:827220205352255549>',
|
||||
test('GIVEN animated emojiId THEN returns "<a:emoji:${emojiId}>"', () => {
|
||||
expect<`<a:emoji:827220205352255549>`>(formatEmoji('827220205352255549', true)).toEqual(
|
||||
'<a:emoji:827220205352255549>',
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN static id in options object THEN returns "<:emoji:${id}>"', () => {
|
||||
expect<`<:emoji:851461487498493952>`>(formatEmoji({ id: '851461487498493952' })).toEqual(
|
||||
'<:emoji:851461487498493952>',
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN static id in options object WITH animated explicitly false THEN returns "<:emoji:${id}>"', () => {
|
||||
expect<`<:emoji:851461487498493952>`>(formatEmoji({ animated: false, id: '851461487498493952' })).toEqual(
|
||||
'<:emoji:851461487498493952>',
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN animated id in options object THEN returns "<a:emoji:${id}>"', () => {
|
||||
expect<`<a:emoji:827220205352255549>`>(formatEmoji({ animated: true, id: '827220205352255549' })).toEqual(
|
||||
'<a:emoji:827220205352255549>',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -335,6 +350,33 @@ describe('Message formatters', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('email', () => {
|
||||
test('GIVEN an email THEN returns "<[email]>"', () => {
|
||||
expect<'<test@example.com>'>(email('test@example.com')).toEqual('<test@example.com>');
|
||||
});
|
||||
|
||||
test('GIVEN an email AND headers THEN returns "<[email]?[headers]>"', () => {
|
||||
expect<`<test@example.com?${string}>`>(email('test@example.com', { subject: 'Hello', body: 'World' })).toEqual(
|
||||
'<test@example.com?subject=Hello&body=World>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('phoneNumber', () => {
|
||||
test('GIVEN a phone number with + THEN returns "<[phoneNumber]>"', () => {
|
||||
expect<'<+1234567890>'>(phoneNumber('+1234567890')).toEqual('<+1234567890>');
|
||||
});
|
||||
|
||||
test('GIVEN a phone number without + THEN throws', () => {
|
||||
expect(() =>
|
||||
phoneNumber(
|
||||
// @ts-expect-error - Invalid input
|
||||
'1234567890',
|
||||
),
|
||||
).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Faces', () => {
|
||||
test('GIVEN Faces.Shrug THEN returns "¯\\_(ツ)_/¯"', () => {
|
||||
expect<'¯\\_(ツ)_/¯'>(Faces.Shrug).toEqual('¯\\_(ツ)_/¯');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@discordjs/formatters",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.1",
|
||||
"description": "A set of functions to format strings for Discord.",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
@@ -55,7 +55,7 @@
|
||||
"homepage": "https://discord.js.org",
|
||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.24"
|
||||
"discord-api-types": "^0.38.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
@@ -75,7 +75,7 @@
|
||||
"vitest": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -212,15 +212,21 @@ export function escapeInlineCode(text: string): string {
|
||||
*/
|
||||
export function escapeItalic(text: string): string {
|
||||
let idx = 0;
|
||||
const newText = text.replaceAll(/(?<=^|[^*])\*([^*]|\*\*|$)/g, (_, match) => {
|
||||
if (match === '**') return ++idx % 2 ? `\\*${match}` : `${match}\\*`;
|
||||
return `\\*${match}`;
|
||||
});
|
||||
const newText = text.replaceAll(
|
||||
/(?<=^|[^*])(?<!(?<!<)https?:\/\/\S*|<[^\s:]+:\/[^\s>]*)\*([^*]|\*\*|$)/g,
|
||||
(_, match) => {
|
||||
if (match === '**') return ++idx % 2 ? `\\*${match}` : `${match}\\*`;
|
||||
return `\\*${match}`;
|
||||
},
|
||||
);
|
||||
idx = 0;
|
||||
return newText.replaceAll(/(?<=^|[^_])(?<!<a?:.+|https?:\/\/\S+)_(?!:\d+>)([^_]|__|$)/g, (_, match) => {
|
||||
if (match === '__') return ++idx % 2 ? `\\_${match}` : `${match}\\_`;
|
||||
return `\\_${match}`;
|
||||
});
|
||||
return newText.replaceAll(
|
||||
/(?<=^|[^_])(?<!<a?:.+|(?<!<)https?:\/\/\S*|<[^\s:]:\/[^\s>]*)_(?!:\d+>)([^_]|__|$)/g,
|
||||
(_, match) => {
|
||||
if (match === '__') return ++idx % 2 ? `\\_${match}` : `${match}\\_`;
|
||||
return `\\_${match}`;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -225,6 +225,16 @@ export function roleMention<RoleId extends Snowflake>(roleId: RoleId): `<@&${Rol
|
||||
return `<@&${roleId}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a role id into a linked role mention.
|
||||
*
|
||||
* @typeParam RoleId - This is inferred by the supplied role id
|
||||
* @param roleId - The role id to format
|
||||
*/
|
||||
export function linkedRoleMention<RoleId extends Snowflake>(roleId: RoleId): `<id:linked-roles:${RoleId}>` {
|
||||
return `<id:linked-roles:${roleId}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an application command name, subcommand group name, subcommand name, and id into an application command mention.
|
||||
*
|
||||
@@ -313,7 +323,7 @@ export function chatInputApplicationCommandMention<
|
||||
* @typeParam EmojiId - This is inferred by the supplied emoji id
|
||||
* @param emojiId - The emoji id to format
|
||||
*/
|
||||
export function formatEmoji<EmojiId extends Snowflake>(emojiId: EmojiId, animated?: false): `<:_:${EmojiId}>`;
|
||||
export function formatEmoji<EmojiId extends Snowflake>(emojiId: EmojiId, animated?: false): `<:emoji:${EmojiId}>`;
|
||||
|
||||
/**
|
||||
* Formats an animated emoji id into a fully qualified emoji identifier.
|
||||
@@ -322,7 +332,7 @@ export function formatEmoji<EmojiId extends Snowflake>(emojiId: EmojiId, animate
|
||||
* @param emojiId - The emoji id to format
|
||||
* @param animated - Whether the emoji is animated
|
||||
*/
|
||||
export function formatEmoji<EmojiId extends Snowflake>(emojiId: EmojiId, animated?: true): `<a:_:${EmojiId}>`;
|
||||
export function formatEmoji<EmojiId extends Snowflake>(emojiId: EmojiId, animated?: true): `<a:emoji:${EmojiId}>`;
|
||||
|
||||
/**
|
||||
* Formats an emoji id into a fully qualified emoji identifier.
|
||||
@@ -334,7 +344,7 @@ export function formatEmoji<EmojiId extends Snowflake>(emojiId: EmojiId, animate
|
||||
export function formatEmoji<EmojiId extends Snowflake>(
|
||||
emojiId: EmojiId,
|
||||
animated?: boolean,
|
||||
): `<:_:${EmojiId}>` | `<a:_:${EmojiId}>`;
|
||||
): `<:emoji:${EmojiId}>` | `<a:emoji:${EmojiId}>`;
|
||||
|
||||
/**
|
||||
* Formats a non-animated emoji id and name into a fully qualified emoji identifier.
|
||||
@@ -383,7 +393,7 @@ export function formatEmoji<EmojiId extends Snowflake, EmojiName extends string>
|
||||
|
||||
const { id, animated: isAnimated, name: emojiName } = options;
|
||||
|
||||
return `<${isAnimated ? 'a' : ''}:${emojiName ?? '_'}:${id}>`;
|
||||
return `<${isAnimated ? 'a' : ''}:${emojiName ?? 'emoji'}:${id}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -658,6 +668,60 @@ export function applicationDirectory<ApplicationId extends Snowflake, SKUId exte
|
||||
return skuId ? `${url}/${skuId}` : url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an email address into an email mention.
|
||||
*
|
||||
* @typeParam Email - This is inferred by the supplied email address
|
||||
* @param email - The email address to format
|
||||
*/
|
||||
export function email<Email extends string>(email: Email): `<${Email}>`;
|
||||
|
||||
/**
|
||||
* Formats an email address and headers into an email mention.
|
||||
*
|
||||
* @typeParam Email - This is inferred by the supplied email address
|
||||
* @param email - The email address to format
|
||||
* @param headers - Optional headers to include in the email mention
|
||||
*/
|
||||
export function email<Email extends string>(
|
||||
email: Email,
|
||||
headers: Record<string, string | readonly string[]> | undefined,
|
||||
): `<${Email}?${string}>`;
|
||||
|
||||
/**
|
||||
* Formats an email address into an email mention.
|
||||
*
|
||||
* @typeParam Email - This is inferred by the supplied email address
|
||||
* @param email - The email address to format
|
||||
* @param headers - Optional headers to include in the email mention
|
||||
*/
|
||||
export function email<Email extends string>(email: Email, headers?: Record<string, string | readonly string[]>) {
|
||||
if (headers) {
|
||||
// eslint-disable-next-line n/prefer-global/url-search-params
|
||||
const searchParams = new URLSearchParams(
|
||||
Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])),
|
||||
);
|
||||
|
||||
return `<${email}?${searchParams.toString()}>` as const;
|
||||
}
|
||||
|
||||
return `<${email}>` as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a phone number into a phone number mention.
|
||||
*
|
||||
* @typeParam PhoneNumber - This is inferred by the supplied phone number
|
||||
* @param phoneNumber - The phone number to format. Must start with a `+` sign.
|
||||
*/
|
||||
export function phoneNumber<PhoneNumber extends `+${string}`>(phoneNumber: PhoneNumber) {
|
||||
if (!phoneNumber.startsWith('+')) {
|
||||
throw new Error('Phone number must start with a "+" sign.');
|
||||
}
|
||||
|
||||
return `<${phoneNumber}>` as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link https://discord.com/developers/docs/reference#message-formatting-timestamp-styles | message formatting timestamp styles}
|
||||
* supported by Discord.
|
||||
@@ -754,4 +818,8 @@ export enum GuildNavigationMentions {
|
||||
* {@link https://support.discord.com/hc/articles/13497665141655 | Server Guide} tab.
|
||||
*/
|
||||
Guide = '<id:guide>',
|
||||
/**
|
||||
* {@link https://support.discord.com/hc/articles/10388356626711 | Linked Roles} tab.
|
||||
*/
|
||||
LinkedRoles = '<id:linked-roles>',
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"@discordjs/rest": "workspace:^",
|
||||
"@discordjs/util": "workspace:^",
|
||||
"@discordjs/ws": "workspace:^",
|
||||
"discord-api-types": "^0.38.24"
|
||||
"discord-api-types": "^0.38.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.3",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.24",
|
||||
"discord-api-types": "^0.38.32",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.21.3"
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||
"dependencies": {
|
||||
"@types/ws": "^8.5.12",
|
||||
"discord-api-types": "^0.38.24",
|
||||
"discord-api-types": "^0.38.32",
|
||||
"prism-media": "^1.3.5",
|
||||
"tslib": "^2.6.3",
|
||||
"ws": "^8.18.0"
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# [@discordjs/ws@2.0.4](https://github.com/discordjs/discord.js/compare/@discordjs/ws@2.0.3...@discordjs/ws@2.0.4) - (2025-11-10)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **WebSocketShard:** Bad error re-throw (#11151) ([6c781ed](https://github.com/discordjs/discord.js/commit/6c781ede30bd9625ef15eda5dcc166f3b97fb3cf))
|
||||
- **SimpleIdentifyThrottler:** Don't sleep negative amounts (#10669) ([be38128](https://github.com/discordjs/discord.js/commit/be38128ea17bfd1d0d0a8b298867ed5b179effda))
|
||||
|
||||
## Refactor
|
||||
|
||||
- **IContextFetchingStrategy:** Explicitly name forwarded properties (#10652) ([737a97d](https://github.com/discordjs/discord.js/commit/737a97d068a39094786d6ada42ca39a0c583ec2d))
|
||||
|
||||
# [@discordjs/ws@2.0.3](https://github.com/discordjs/discord.js/compare/@discordjs/ws@2.0.2...@discordjs/ws@2.0.3) - (2025-06-16)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
@@ -1,33 +1,58 @@
|
||||
// @ts-nocheck
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import * as timers from 'node:timers/promises';
|
||||
import { expect, test, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { SimpleIdentifyThrottler } from '../../src/index.js';
|
||||
|
||||
vi.mock('node:timers/promises', () => ({
|
||||
setTimeout: vi.fn(),
|
||||
}));
|
||||
|
||||
const throttler = new SimpleIdentifyThrottler(2);
|
||||
let throttler: SimpleIdentifyThrottler;
|
||||
const controller = new AbortController();
|
||||
|
||||
vi.useFakeTimers();
|
||||
|
||||
const NOW = vi.fn().mockReturnValue(Date.now());
|
||||
const TIME = Date.now();
|
||||
const NOW = vi.fn().mockReturnValue(TIME);
|
||||
global.Date.now = NOW;
|
||||
|
||||
const sleep = vi.spyOn(timers, 'setTimeout');
|
||||
|
||||
beforeEach(() => {
|
||||
throttler = new SimpleIdentifyThrottler(2);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sleep.mockClear();
|
||||
});
|
||||
|
||||
test('basic case', async () => {
|
||||
// Those shouldn't wait since they're in different keys
|
||||
|
||||
await throttler.waitForIdentify(0);
|
||||
await throttler.waitForIdentify(0, controller.signal);
|
||||
expect(sleep).not.toHaveBeenCalled();
|
||||
|
||||
await throttler.waitForIdentify(1);
|
||||
await throttler.waitForIdentify(1, controller.signal);
|
||||
expect(sleep).not.toHaveBeenCalled();
|
||||
|
||||
// Those should wait
|
||||
|
||||
await throttler.waitForIdentify(2);
|
||||
await throttler.waitForIdentify(2, controller.signal);
|
||||
expect(sleep).toHaveBeenCalledTimes(1);
|
||||
|
||||
await throttler.waitForIdentify(3);
|
||||
await throttler.waitForIdentify(3, controller.signal);
|
||||
expect(sleep).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('does not call sleep with a negative time', async () => {
|
||||
await throttler.waitForIdentify(0, controller.signal);
|
||||
expect(sleep).not.toHaveBeenCalled();
|
||||
|
||||
await throttler.waitForIdentify(0, controller.signal);
|
||||
expect(sleep).toHaveBeenCalledTimes(1);
|
||||
|
||||
// By overshooting, we're simulating a bug that existed prior to this test, where-in by enough time
|
||||
// passing before the shard tried to identify for a subsequent time, the passed value would end up being negative
|
||||
// (and this was unchecked). Node simply treats that as 1ms, so it wasn't particularly harmful, but they
|
||||
// recently introduced a warning for it, so we want to avoid that.
|
||||
NOW.mockReturnValueOnce(TIME + 10_000);
|
||||
await throttler.waitForIdentify(0, controller.signal);
|
||||
expect(sleep).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@discordjs/ws",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.4",
|
||||
"description": "Wrapper around Discord's gateway",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
@@ -79,7 +79,7 @@
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.24",
|
||||
"discord-api-types": "^0.38.32",
|
||||
"tslib": "^2.6.3",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
|
||||
@@ -3,15 +3,20 @@ import type { APIGatewayBotInfo } from 'discord-api-types/v10';
|
||||
import type { SessionInfo, WebSocketManager, WebSocketManagerOptions } from '../../ws/WebSocketManager.js';
|
||||
|
||||
export interface FetchingStrategyOptions
|
||||
extends Omit<
|
||||
extends Pick<
|
||||
WebSocketManagerOptions,
|
||||
| 'buildIdentifyThrottler'
|
||||
| 'buildStrategy'
|
||||
| 'rest'
|
||||
| 'retrieveSessionInfo'
|
||||
| 'shardCount'
|
||||
| 'shardIds'
|
||||
| 'updateSessionInfo'
|
||||
| 'compression'
|
||||
| 'encoding'
|
||||
| 'handshakeTimeout'
|
||||
| 'helloTimeout'
|
||||
| 'identifyProperties'
|
||||
| 'initialPresence'
|
||||
| 'intents'
|
||||
| 'largeThreshold'
|
||||
| 'readyTimeout'
|
||||
| 'token'
|
||||
| 'useIdentifyCompression'
|
||||
| 'version'
|
||||
> {
|
||||
readonly gatewayInformation: APIGatewayBotInfo;
|
||||
readonly shardCount: number;
|
||||
@@ -33,20 +38,20 @@ export interface IContextFetchingStrategy {
|
||||
}
|
||||
|
||||
export async function managerToFetchingStrategyOptions(manager: WebSocketManager): Promise<FetchingStrategyOptions> {
|
||||
const {
|
||||
buildIdentifyThrottler,
|
||||
buildStrategy,
|
||||
retrieveSessionInfo,
|
||||
updateSessionInfo,
|
||||
shardCount,
|
||||
shardIds,
|
||||
rest,
|
||||
...managerOptions
|
||||
} = manager.options;
|
||||
|
||||
return {
|
||||
...managerOptions,
|
||||
compression: manager.options.compression,
|
||||
encoding: manager.options.encoding,
|
||||
handshakeTimeout: manager.options.handshakeTimeout,
|
||||
helloTimeout: manager.options.helloTimeout,
|
||||
identifyProperties: manager.options.identifyProperties,
|
||||
initialPresence: manager.options.initialPresence,
|
||||
intents: manager.options.intents,
|
||||
largeThreshold: manager.options.largeThreshold,
|
||||
readyTimeout: manager.options.readyTimeout,
|
||||
token: manager.token,
|
||||
useIdentifyCompression: manager.options.useIdentifyCompression,
|
||||
version: manager.options.version,
|
||||
|
||||
gatewayInformation: await manager.fetchGatewayInformation(),
|
||||
shardCount: await manager.getShardCount(),
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ export class SimpleIdentifyThrottler implements IIdentifyThrottler {
|
||||
|
||||
try {
|
||||
const diff = state.resetsAt - Date.now();
|
||||
if (diff <= 5_000) {
|
||||
if (diff > 0 && diff <= 5_000) {
|
||||
// To account for the latency the IDENTIFY payload goes through, we add a bit more wait time
|
||||
const time = diff + Math.random() * 1_500;
|
||||
await sleep(time);
|
||||
|
||||
@@ -173,8 +173,6 @@ export class WebSocketShard extends AsyncEventEmitter<WebSocketShardEventsMap> {
|
||||
|
||||
try {
|
||||
await promise;
|
||||
} catch ({ error }: any) {
|
||||
throw error;
|
||||
} finally {
|
||||
// cleanup hanging listeners
|
||||
controller.abort();
|
||||
|
||||
77
pnpm-lock.yaml
generated
77
pnpm-lock.yaml
generated
@@ -692,8 +692,8 @@ importers:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
discord-api-types:
|
||||
specifier: ^0.37.119
|
||||
version: 0.37.119
|
||||
specifier: ^0.38.32
|
||||
version: 0.38.32
|
||||
fast-deep-equal:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3
|
||||
@@ -816,8 +816,8 @@ importers:
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
discord-api-types:
|
||||
specifier: ^0.38.24
|
||||
version: 0.38.24
|
||||
specifier: ^0.38.32
|
||||
version: 0.38.32
|
||||
devDependencies:
|
||||
'@discordjs/api-extractor':
|
||||
specifier: workspace:^
|
||||
@@ -932,14 +932,14 @@ importers:
|
||||
packages/discord.js:
|
||||
dependencies:
|
||||
'@discordjs/builders':
|
||||
specifier: ^1.11.2
|
||||
version: 1.11.2
|
||||
specifier: workspace:^
|
||||
version: link:../builders
|
||||
'@discordjs/collection':
|
||||
specifier: 1.5.3
|
||||
version: 1.5.3
|
||||
'@discordjs/formatters':
|
||||
specifier: ^0.6.1
|
||||
version: 0.6.1
|
||||
specifier: workspace:^
|
||||
version: link:../formatters
|
||||
'@discordjs/rest':
|
||||
specifier: workspace:^
|
||||
version: link:../rest
|
||||
@@ -953,8 +953,8 @@ importers:
|
||||
specifier: 3.5.3
|
||||
version: 3.5.3
|
||||
discord-api-types:
|
||||
specifier: ^0.38.24
|
||||
version: 0.38.24
|
||||
specifier: ^0.38.32
|
||||
version: 0.38.32
|
||||
fast-deep-equal:
|
||||
specifier: 3.1.3
|
||||
version: 3.1.3
|
||||
@@ -1075,8 +1075,8 @@ importers:
|
||||
packages/formatters:
|
||||
dependencies:
|
||||
discord-api-types:
|
||||
specifier: ^0.38.24
|
||||
version: 0.38.24
|
||||
specifier: ^0.38.32
|
||||
version: 0.38.32
|
||||
devDependencies:
|
||||
'@discordjs/api-extractor':
|
||||
specifier: workspace:^
|
||||
@@ -1148,8 +1148,8 @@ importers:
|
||||
specifier: workspace:^
|
||||
version: link:../ws
|
||||
discord-api-types:
|
||||
specifier: ^0.38.24
|
||||
version: 0.38.24
|
||||
specifier: ^0.38.32
|
||||
version: 0.38.32
|
||||
devDependencies:
|
||||
'@discordjs/api-extractor':
|
||||
specifier: workspace:^
|
||||
@@ -1322,8 +1322,8 @@ importers:
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
discord-api-types:
|
||||
specifier: ^0.38.24
|
||||
version: 0.38.24
|
||||
specifier: ^0.38.32
|
||||
version: 0.38.32
|
||||
magic-bytes.js:
|
||||
specifier: ^1.10.0
|
||||
version: 1.10.0
|
||||
@@ -1619,8 +1619,8 @@ importers:
|
||||
specifier: ^8.5.12
|
||||
version: 8.5.12
|
||||
discord-api-types:
|
||||
specifier: ^0.38.24
|
||||
version: 0.38.24
|
||||
specifier: ^0.38.32
|
||||
version: 0.38.32
|
||||
prism-media:
|
||||
specifier: ^1.3.5
|
||||
version: 1.3.5
|
||||
@@ -1716,8 +1716,8 @@ importers:
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
discord-api-types:
|
||||
specifier: ^0.38.24
|
||||
version: 0.38.24
|
||||
specifier: ^0.38.32
|
||||
version: 0.38.32
|
||||
tslib:
|
||||
specifier: ^2.6.3
|
||||
version: 2.6.3
|
||||
@@ -2789,10 +2789,6 @@ packages:
|
||||
resolution: {integrity: sha512-4JINx4Rttha29f50PBsJo48xZXx/He5yaIWJRwVarhYAN947+S84YciHl+AIhQNRPAFkg8+5qFngEGtKxQDWXA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
'@discordjs/builders@1.11.2':
|
||||
resolution: {integrity: sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==}
|
||||
engines: {node: '>=16.11.0'}
|
||||
|
||||
'@discordjs/collection@1.5.3':
|
||||
resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==}
|
||||
engines: {node: '>=16.11.0'}
|
||||
@@ -2801,10 +2797,6 @@ packages:
|
||||
resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@discordjs/formatters@0.6.1':
|
||||
resolution: {integrity: sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==}
|
||||
engines: {node: '>=16.11.0'}
|
||||
|
||||
'@discordjs/rest@2.5.1':
|
||||
resolution: {integrity: sha512-Tg9840IneBcbrAjcGaQzHUJWFNq1MMWZjTdjJ0WS/89IffaNKc++iOvffucPxQTF/gviO9+9r8kEPea1X5J2Dw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -8354,11 +8346,8 @@ packages:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
discord-api-types@0.37.119:
|
||||
resolution: {integrity: sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==}
|
||||
|
||||
discord-api-types@0.38.24:
|
||||
resolution: {integrity: sha512-P7/DkcFIiIoaBogStnhhcGRX7KR+gIFp0SpmwsZUIM0bgDkYMEUx+8l+t3quYc/KSgg92wvE9w/4mabO57EMug==}
|
||||
discord-api-types@0.38.32:
|
||||
resolution: {integrity: sha512-UhIqkFuUVwBzejLPPWF18qixYPucMf718RnGh1NxZYNS7czXUmcUsWWkzWR7lRWj5pjfj4LwrnN9McvpfLvGqQ==}
|
||||
|
||||
dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
@@ -16318,24 +16307,10 @@ snapshots:
|
||||
tar-stream: 3.1.7
|
||||
which: 4.0.0
|
||||
|
||||
'@discordjs/builders@1.11.2':
|
||||
dependencies:
|
||||
'@discordjs/formatters': 0.6.1
|
||||
'@discordjs/util': 1.1.1
|
||||
'@sapphire/shapeshift': 4.0.0
|
||||
discord-api-types: 0.38.24
|
||||
fast-deep-equal: 3.1.3
|
||||
ts-mixer: 6.0.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@discordjs/collection@1.5.3': {}
|
||||
|
||||
'@discordjs/collection@2.1.1': {}
|
||||
|
||||
'@discordjs/formatters@0.6.1':
|
||||
dependencies:
|
||||
discord-api-types: 0.38.24
|
||||
|
||||
'@discordjs/rest@2.5.1':
|
||||
dependencies:
|
||||
'@discordjs/collection': 2.1.1
|
||||
@@ -16343,7 +16318,7 @@ snapshots:
|
||||
'@sapphire/async-queue': 1.5.3
|
||||
'@sapphire/snowflake': 3.5.3
|
||||
'@vladfrangu/async_event_emitter': 2.4.6
|
||||
discord-api-types: 0.38.24
|
||||
discord-api-types: 0.38.32
|
||||
magic-bytes.js: 1.10.0
|
||||
tslib: 2.8.1
|
||||
undici: 6.21.3
|
||||
@@ -16358,7 +16333,7 @@ snapshots:
|
||||
'@sapphire/async-queue': 1.5.3
|
||||
'@types/ws': 8.5.12
|
||||
'@vladfrangu/async_event_emitter': 2.4.6
|
||||
discord-api-types: 0.38.24
|
||||
discord-api-types: 0.38.32
|
||||
tslib: 2.8.1
|
||||
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
transitivePeerDependencies:
|
||||
@@ -23709,9 +23684,7 @@ snapshots:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
||||
discord-api-types@0.37.119: {}
|
||||
|
||||
discord-api-types@0.38.24: {}
|
||||
discord-api-types@0.38.32: {}
|
||||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user