Compare commits

..

17 Commits

Author SHA1 Message Date
Vlad Frangu
93cf7721d0 chore(builders): release @discordjs/builders@1.11.2 2025-05-03 01:03:20 +03:00
Qjuh
e6370ae378 chore: remove container limit (#10867)
* chore: remove container limit

* fix: typo

* fix: setValidationEnabled
2025-04-30 22:19:31 +02:00
Vlad Frangu
f8d7b1c0e0 chore(builders): release @discordjs/builders@1.11.1 2025-04-26 00:44:58 +03:00
Vlad Frangu
669a23050a chore(formatters): release @discordjs/formatters@0.6.1 2025-04-26 00:44:33 +03:00
Vlad Frangu
87f474aea8 chore(deps): bump discord-api-types in formatters 2025-04-26 00:43:37 +03:00
Vlad Frangu
0903e6d498 chore(builders): release @discordjs/builders@1.11.0 2025-04-25 18:02:15 +03:00
Qjuh
118e682682 feat: components v2 in builders v1 (#10787)
* feat(builders): components v2 in builders v1

* feat: implemented the first components

* fix: tests

* fix: tests

* fix: export the new stuff

* feat: add rest of components

* feat: add callback syntax

* feat: callback syntax for section

* fix: missing implements

* fix: accessory property

* fix: apply suggestions from v2 PR

* chore: bring in line with builders v2

* fix: add missing type

* chore: split accessory methods

* chore: backport changes from v2 PR

* fix: accent_color is nullish

* fix: allow passing raw json to MediaGallery methods

* fix: add test

* chore: add Container#addXComponents

* fix: docs

* chore: bump discord-api-types

* Update packages/builders/src/components/Assertions.ts

Co-authored-by: Denis-Adrian Cristea <didinele.dev@gmail.com>

---------

Co-authored-by: Denis-Adrian Cristea <didinele.dev@gmail.com>
2025-04-24 21:59:58 +02:00
Vlad Frangu
53dbc96194 chore(builders): release @discordjs/builders@1.10.1 2025-02-11 00:51:35 +02:00
Almeida
49ef3a833e fix(EmbedBuilder): allow empty name and value on fields (#10747) 2025-02-10 23:55:17 +02:00
Jiralite
bffe8a423e build: bump undici to 6.21.1 2025-02-08 15:38:37 +00:00
Jiralite
5d896f7d4f build: bump discord-api-types to 0.37.119 2025-02-07 21:46:15 +00:00
Jiralite
f3a2fb8a68 build: bump discord-api-types to 0.37.118 2025-01-29 09:36:34 +00:00
Qjuh
fbc3d57828 feat(website): include reexported members in docs (#10518)
* feat(website): add re-exported members to docs site

* refactor(scripts): rewrite sourceURL for externals

* feat(website): add external badge

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2025-01-10 04:04:28 +00:00
Vlad Frangu
d6c7b8fe05 chore(builders): release @discordjs/builders@1.10.0 2025-01-01 23:24:11 +02:00
Vlad Frangu
e9d6046047 chore(formatters): release @discordjs/formatters@0.6.0 2025-01-01 23:23:37 +02:00
Jiralite
24e20d27a9 build: bump discord-api-types to 0.37.114 2024-12-24 12:07:58 +00:00
Jiralite
8760fde9ff build: bump discord-api-types to 0.37.113 2024-12-22 21:58:29 +00:00
153 changed files with 2436 additions and 2180 deletions

View File

@@ -18,8 +18,9 @@ export async function Badges({ node }: { readonly node: any }) {
const isAbstract = node.isAbstract;
const isReadonly = node.isReadonly;
const isOptional = node.isOptional;
const isExternal = node.isExternal;
const isAny = isDeprecated || isProtected || isStatic || isAbstract || isReadonly || isOptional;
const isAny = isDeprecated || isProtected || isStatic || isAbstract || isReadonly || isOptional || isExternal;
return isAny ? (
<div className="mb-1 flex gap-3">
@@ -33,6 +34,7 @@ export async function Badges({ node }: { readonly node: any }) {
{isAbstract ? <Badge className="bg-cyan-500/20 text-cyan-500">abstract</Badge> : null}
{isReadonly ? <Badge className="bg-purple-500/20 text-purple-500">readonly</Badge> : null}
{isOptional ? <Badge className="bg-cyan-500/20 text-cyan-500">optional</Badge> : null}
{isExternal ? <Badge className="bg-purple-500/20 text-purple-500">external</Badge> : null}
</div>
) : null;
}

View File

@@ -20,7 +20,7 @@ export async function fetchDependencies({
return Object.entries<string>(parsedDependencies)
.filter(([key]) => key.startsWith('@discordjs/') && !key.includes('api-extractor'))
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${sanitizeVersion(value)}`);
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${value.replaceAll('.', '-')}`);
} catch {
return [];
}
@@ -36,12 +36,8 @@ export async function fetchDependencies({
return Object.entries<string>(parsedDependencies)
.filter(([key]) => key.startsWith('@discordjs/') && !key.includes('api-extractor'))
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${sanitizeVersion(value)}`);
.map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${value.replaceAll('.', '-')}`);
} catch {
return [];
}
}
function sanitizeVersion(version: string) {
return version.replaceAll('.', '-').replace(/^[\^~]/, '');
}

View File

@@ -49,7 +49,7 @@
"meilisearch": "^0.38.0",
"p-limit": "^6.1.0",
"tslib": "^2.6.3",
"undici": "6.19.8"
"undici": "6.21.1"
},
"devDependencies": {
"@types/node": "^18.19.45",

View File

@@ -114,7 +114,7 @@ interface DocgenEventJson {
}
interface DocgenParamJson {
default?: boolean | number | string;
default?: string;
description: string;
name: string;
nullable?: boolean;
@@ -155,7 +155,7 @@ interface DocgenMethodJson {
interface DocgenPropertyJson {
abstract?: boolean;
access?: DocgenAccess;
default?: boolean | number | string;
default?: string;
deprecated?: DocgenDeprecated;
description: string;
meta: DocgenMetaJson;
@@ -1264,7 +1264,7 @@ export class ApiModelGenerator {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? this._tsDocParser.parseString(
`/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}${jsDoc.default ? ` (default: ${this._escapeSpecialChars(jsDoc.default)})` : ''}\n${
`/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}\n${
'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : ''
}${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${
'deprecated' in jsDoc && jsDoc.deprecated
@@ -1342,7 +1342,7 @@ export class ApiModelGenerator {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? this._tsDocParser.parseString(
`/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}${jsDoc.default ? ` (default: ${this._escapeSpecialChars(jsDoc.default)})` : ''}\n${
`/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}\n${
'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : ''
}${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${
'deprecated' in jsDoc && jsDoc.deprecated
@@ -1510,17 +1510,15 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = [
{
kind: ExcerptTokenKind.Content,
text: `public on(eventName: '${name}', listener: (${
jsDoc.params?.length
? `${jsDoc.params[0]?.name}${jsDoc.params[0]?.optional ? '?' : ''}: `
: ') => void): this;'
text: `on('${name}', (${
jsDoc.params?.length ? `${jsDoc.params[0]?.name}${jsDoc.params[0]?.nullable ? '?' : ''}: ` : ') => {})'
}`,
},
];
const parameters: IApiParameterOptions[] = [];
for (let index = 0; index < (jsDoc.params?.length ?? 0) - 1; index++) {
const parameter = jsDoc.params![index]!;
const newTokens = this._mapVarType(parameter.type, parameter.nullable);
const newTokens = this._mapVarType(parameter.type);
parameters.push({
parameterName: parameter.name,
parameterTypeTokenRange: {
@@ -1539,7 +1537,7 @@ export class ApiModelGenerator {
if (jsDoc.params?.length) {
const parameter = jsDoc.params![jsDoc.params.length - 1]!;
const newTokens = this._mapVarType(parameter.type, parameter.nullable);
const newTokens = this._mapVarType(parameter.type);
parameters.push({
parameterName: parameter.name,
parameterTypeTokenRange: {
@@ -1552,7 +1550,7 @@ export class ApiModelGenerator {
excerptTokens.push(...newTokens);
excerptTokens.push({
kind: ExcerptTokenKind.Content,
text: `) => void): this;`,
text: `) => {})`,
});
}
@@ -1748,14 +1746,6 @@ export class ApiModelGenerator {
return sourceLocation;
}
private _escapeSpecialChars(input: boolean | number | string) {
if (typeof input !== 'string') {
return input;
}
return input.replaceAll(/(?<char>[{}])/g, '\\$<char>');
}
private _fixLinkTags(input?: string): string | undefined {
return input
?.replaceAll(linkRegEx, (_match, _p1, _p2, _p3, _p4, _p5, _offset, _string, groups) => {
@@ -1775,7 +1765,7 @@ export class ApiModelGenerator {
.replaceAll('* ', '\n * * ');
}
private _mapVarType(typey: DocgenVarTypeJson, nullable?: boolean): IExcerptToken[] {
private _mapVarType(typey: DocgenVarTypeJson): IExcerptToken[] {
const mapper = Array.isArray(typey) ? typey : (typey.types ?? []);
const lookup: { [K in ts.SyntaxKind]?: string } = {
[ts.SyntaxKind.ClassDeclaration]: 'class',
@@ -1818,22 +1808,7 @@ export class ApiModelGenerator {
{ kind: ExcerptTokenKind.Content, text: symbol ?? '' },
];
}, []);
return index === 0
? mapper.length === 1 && (nullable || ('nullable' in typey && typey.nullable))
? [
...result,
{ kind: ExcerptTokenKind.Content, text: ' | ' },
{ kind: ExcerptTokenKind.Reference, text: 'null' },
]
: result
: index === mapper.length - 1 && (nullable || ('nullable' in typey && typey.nullable))
? [
{ kind: ExcerptTokenKind.Content, text: ' | ' },
...result,
{ kind: ExcerptTokenKind.Content, text: ' | ' },
{ kind: ExcerptTokenKind.Reference, text: 'null' },
]
: [{ kind: ExcerptTokenKind.Content, text: ' | ' }, ...result];
return index === 0 ? result : [{ kind: ExcerptTokenKind.Content, text: ' | ' }, ...result];
})
.filter((excerpt) => excerpt.text.length);
}
@@ -1848,7 +1823,7 @@ export class ApiModelGenerator {
isOptional: Boolean(prop.nullable),
isReadonly: Boolean(prop.readonly),
docComment: this._tsDocParser.parseString(
`/**\n * ${this._fixLinkTags(prop.description) ?? ''}${prop.default ? ` (default: ${this._escapeSpecialChars(prop.default)})` : ''}\n${
`/**\n * ${this._fixLinkTags(prop.description) ?? ''}\n${
prop.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''
}${prop.readonly ? ' * @readonly\n' : ''} */`,
).docComment,

View File

@@ -23,7 +23,7 @@
## Installation
**Node.js 20 or newer is required.**
**Node.js 18 or newer is required.**
```sh
npm install @discordjs/brokers

View File

@@ -30,10 +30,8 @@ body = """
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}

View File

@@ -89,7 +89,7 @@
"vitest": "^2.0.5"
},
"engines": {
"node": ">=20"
"node": ">=18"
},
"publishConfig": {
"access": "public",

View File

@@ -2,6 +2,18 @@
All notable changes to this project will be documented in this file.
# [@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

View File

@@ -23,7 +23,7 @@
## Installation
**Node.js 18 or newer is required.**
**Node.js 16.11.0 or newer is required.**
```sh
npm install @discordjs/builders

View File

@@ -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: [
{

View File

@@ -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,
};

View File

@@ -0,0 +1,248 @@
import { type APIContainerComponent, ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { ButtonBuilder } from '../../../dist/index.mjs';
import { ActionRowBuilder } from '../../../src/components/ActionRow.js';
import { createComponentBuilder } from '../../../src/components/Components.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,
});
});
});
});

View 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 });
});
});
});

View 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);
});
});
});

View 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);
});
});
});

View 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 });
});
});
});

View File

@@ -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' });
});
});
});

View 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 });
});
});
});

View File

@@ -166,7 +166,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,

View File

@@ -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();
});
});
});

View File

@@ -30,10 +30,8 @@ body = """
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@discordjs/builders",
"version": "1.9.0",
"version": "1.11.2",
"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.114",
"discord-api-types": "^0.38.1",
"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",

View File

@@ -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[];
}

View File

@@ -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)

View File

@@ -1,15 +1,20 @@
import type { JSONEncodable } from '@discordjs/util';
import type {
APIActionRowComponent,
APIActionRowComponentTypes,
APIComponentInActionRow,
APIBaseComponent,
ComponentType,
APIMessageComponent,
} 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;
/**
* The base component builder that contains common symbols for all sorts of components.
@@ -42,4 +47,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;
}
}

View File

@@ -1,8 +1,9 @@
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';
@@ -13,6 +14,27 @@ 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 +72,34 @@ 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;
}
/**
@@ -97,8 +147,44 @@ 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);
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);
}

View 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);
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -34,6 +34,16 @@ export {
export * from './components/selectMenu/StringSelectMenuOption.js';
export * from './components/selectMenu/UserSelectMenu.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';

View File

@@ -3,7 +3,7 @@
import type { JSONEncodable } from '@discordjs/util';
import type {
APIActionRowComponent,
APIModalActionRowComponent,
APIComponentInModalActionRow,
APIModalInteractionResponseCallbackData,
} from 'discord-api-types/v10';
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
@@ -64,7 +64,7 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
*/
public addComponents(
...components: RestOrArray<
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIModalActionRowComponent>
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIComponentInModalActionRow>
>
) {
this.components.push(

View File

@@ -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);

View File

@@ -30,10 +30,8 @@ body = """
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}

View File

@@ -23,7 +23,7 @@
## Installation
**Node.js 20 or newer is required.**
**Node.js 18 or newer is required.**
```sh
npm install @discordjs/core

View File

@@ -1,5 +1,6 @@
{
"extends": "../../api-extractor.json",
"bundledPackages": ["discord-api-types"],
"docModel": {
"projectFolderUrl": "https://github.com/discordjs/discord.js/tree/main/packages/core"
}

View File

@@ -30,10 +30,8 @@ body = """
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@discordjs/core",
"version": "2.0.1",
"version": "2.0.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.37.114"
"discord-api-types": "^0.37.119"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
@@ -90,7 +90,7 @@
"vitest": "^2.0.5"
},
"engines": {
"node": ">=20"
"node": ">=18"
},
"publishConfig": {
"access": "public",

View File

@@ -45,7 +45,7 @@ export class ChannelsAPI {
/**
* Sends a message in a channel
*
* @see {@link https://discord.com/developers/docs/resources/message#create-message}
* @see {@link https://discord.com/developers/docs/resources/channel#create-message}
* @param channelId - The id of the channel to send the message in
* @param body - The data for sending the message
* @param options - The options for sending the message
@@ -65,7 +65,7 @@ export class ChannelsAPI {
/**
* Edits a message
*
* @see {@link https://discord.com/developers/docs/resources/message#edit-message}
* @see {@link https://discord.com/developers/docs/resources/channel#edit-message}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to edit
* @param body - The data for editing the message
@@ -87,7 +87,7 @@ export class ChannelsAPI {
/**
* Fetches the reactions for a message
*
* @see {@link https://discord.com/developers/docs/resources/message#get-reactions}
* @see {@link https://discord.com/developers/docs/resources/channel#get-reactions}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to get the reactions for
* @param emoji - The emoji to get the reactions for
@@ -110,7 +110,7 @@ export class ChannelsAPI {
/**
* Deletes a reaction for the current user
*
* @see {@link https://discord.com/developers/docs/resources/message#delete-own-reaction}
* @see {@link https://discord.com/developers/docs/resources/channel#delete-own-reaction}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to delete the reaction for
* @param emoji - The emoji to delete the reaction for
@@ -130,7 +130,7 @@ export class ChannelsAPI {
/**
* Deletes a reaction for a user
*
* @see {@link https://discord.com/developers/docs/resources/message#delete-user-reaction}
* @see {@link https://discord.com/developers/docs/resources/channel#delete-user-reaction}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to delete the reaction for
* @param emoji - The emoji to delete the reaction for
@@ -152,7 +152,7 @@ export class ChannelsAPI {
/**
* Deletes all reactions for a message
*
* @see {@link https://discord.com/developers/docs/resources/message#delete-all-reactions}
* @see {@link https://discord.com/developers/docs/resources/channel#delete-all-reactions}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to delete the reactions for
* @param options - The options for deleting the reactions
@@ -168,7 +168,7 @@ export class ChannelsAPI {
/**
* Deletes all reactions of an emoji for a message
*
* @see {@link https://discord.com/developers/docs/resources/message#delete-all-reactions-for-emoji}
* @see {@link https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to delete the reactions for
* @param emoji - The emoji to delete the reactions for
@@ -186,7 +186,7 @@ export class ChannelsAPI {
/**
* Adds a reaction to a message
*
* @see {@link https://discord.com/developers/docs/resources/message#create-reaction}
* @see {@link https://discord.com/developers/docs/resources/channel#create-reaction}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to add the reaction to
* @param emoji - The emoji to add the reaction with
@@ -242,7 +242,7 @@ export class ChannelsAPI {
/**
* Fetches the messages of a channel
*
* @see {@link https://discord.com/developers/docs/resources/message#get-channel-messages}
* @see {@link https://discord.com/developers/docs/resources/channel#get-channel-messages}
* @param channelId - The id of the channel to fetch messages from
* @param query - The query options for fetching messages
* @param options - The options for fetching the messages
@@ -299,7 +299,7 @@ export class ChannelsAPI {
/**
* Deletes a message
*
* @see {@link https://discord.com/developers/docs/resources/message#delete-message}
* @see {@link https://discord.com/developers/docs/resources/channel#delete-message}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to delete
* @param options - The options for deleting the message
@@ -315,7 +315,7 @@ export class ChannelsAPI {
/**
* Bulk deletes messages
*
* @see {@link https://discord.com/developers/docs/resources/message#bulk-delete-messages}
* @see {@link https://discord.com/developers/docs/resources/channel#bulk-delete-messages}
* @param channelId - The id of the channel the messages are in
* @param messageIds - The ids of the messages to delete
* @param options - The options for deleting the messages
@@ -331,7 +331,7 @@ export class ChannelsAPI {
/**
* Fetches a message
*
* @see {@link https://discord.com/developers/docs/resources/message#get-channel-message}
* @see {@link https://discord.com/developers/docs/resources/channel#get-channel-message}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to fetch
* @param options - The options for fetching the message
@@ -345,7 +345,7 @@ export class ChannelsAPI {
/**
* Crossposts a message
*
* @see {@link https://discord.com/developers/docs/resources/message#crosspost-message}
* @see {@link https://discord.com/developers/docs/resources/channel#crosspost-message}
* @param channelId - The id of the channel the message is in
* @param messageId - The id of the message to crosspost
* @param options - The options for crossposting the message
@@ -452,7 +452,7 @@ export class ChannelsAPI {
/**
* Creates a new forum post
*
* @see {@link https://discord.com/developers/docs/resources/channel#start-thread-in-forum-or-media-channel}
* @see {@link https://discord.com/developers/docs/resources/channel#start-thread-in-forum-channel}
* @param channelId - The id of the forum channel to start the thread in
* @param body - The data for starting the thread
* @param options - The options for starting the thread

View File

@@ -258,7 +258,7 @@ export class InteractionsAPI {
* @param interactionId - The id of the interaction
* @param interactionToken - The token of the interaction
* @param options - The options for sending the premium required response
* @deprecated Sending a premium-style button is the new Discord behavior.
* @deprecated Sending a premium-style button is the new Discord behaviour.
*/
public async sendPremiumRequired(
interactionId: Snowflake,

View File

@@ -30,10 +30,8 @@ body = """
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}

View File

@@ -2,94 +2,6 @@
All notable changes to this project will be documented in this file.
# [14.17.0](https://github.com/discordjs/discord.js/compare/14.16.3...14.17.0) - (2025-01-01)
## Bug Fixes
- **InteractionResponses:** Do not use `in` if a string is passed ([ff42d7a](https://github.com/discordjs/discord.js/commit/ff42d7af72e940ae72c61d2c5164ae68f2708b96))
- Use Message#interactionMetadata (#10654) ([6087088](https://github.com/discordjs/discord.js/commit/60870885790eb1857ed4c2969c9c404e356a1299))
- **InteractionResponses:** Properly resolve message flags (#10661) ([b2754d4](https://github.com/discordjs/discord.js/commit/b2754d4a0ec250ae84057d0f07c078376f54829c))
- **ThreadChannel:** Make `ownerId` always present (#10618) ([7678f11](https://github.com/discordjs/discord.js/commit/7678f1176a645878261361faef0429f9cf7f4810))
- **MessageReaction:** Address `undefined` burst properties (#10597) ([76968b4](https://github.com/discordjs/discord.js/commit/76968b4bc14b8a66825f9140d130b1e04c11855a))
- **ThreadChannel:** Address parameter type on fetchOwner() (#10592) ([56c9396](https://github.com/discordjs/discord.js/commit/56c9396b717d4dec2410ca13938ce238ec21215d))
- **InteractionResponses:** Throw error on deleting response of unacknowledged interaction (#10587) ([21c283f](https://github.com/discordjs/discord.js/commit/21c283f964ab9e331db53cc0c21ca64980372488))
- **GuildScheduledEvent:** Handle null recurrence_rule (#10543) ([831aafa](https://github.com/discordjs/discord.js/commit/831aafa733e8eea55534c4c39b87775d2e2f56c4))
## Documentation
- Correct discord-api-types URLs (#10622) ([76042f0](https://github.com/discordjs/discord.js/commit/76042f05386edcbadc5ad4ded22e8b15c7b6f8ec))
- Typos (#10628) ([388783d](https://github.com/discordjs/discord.js/commit/388783d7dd718aae519801b90aa781d07b7fb64e))
- Add note about idempotence to role add/remove routes (#10586) ([565fc01](https://github.com/discordjs/discord.js/commit/565fc0192a5ed2642ff1bd615c59678b5c3cd24b))
- **Client:** Fix incorrect managers descriptions ([f79ba52](https://github.com/discordjs/discord.js/commit/f79ba52c7a1334d987e9873a8a411e92d5140116))
- **discord.js:** Remove `utf-8-validate` (#10531) ([297e959](https://github.com/discordjs/discord.js/commit/297e959f48abbfd3af58cc29cdcef139d3579821))
## Features
- **ClientApplication:** Add webhook events (#10588) ([7b2a2e3](https://github.com/discordjs/discord.js/commit/7b2a2e3a154afd69ff892da615ea75c46730f226))
- **InteractionResponses:** Support `with_response` query parameter (#10636) ([622acbc](https://github.com/discordjs/discord.js/commit/622acbcbf02c3b8e0eae4296964c3e745e19378d))
- **ClientApplication:** Add webhook events (#10588) ([ae1deac](https://github.com/discordjs/discord.js/commit/ae1deac2bf37aecda4c044bf5c28d03930bd763b))
- **EntitlementManager:** Support get entitlement (#10606) ([a367e2c](https://github.com/discordjs/discord.js/commit/a367e2c8c99ab3bfb83cdbfb65e7a5020b50b7f7))
- Add subscriptions (#10541) ([4cca33d](https://github.com/discordjs/discord.js/commit/4cca33d9b0759294c9a2dfec39d80a24a2cc1595))
- Emit reaction type on gateway events (#10598) ([bda3128](https://github.com/discordjs/discord.js/commit/bda31284bf46515747e002e86ea35d0b6910e269))
- Voice Channel Effect Send (#10318) ([34343c6](https://github.com/discordjs/discord.js/commit/34343c6afae65205d3b17b60fdd202d0937d6a46))
- **GuildMember:** Banners (#10384) ([b1ded63](https://github.com/discordjs/discord.js/commit/b1ded63e42e7349f535df4680509b9393dd8f288))
- Add ApplicationEmoji to EmojiResolvable and MessageReaction#emoji (#10477) ([1fc87a9](https://github.com/discordjs/discord.js/commit/1fc87a96987fe69722502d7574500926a4e0bfde))
- Recurring scheduled events (#10447) ([97c3237](https://github.com/discordjs/discord.js/commit/97c3237a70027f71bb3f046357a55bb730daca14))
- Message forwarding (#10464) ([c122178](https://github.com/discordjs/discord.js/commit/c12217829b46f7a60266f65af4af19cdbfcd7906))
## Refactor
- **FetchApplicationCommandOptions:** Use `Locale` over `LocaleString` (#10625) ([7ce6f2f](https://github.com/discordjs/discord.js/commit/7ce6f2fc8a8756532d71a542186d10a0aa951471))
- Use `cache.get()` for snowflakes, `resolve()` otherwise (#10626) ([dedaa5d](https://github.com/discordjs/discord.js/commit/dedaa5d657f15491910ec05102ce72affc822b97))
- Remove extra traversing (#10580) ([33533b7](https://github.com/discordjs/discord.js/commit/33533b72849d9741dae8c979734b45abbf3657a7))
- **InteractionResponses:** Deprecate ephemeral response option (#10574) ([be38f57](https://github.com/discordjs/discord.js/commit/be38f5792602ed1a79a9638aa8e629e7ad6bdd0d))
- Deprecate `reason` parameter on adding and removing thread members (#10551) ([72e0c99](https://github.com/discordjs/discord.js/commit/72e0c994547f2a9c99b320870e14d7f1643f3851))
- Deprecate fetching user flags (#10550) ([3d06c9d](https://github.com/discordjs/discord.js/commit/3d06c9d872b2e79356f1239f7d0eb0577a4bcedf))
## Testing
- Remove unused test (#10638) ([53cbb0e](https://github.com/discordjs/discord.js/commit/53cbb0e36d4ab191cbc15a022d752da14c2e0ace))
## Typings
- Add missing `Caches` managers (#10540) ([13471fa](https://github.com/discordjs/discord.js/commit/13471fa1b7c44b236db9fe9b1a64dacd41b14b76))
- Remove newMessage partial on messageUpdate event typing (#10526) ([5faf074](https://github.com/discordjs/discord.js/commit/5faf074c145044f0edefafab97fd07a8dfb8bc30))
# [14.16.3](https://github.com/discordjs/discord.js/compare/14.16.2...14.16.3) - (2024-09-29)
## Bug Fixes
- **BaseInteraction:** Add missing props (#10517) ([6c77fee](https://github.com/discordjs/discord.js/commit/6c77fee41b1aabc243bff623debd157a4c7fad6a)) by @monbrey
- `GuildChannel#guildId` not being patched to `undefined` (#10505) ([2adee06](https://github.com/discordjs/discord.js/commit/2adee06b6e92b7854ebb1c2bfd04940aab68dd10)) by @Qjuh
## Typings
- **MessageEditOptions:** Omit `poll` (#10509) ([665bf14](https://github.com/discordjs/discord.js/commit/665bf1486aec62e9528f5f7b5a6910ae6b5a6c9c)) by @TAEMBO
# [14.16.2](https://github.com/discordjs/discord.js/compare/14.16.1...14.16.2) - (2024-09-12)
## Bug Fixes
- **ApplicationCommand:** Incorrect comparison in equals method (#10497) ([3c74aa2](https://github.com/discordjs/discord.js/commit/3c74aa204909323ff6d05991438bee2c583e838b)) by @monbrey
- Type guard for sendable text-based channels (#10482) ([dea6840](https://github.com/discordjs/discord.js/commit/dea68400a38edb90b8b4242d64be14968943130d)) by @vladfrangu
## Documentation
- Update discord documentation links (#10484) ([799fa54](https://github.com/discordjs/discord.js/commit/799fa54fa4434144855be2f7a0bbac6ff8ce9d0b)) by @sdanialraza
- **Message:** Mark `interaction` as deprecated (#10481) ([c13f18e](https://github.com/discordjs/discord.js/commit/c13f18e90eb6eb315397c095e948993856428757)) by @sdanialraza
- **ApplicationEmojiManager:** Fix fetch example (#10480) ([4594896](https://github.com/discordjs/discord.js/commit/4594896b5404c6a34e07544951c59ff8f3657184)) by @sdanialraza
## Typings
- Export GroupDM helper type (#10478) ([aff772c](https://github.com/discordjs/discord.js/commit/aff772c7aa3b3de58780a94588d1f3576a434f32)) by @Qjuh
# [14.16.1](https://github.com/discordjs/discord.js/compare/14.16.0...14.16.1) - (2024-09-02)
## Bug Fixes
- **Message:** Reacting returning undefined (#10475) ([9257a09](https://github.com/discordjs/discord.js/commit/9257a09abbf80558ed2d5d209a2f6bd2a4b3d799)) by @vladfrangu
- **Transformers:** Pass client to recursive call (#10474) ([4810f7c](https://github.com/discordjs/discord.js/commit/4810f7c8637dacf77d0442bd84e0d579e1f1d3bd)) by @SpaceEEC
# [14.16.0](https://github.com/discordjs/discord.js/compare/14.15.3...14.16.0) - (2024-09-01)
## Bug Fixes

View File

@@ -29,7 +29,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation
**Node.js 18 or newer is required.**
**Node.js 16.11.0 or newer is required.**
```sh
npm install discord.js
@@ -42,6 +42,7 @@ bun add discord.js
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`)
- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`)
- [@discordjs/voice](https://www.npmjs.com/package/@discordjs/voice) for interacting with the Discord Voice API (`npm install @discordjs/voice`)
## Example usage

View File

@@ -1,6 +1,14 @@
{
"extends": "../../api-extractor.json",
"mainEntryPointFilePath": "<projectFolder>/typings/index.d.ts",
"bundledPackages": [
"discord-api-types",
"@discordjs/builders",
"@discordjs/formatters",
"@discordjs/rest",
"@discordjs/util",
"@discordjs/ws"
],
"docModel": {
"projectFolderUrl": "https://github.com/discordjs/discord.js/tree/main/packages/discord.js"
}

View File

@@ -30,10 +30,8 @@ body = """
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}
@@ -69,7 +67,8 @@ commit_parsers = [
{ body = ".*security", group = "Security"},
]
filter_commits = true
tag_pattern = "^[0-9]+"
tag_pattern = "[0-9]*"
skip_tags = "v[0-9]*|@discordjs*"
ignore_tags = ""
topo_order = false
sort_commits = "newest"

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "discord.js",
"version": "14.17.0",
"version": "14.16.0",
"description": "A powerful library for interacting with the Discord API",
"scripts": {
"test": "pnpm run docs:test && pnpm run test:typescript",
@@ -65,18 +65,18 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@discordjs/builders": "^1.10.0",
"@discordjs/builders": "workspace:^",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.0",
"@discordjs/formatters": "workspace:^",
"@discordjs/rest": "workspace:^",
"@discordjs/util": "workspace:^",
"@discordjs/ws": "^1.2.0",
"@discordjs/ws": "1.1.1",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.37.114",
"discord-api-types": "^0.38.1",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"tslib": "^2.6.3",
"undici": "6.19.8"
"undici": "6.21.1"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
@@ -98,7 +98,7 @@
"typescript": "~5.5.4"
},
"engines": {
"node": ">=18"
"node": ">=16.11.0"
},
"publishConfig": {
"provenance": true

View File

@@ -107,20 +107,20 @@ class Client extends BaseClient {
: null;
/**
* The user manager of this client
* All of the {@link User} objects that have been cached at any point, mapped by their ids
* @type {UserManager}
*/
this.users = new UserManager(this);
/**
* A manager of all the guilds the client is currently handling -
* All of the guilds the client is currently handling, mapped by their ids -
* as long as sharding isn't being used, this will be *every* guild the bot is a member of
* @type {GuildManager}
*/
this.guilds = new GuildManager(this);
/**
* All of the {@link BaseChannel}s that the client is currently handling -
* All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids -
* as long as sharding isn't being used, this will be *every* channel in *every* guild the bot
* is a member of. Note that DM channels will not be initially cached, and thus not be present
* in the Manager without their explicit fetching or use.
@@ -174,7 +174,7 @@ class Client extends BaseClient {
}
/**
* A manager of all the custom emojis that the client has access to
* All custom emojis that the client has access to, mapped by their ids
* @type {BaseGuildEmojiManager}
* @readonly
*/

View File

@@ -111,10 +111,6 @@ class GenericAction {
getThreadMember(id, manager) {
return this.getPayload({ user_id: id }, manager, id, Partials.ThreadMember, false);
}
spreadInjectedData(data) {
return Object.fromEntries(Object.getOwnPropertySymbols(data).map(symbol => [symbol, data[symbol]]));
}
}
module.exports = GenericAction;

View File

@@ -6,11 +6,7 @@ const Events = require('../../util/Events');
class MessageCreateAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel({
id: data.channel_id,
author: data.author,
...('guild_id' in data && { guild_id: data.guild_id }),
});
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id, author: data.author });
if (channel) {
if (!channel.isTextBased()) return {};

View File

@@ -6,7 +6,7 @@ const Events = require('../../util/Events');
class MessageDeleteAction extends Action {
handle(data) {
const client = this.client;
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
let message;
if (channel) {
if (!channel.isTextBased()) return {};

View File

@@ -5,7 +5,7 @@ const Events = require('../../util/Events');
class MessagePollVoteAddAction extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
if (!channel?.isTextBased()) return false;
const message = this.getMessage(data, channel);

View File

@@ -5,7 +5,7 @@ const Events = require('../../util/Events');
class MessagePollVoteRemoveAction extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
if (!channel?.isTextBased()) return false;
const message = this.getMessage(data, channel);

View File

@@ -23,13 +23,7 @@ class MessageReactionAdd extends Action {
if (!user) return false;
// Verify channel
const channel = this.getChannel({
id: data.channel_id,
...('guild_id' in data && { guild_id: data.guild_id }),
user_id: data.user_id,
...this.spreadInjectedData(data),
});
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id, user_id: data.user_id });
if (!channel?.isTextBased()) return false;
// Verify message
@@ -51,7 +45,6 @@ class MessageReactionAdd extends Action {
/**
* Provides additional information about altered reaction
* @typedef {Object} MessageReactionEventDetails
* @property {ReactionType} type The type of the reaction
* @property {boolean} burst Determines whether a super reaction was used
*/
/**
@@ -61,7 +54,7 @@ class MessageReactionAdd extends Action {
* @param {User} user The user that applied the guild or reaction emoji
* @param {MessageReactionEventDetails} details Details of adding the reaction
*/
this.client.emit(Events.MessageReactionAdd, reaction, user, { type: data.type, burst: data.burst });
this.client.emit(Events.MessageReactionAdd, reaction, user, { burst: data.burst });
return { message, reaction, user };
}

View File

@@ -19,11 +19,7 @@ class MessageReactionRemove extends Action {
if (!user) return false;
// Verify channel
const channel = this.getChannel({
id: data.channel_id,
...('guild_id' in data && { guild_id: data.guild_id }),
user_id: data.user_id,
});
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id, user_id: data.user_id });
if (!channel?.isTextBased()) return false;
// Verify message
@@ -41,7 +37,7 @@ class MessageReactionRemove extends Action {
* @param {User} user The user whose emoji or reaction emoji was removed
* @param {MessageReactionEventDetails} details Details of removing the reaction
*/
this.client.emit(Events.MessageReactionRemove, reaction, user, { type: data.type, burst: data.burst });
this.client.emit(Events.MessageReactionRemove, reaction, user, { burst: data.burst });
return { message, reaction, user };
}

View File

@@ -6,7 +6,7 @@ const Events = require('../../util/Events');
class MessageReactionRemoveAll extends Action {
handle(data) {
// Verify channel
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
if (!channel?.isTextBased()) return false;
// Verify message

View File

@@ -5,7 +5,7 @@ const Events = require('../../util/Events');
class MessageReactionRemoveEmoji extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
if (!channel?.isTextBased()) return false;
const message = this.getMessage(data, channel);

View File

@@ -4,7 +4,7 @@ const Action = require('./Action');
class MessageUpdateAction extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
if (channel) {
if (!channel.isTextBased()) return {};

View File

@@ -13,7 +13,7 @@ class ThreadListSyncAction extends Action {
if (data.channel_ids) {
for (const id of data.channel_ids) {
const channel = client.channels.cache.get(id);
const channel = client.channels.resolve(id);
if (channel) this.removeStale(channel);
}
} else {

View File

@@ -6,7 +6,7 @@ const Events = require('../../util/Events');
class TypingStart extends Action {
handle(data) {
const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) });
const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id });
if (!channel) return;
if (!channel.isTextBased()) {

View File

@@ -1,14 +0,0 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const subscription = client.application.subscriptions._add(data);
/**
* Emitted whenever a subscription is created.
* @event Client#subscriptionCreate
* @param {Subscription} subscription The subscription that was created
*/
client.emit(Events.SubscriptionCreate, subscription);
};

View File

@@ -1,16 +0,0 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const subscription = client.application.subscriptions._add(data, false);
client.application.subscriptions.cache.delete(subscription.id);
/**
* Emitted whenever a subscription is deleted.
* @event Client#subscriptionDelete
* @param {Subscription} subscription The subscription that was deleted
*/
client.emit(Events.SubscriptionDelete, subscription);
};

View File

@@ -1,16 +0,0 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const oldSubscription = client.application.subscriptions.cache.get(data.id)?._clone() ?? null;
const newSubscription = client.application.subscriptions._add(data);
/**
* Emitted whenever a subscription is updated - i.e. when a user's subscription renews.
* @event Client#subscriptionUpdate
* @param {?Subscription} oldSubscription The subscription before the update
* @param {Subscription} newSubscription The subscription after the update
*/
client.emit(Events.SubscriptionUpdate, oldSubscription, newSubscription);
};

View File

@@ -1,16 +0,0 @@
'use strict';
const VoiceChannelEffect = require('../../../structures/VoiceChannelEffect');
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const guild = client.guilds.cache.get(data.guild_id);
if (!guild) return;
/**
* Emitted when someone sends an effect, such as an emoji reaction, in a voice channel the client is connected to.
* @event Client#voiceChannelEffectSend
* @param {VoiceChannelEffect} voiceChannelEffect The sent voice channel effect
*/
client.emit(Events.VoiceChannelEffectSend, new VoiceChannelEffect(data, guild));
};

View File

@@ -53,9 +53,6 @@ const handlers = Object.fromEntries([
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],
['SUBSCRIPTION_CREATE', require('./SUBSCRIPTION_CREATE')],
['SUBSCRIPTION_DELETE', require('./SUBSCRIPTION_DELETE')],
['SUBSCRIPTION_UPDATE', require('./SUBSCRIPTION_UPDATE')],
['THREAD_CREATE', require('./THREAD_CREATE')],
['THREAD_DELETE', require('./THREAD_DELETE')],
['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')],
@@ -64,7 +61,6 @@ const handlers = Object.fromEntries([
['THREAD_UPDATE', require('./THREAD_UPDATE')],
['TYPING_START', require('./TYPING_START')],
['USER_UPDATE', require('./USER_UPDATE')],
['VOICE_CHANNEL_EFFECT_SEND', require('./VOICE_CHANNEL_EFFECT_SEND')],
['VOICE_SERVER_UPDATE', require('./VOICE_SERVER_UPDATE')],
['VOICE_STATE_UPDATE', require('./VOICE_STATE_UPDATE')],
['WEBHOOKS_UPDATE', require('./WEBHOOKS_UPDATE')],

View File

@@ -85,7 +85,6 @@ exports.ReactionManager = require('./managers/ReactionManager');
exports.ReactionUserManager = require('./managers/ReactionUserManager');
exports.RoleManager = require('./managers/RoleManager');
exports.StageInstanceManager = require('./managers/StageInstanceManager');
exports.SubscriptionManager = require('./managers/SubscriptionManager').SubscriptionManager;
exports.ThreadManager = require('./managers/ThreadManager');
exports.ThreadMemberManager = require('./managers/ThreadMemberManager');
exports.UserManager = require('./managers/UserManager');
@@ -147,9 +146,6 @@ exports.GuildScheduledEvent = require('./structures/GuildScheduledEvent').GuildS
exports.GuildTemplate = require('./structures/GuildTemplate');
exports.Integration = require('./structures/Integration');
exports.IntegrationApplication = require('./structures/IntegrationApplication');
exports.InteractionCallback = require('./structures/InteractionCallback');
exports.InteractionCallbackResource = require('./structures/InteractionCallbackResource');
exports.InteractionCallbackResponse = require('./structures/InteractionCallbackResponse');
exports.BaseInteraction = require('./structures/BaseInteraction');
exports.InteractionCollector = require('./structures/InteractionCollector');
exports.InteractionResponse = require('./structures/InteractionResponse');
@@ -206,7 +202,6 @@ exports.SKU = require('./structures/SKU').SKU;
exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder');
exports.StageChannel = require('./structures/StageChannel');
exports.StageInstance = require('./structures/StageInstance').StageInstance;
exports.Subscription = require('./structures/Subscription').Subscription;
exports.Sticker = require('./structures/Sticker').Sticker;
exports.StickerPack = require('./structures/StickerPack');
exports.Team = require('./structures/Team');
@@ -220,7 +215,6 @@ exports.ThreadOnlyChannel = require('./structures/ThreadOnlyChannel');
exports.Typing = require('./structures/Typing');
exports.User = require('./structures/User');
exports.UserContextMenuCommandInteraction = require('./structures/UserContextMenuCommandInteraction');
exports.VoiceChannelEffect = require('./structures/VoiceChannelEffect');
exports.VoiceChannel = require('./structures/VoiceChannel');
exports.VoiceRegion = require('./structures/VoiceRegion');
exports.VoiceState = require('./structures/VoiceState');

View File

@@ -82,7 +82,7 @@ class ApplicationCommandManager extends CachedManager {
* Options used to fetch Application Commands from Discord
* @typedef {BaseFetchOptions} FetchApplicationCommandOptions
* @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached
* @property {Locale} [locale] The locale to use when fetching this command
* @property {LocaleString} [locale] The locale to use when fetching this command
* @property {boolean} [withLocalizations] Whether to fetch all localization data
*/

View File

@@ -65,12 +65,12 @@ class ApplicationEmojiManager extends CachedManager {
* @returns {Promise<ApplicationEmoji|Collection<Snowflake, ApplicationEmoji>>}
* @example
* // Fetch all emojis from the application
* application.emojis.fetch()
* message.application.emojis.fetch()
* .then(emojis => console.log(`There are ${emojis.size} emojis.`))
* .catch(console.error);
* @example
* // Fetch a single emoji
* application.emojis.fetch('222078108977594368')
* message.application.emojis.fetch('222078108977594368')
* .then(emoji => console.log(`The emoji name is: ${emoji.name}`))
* .catch(console.error);
*/

View File

@@ -1,7 +1,6 @@
'use strict';
const CachedManager = require('./CachedManager');
const ApplicationEmoji = require('../structures/ApplicationEmoji');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const { parseEmoji } = require('../util/Util');
@@ -26,8 +25,7 @@ class BaseGuildEmojiManager extends CachedManager {
* * A Snowflake
* * A GuildEmoji object
* * A ReactionEmoji object
* * An ApplicationEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji|ApplicationEmoji} EmojiResolvable
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
*/
/**
@@ -36,8 +34,7 @@ class BaseGuildEmojiManager extends CachedManager {
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.cache.get(emoji.id) ?? null;
if (emoji instanceof ApplicationEmoji) return super.cache.get(emoji.id) ?? null;
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
return super.resolve(emoji);
}
@@ -48,7 +45,6 @@ class BaseGuildEmojiManager extends CachedManager {
*/
resolveId(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
if (emoji instanceof ApplicationEmoji) return emoji.id;
return super.resolveId(emoji);
}
@@ -69,7 +65,6 @@ class BaseGuildEmojiManager extends CachedManager {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (emoji instanceof ApplicationEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
const res = parseEmoji(emoji);
if (res?.name.length) {

View File

@@ -37,12 +37,6 @@ class EntitlementManager extends CachedManager {
* @typedef {SKU|Snowflake} SKUResolvable
*/
/**
* Options used to fetch an entitlement
* @typedef {BaseFetchOptions} FetchEntitlementOptions
* @property {EntitlementResolvable} entitlement The entitlement to fetch
*/
/**
* Options used to fetch entitlements
* @typedef {Object} FetchEntitlementsOptions
@@ -51,7 +45,6 @@ class EntitlementManager extends CachedManager {
* @property {UserResolvable} [user] The user to fetch entitlements for
* @property {SKUResolvable[]} [skus] The SKUs to fetch entitlements for
* @property {boolean} [excludeEnded] Whether to exclude ended entitlements
* @property {boolean} [excludeDeleted] Whether to exclude deleted entitlements
* @property {boolean} [cache=true] Whether to cache the fetched entitlements
* @property {Snowflake} [before] Consider only entitlements before this entitlement id
* @property {Snowflake} [after] Consider only entitlements after this entitlement id
@@ -60,49 +53,21 @@ class EntitlementManager extends CachedManager {
/**
* Fetches entitlements for this application
* @param {EntitlementResolvable|FetchEntitlementOptions|FetchEntitlementsOptions} [options]
* Options for fetching the entitlements
* @returns {Promise<Entitlement|Collection<Snowflake, Entitlement>>}
* @param {FetchEntitlementsOptions} [options={}] Options for fetching the entitlements
* @returns {Promise<Collection<Snowflake, Entitlement>>}
*/
async fetch(options) {
if (!options) return this._fetchMany(options);
const { entitlement, cache, force } = options;
const resolvedEntitlement = this.resolveId(entitlement ?? options);
if (resolvedEntitlement) {
return this._fetchSingle({ entitlement: resolvedEntitlement, cache, force });
}
return this._fetchMany(options);
}
async _fetchSingle({ entitlement, cache, force = false }) {
if (!force) {
const existing = this.cache.get(entitlement);
if (existing) {
return existing;
}
}
const data = await this.client.rest.get(Routes.entitlement(this.client.application.id, entitlement));
return this._add(data, cache);
}
async _fetchMany({ limit, guild, user, skus, excludeEnded, excludeDeleted, cache, before, after } = {}) {
async fetch({ limit, guild, user, skus, excludeEnded, cache = true, before, after } = {}) {
const query = makeURLSearchParams({
limit,
guild_id: guild && this.client.guilds.resolveId(guild),
user_id: user && this.client.users.resolveId(user),
sku_ids: skus?.map(sku => resolveSKUId(sku)).join(','),
exclude_ended: excludeEnded,
exclude_deleted: excludeDeleted,
before,
after,
});
const entitlements = await this.client.rest.get(Routes.entitlements(this.client.application.id), { query });
return entitlements.reduce(
(coll, entitlement) => coll.set(entitlement.id, this._add(entitlement, cache)),
new Collection(),

View File

@@ -175,7 +175,7 @@ class GuildBanManager extends CachedManager {
reason: options.reason,
});
if (user instanceof GuildMember) return user;
const _user = this.client.users.cache.get(id);
const _user = this.client.users.resolve(id);
if (_user) {
return this.guild.members.resolve(_user) ?? _user;
}

View File

@@ -84,7 +84,7 @@ class GuildChannelManager extends CachedManager {
* @returns {?(GuildChannel|ThreadChannel)}
*/
resolve(channel) {
if (channel instanceof ThreadChannel) return super.cache.get(channel.id) ?? null;
if (channel instanceof ThreadChannel) return super.resolve(channel.id);
return super.resolve(channel);
}
@@ -287,7 +287,7 @@ class GuildChannelManager extends CachedManager {
const resolvedChannel = this.resolve(channel);
if (!resolvedChannel) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'GuildChannelResolvable');
const parentId = options.parent && this.client.channels.resolveId(options.parent);
const parent = options.parent && this.client.channels.resolveId(options.parent);
if (options.position !== undefined) {
await this.setPosition(resolvedChannel, options.position, { position: options.position, reason: options.reason });
@@ -298,8 +298,8 @@ class GuildChannelManager extends CachedManager {
);
if (options.lockPermissions) {
if (parentId) {
const newParent = this.cache.get(parentId);
if (parent) {
const newParent = this.guild.channels.resolve(parent);
if (newParent?.type === ChannelType.GuildCategory) {
permission_overwrites = newParent.permissionOverwrites.cache.map(overwrite =>
PermissionOverwrites.resolve(overwrite, this.guild),
@@ -322,7 +322,7 @@ class GuildChannelManager extends CachedManager {
user_limit: options.userLimit,
rtc_region: options.rtcRegion,
video_quality_mode: options.videoQualityMode,
parent_id: parentId,
parent_id: parent,
lock_permissions: options.lockPermissions,
rate_limit_per_user: options.rateLimitPerUser,
default_auto_archive_duration: options.defaultAutoArchiveDuration,

View File

@@ -55,7 +55,7 @@ class GuildMemberManager extends CachedManager {
const memberResolvable = super.resolve(member);
if (memberResolvable) return memberResolvable;
const userResolvable = this.client.users.resolveId(member);
if (userResolvable) return super.cache.get(userResolvable) ?? null;
if (userResolvable) return super.resolve(userResolvable);
return null;
}
@@ -144,7 +144,7 @@ class GuildMemberManager extends CachedManager {
*/
get me() {
return (
this.cache.get(this.client.user.id) ??
this.resolve(this.client.user.id) ??
(this.client.options.partials.includes(Partials.GuildMember)
? this._add({ user: { id: this.client.user.id } }, true)
: null)
@@ -535,7 +535,7 @@ class GuildMemberManager extends CachedManager {
*/
async addRole(options) {
const { user, role, reason } = options;
const userId = this.resolveId(user);
const userId = this.guild.members.resolveId(user);
const roleId = this.guild.roles.resolveId(role);
await this.client.rest.put(Routes.guildMemberRole(this.guild.id, userId, roleId), { reason });
@@ -549,7 +549,7 @@ class GuildMemberManager extends CachedManager {
*/
async removeRole(options) {
const { user, role, reason } = options;
const userId = this.resolveId(user);
const userId = this.guild.members.resolveId(user);
const roleId = this.guild.roles.resolveId(role);
await this.client.rest.delete(Routes.guildMemberRole(this.guild.id, userId, roleId), { reason });

View File

@@ -101,8 +101,6 @@ class GuildMemberRoleManager extends DataManager {
/**
* Adds a role (or multiple roles) to the member.
*
* <info>Uses the idempotent PUT route for singular roles, otherwise PATCHes the underlying guild member</info>
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add
* @param {string} [reason] Reason for adding the role(s)
* @returns {Promise<GuildMember>}
@@ -140,8 +138,6 @@ class GuildMemberRoleManager extends DataManager {
/**
* Removes a role (or multiple roles) from the member.
*
* <info>Uses the idempotent DELETE route for singular roles, otherwise PATCHes the underlying guild member</info>
* @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove
* @param {string} [reason] Reason for removing the role(s)
* @returns {Promise<GuildMember>}

View File

@@ -7,7 +7,6 @@ const CachedManager = require('./CachedManager');
const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../errors');
const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent');
const { resolveImage } = require('../util/DataResolver');
const { _transformGuildScheduledEventRecurrenceRule } = require('../util/Transformers');
/**
* Manages API methods for GuildScheduledEvents and stores their cache.
@@ -37,21 +36,6 @@ class GuildScheduledEventManager extends CachedManager {
* @typedef {Snowflake|GuildScheduledEvent} GuildScheduledEventResolvable
*/
/**
* Options for setting a recurrence rule for a guild scheduled event.
* @typedef {Object} GuildScheduledEventRecurrenceRuleOptions
* @property {DateResolvable} startAt The time the recurrence rule interval starts at
* @property {?DateResolvable} endAt The time the recurrence rule interval ends at
* @property {GuildScheduledEventRecurrenceRuleFrequency} frequency How often the event occurs
* @property {number} interval The spacing between the events
* @property {?GuildScheduledEventRecurrenceRuleWeekday[]} byWeekday The days within a week to recur on
* @property {?GuildScheduledEventRecurrenceRuleNWeekday[]} byNWeekday The days within a week to recur on
* @property {?GuildScheduledEventRecurrenceRuleMonth[]} byMonth The months to recur on
* @property {?number[]} byMonthDay The days within a month to recur on
* @property {?number[]} byYearDay The days within a year to recur on
* @property {?number} count The total amount of times the event is allowed to recur before stopping
*/
/**
* Options used to create a guild scheduled event.
* @typedef {Object} GuildScheduledEventCreateOptions
@@ -70,8 +54,6 @@ class GuildScheduledEventManager extends CachedManager {
* <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for creating the guild scheduled event
* @property {GuildScheduledEventRecurrenceRuleOptions} [recurrenceRule]
* The recurrence rule of the guild scheduled event
*/
/**
@@ -99,7 +81,6 @@ class GuildScheduledEventManager extends CachedManager {
entityMetadata,
reason,
image,
recurrenceRule,
} = options;
let entity_metadata, channel_id;
@@ -123,7 +104,6 @@ class GuildScheduledEventManager extends CachedManager {
entity_type: entityType,
entity_metadata,
image: image && (await resolveImage(image)),
recurrence_rule: recurrenceRule && _transformGuildScheduledEventRecurrenceRule(recurrenceRule),
},
reason,
});
@@ -173,7 +153,10 @@ class GuildScheduledEventManager extends CachedManager {
return data.reduce(
(coll, rawGuildScheduledEventData) =>
coll.set(rawGuildScheduledEventData.id, this._add(rawGuildScheduledEventData, options.cache)),
coll.set(
rawGuildScheduledEventData.id,
this.guild.scheduledEvents._add(rawGuildScheduledEventData, options.cache),
),
new Collection(),
);
}
@@ -195,8 +178,6 @@ class GuildScheduledEventManager extends CachedManager {
* {@link GuildScheduledEventEntityType.External}</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for editing the guild scheduled event
* @property {?GuildScheduledEventRecurrenceRuleOptions} [recurrenceRule]
* The recurrence rule of the guild scheduled event
*/
/**
@@ -222,7 +203,6 @@ class GuildScheduledEventManager extends CachedManager {
entityMetadata,
reason,
image,
recurrenceRule,
} = options;
let entity_metadata;
@@ -244,7 +224,6 @@ class GuildScheduledEventManager extends CachedManager {
status,
image: image && (await resolveImage(image)),
entity_metadata,
recurrence_rule: recurrenceRule && _transformGuildScheduledEventRecurrenceRule(recurrenceRule),
},
reason,
});

View File

@@ -38,8 +38,8 @@ class PresenceManager extends CachedManager {
resolve(presence) {
const presenceResolvable = super.resolve(presence);
if (presenceResolvable) return presenceResolvable;
const userId = this.client.users.resolveId(presence);
return super.cache.get(userId) ?? null;
const UserResolvable = this.client.users.resolveId(presence);
return super.resolve(UserResolvable);
}
/**
@@ -50,8 +50,8 @@ class PresenceManager extends CachedManager {
resolveId(presence) {
const presenceResolvable = super.resolveId(presence);
if (presenceResolvable) return presenceResolvable;
const userId = this.client.users.resolveId(presence);
return this.cache.has(userId) ? userId : null;
const userResolvable = this.client.users.resolveId(presence);
return this.cache.has(userResolvable) ? userResolvable : null;
}
}

View File

@@ -1,81 +0,0 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const { DiscordjsTypeError, ErrorCodes } = require('../errors/index');
const { Subscription } = require('../structures/Subscription');
const { resolveSKUId } = require('../util/Util');
/**
* Manages API methods for subscriptions and stores their cache.
* @extends {CachedManager}
*/
class SubscriptionManager extends CachedManager {
constructor(client, iterable) {
super(client, Subscription, iterable);
}
/**
* The cache of this manager
* @type {Collection<Snowflake, Subscription>}
* @name SubscriptionManager#cache
*/
/**
* Options used to fetch a subscription
* @typedef {BaseFetchOptions} FetchSubscriptionOptions
* @property {SKUResolvable} sku The SKU to fetch the subscription for
* @property {Snowflake} subscriptionId The id of the subscription to fetch
*/
/**
* Options used to fetch subscriptions
* @typedef {Object} FetchSubscriptionsOptions
* @property {Snowflake} [after] Consider only subscriptions after this subscription id
* @property {Snowflake} [before] Consider only subscriptions before this subscription id
* @property {number} [limit] The maximum number of subscriptions to fetch
* @property {SKUResolvable} sku The SKU to fetch subscriptions for
* @property {UserResolvable} user The user to fetch entitlements for
* <warn>If both `before` and `after` are provided, only `before` is respected</warn>
*/
/**
* Fetches subscriptions for this application
* @param {FetchSubscriptionOptions|FetchSubscriptionsOptions} [options={}] Options for fetching the subscriptions
* @returns {Promise<Subscription|Collection<Snowflake, Subscription>>}
*/
async fetch(options = {}) {
if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true);
const { after, before, cache, limit, sku, subscriptionId, user } = options;
const skuId = resolveSKUId(sku);
if (!skuId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'sku', 'SKUResolvable');
if (subscriptionId) {
const subscription = await this.client.rest.get(Routes.skuSubscription(skuId, subscriptionId));
return this._add(subscription, cache);
}
const query = makeURLSearchParams({
limit,
user_id: this.client.users.resolveId(user) ?? undefined,
sku_id: skuId,
before,
after,
});
const subscriptions = await this.client.rest.get(Routes.skuSubscriptions(skuId), { query });
return subscriptions.reduce(
(coll, subscription) => coll.set(subscription.id, this._add(subscription, cache)),
new Collection(),
);
}
}
exports.SubscriptionManager = SubscriptionManager;

View File

@@ -1,15 +1,11 @@
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const { DiscordjsTypeError, ErrorCodes } = require('../errors');
const ThreadMember = require('../structures/ThreadMember');
const { emitDeprecationWarningForRemoveThreadMember } = require('../util/Util');
let deprecationEmittedForAdd = false;
/**
* Manages API methods for GuildMembers and stores their cache.
@@ -57,7 +53,7 @@ class ThreadMemberManager extends CachedManager {
* @readonly
*/
get me() {
return this.cache.get(this.client.user.id) ?? null;
return this.resolve(this.client.user.id);
}
/**
@@ -75,8 +71,8 @@ class ThreadMemberManager extends CachedManager {
resolve(member) {
const memberResolvable = super.resolve(member);
if (memberResolvable) return memberResolvable;
const userId = this.client.users.resolveId(member);
if (userId) return super.cache.get(userId) ?? null;
const userResolvable = this.client.users.resolveId(member);
if (userResolvable) return super.resolve(userResolvable);
return null;
}
@@ -96,20 +92,9 @@ class ThreadMemberManager extends CachedManager {
* Adds a member to the thread.
* @param {UserResolvable|'@me'} member The member to add
* @param {string} [reason] The reason for adding this member
* <warn>This parameter is **deprecated**. Reasons cannot be used.</warn>
* @returns {Promise<Snowflake>}
*/
async add(member, reason) {
if (reason !== undefined && !deprecationEmittedForAdd) {
process.emitWarning(
// eslint-disable-next-line max-len
'The reason parameter of ThreadMemberManager#add() is deprecated as Discord does not parse them. It will be removed in the next major version.',
'DeprecationWarning',
);
deprecationEmittedForAdd = true;
}
const id = member === '@me' ? member : this.client.users.resolveId(member);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'member', 'UserResolvable');
await this.client.rest.put(Routes.threadMembers(this.thread.id, id), { reason });
@@ -120,14 +105,9 @@ class ThreadMemberManager extends CachedManager {
* Remove a user from the thread.
* @param {UserResolvable|'@me'} member The member to remove
* @param {string} [reason] The reason for removing this member from the thread
* <warn>This parameter is **deprecated**. Reasons cannot be used.</warn>
* @returns {Promise<Snowflake>}
*/
async remove(member, reason) {
if (reason !== undefined) {
emitDeprecationWarningForRemoveThreadMember(this.constructor.name);
}
const id = member === '@me' ? member : this.client.users.resolveId(member);
if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'member', 'UserResolvable');
await this.client.rest.delete(Routes.threadMembers(this.thread.id, id), { reason });

View File

@@ -7,7 +7,6 @@ const { GuildMember } = require('../structures/GuildMember');
const { Message } = require('../structures/Message');
const ThreadMember = require('../structures/ThreadMember');
const User = require('../structures/User');
const { emitDeprecationWarningForUserFetchFlags } = require('../util/Util');
/**
* Manages API methods for users and stores their cache.
@@ -101,11 +100,8 @@ class UserManager extends CachedManager {
* @param {UserResolvable} user The UserResolvable to identify
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<UserFlagsBitField>}
* @deprecated <warn>This method is deprecated and will be removed in the next major version.
* Flags may still be retrieved via {@link UserManager#fetch}.</warn>
*/
async fetchFlags(user, options) {
emitDeprecationWarningForUserFetchFlags(this.constructor.name);
return (await this.fetch(user, options)).flags;
}

View File

@@ -8,7 +8,7 @@ const { makeError, makePlainError } = require('../util/Util');
/**
* Helper class for sharded clients spawned as a child process/worker, such as from a {@link ShardingManager}.
* Utilizes IPC to send and receive data to/from the master process and other shards.
* Utilises IPC to send and receive data to/from the master process and other shards.
*/
class ShardClientUtil {
constructor(client, mode) {

View File

@@ -14,7 +14,7 @@ const { fetchRecommendedShardCount } = require('../util/Util');
* This is a utility class that makes multi-process sharding of a bot an easy and painless experience.
* It works by spawning a self-contained {@link ChildProcess} or {@link Worker} for each individual shard, each
* containing its own instance of your bot's {@link Client}. They all have a line of communication with the master
* process, and there are several useful methods that utilize it in order to simplify tasks that are normally difficult
* process, and there are several useful methods that utilise it in order to simplify tasks that are normally difficult
* with sharding. It can spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a
* path to your main bot script to launch for each one.
* @extends {EventEmitter}

View File

@@ -418,7 +418,7 @@ class ApplicationCommand extends Base {
command.descriptionLocalizations ?? command.description_localizations ?? {},
this.descriptionLocalizations ?? {},
) ||
!isEqual(command.integrationTypes ?? command.integration_types ?? [], this.integrationTypes ?? []) ||
!isEqual(command.integrationTypes ?? command.integration_types ?? [], this.integrationTypes ?? {}) ||
!isEqual(command.contexts ?? [], this.contexts ?? [])
) {
return false;

View File

@@ -155,14 +155,6 @@ class BaseChannel extends Base {
return 'availableTags' in this;
}
/**
* Indicates whether this channel is sendable.
* @returns {boolean}
*/
isSendable() {
return 'send' in this;
}
toJSON(...props) {
return super.toJSON({ createdTimestamp: true }, ...props);
}

View File

@@ -107,21 +107,6 @@ class BaseInteraction extends Base {
(coll, entitlement) => coll.set(entitlement.id, this.client.application.entitlements._add(entitlement)),
new Collection(),
);
/* eslint-disable max-len */
/**
* Mapping of installation contexts that the interaction was authorized for the related user or guild ids
* @type {APIAuthorizingIntegrationOwnersMap}
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object}
*/
this.authorizingIntegrationOwners = data.authorizing_integration_owners;
/* eslint-enable max-len */
/**
* Context where the interaction was triggered from
* @type {?InteractionContextType}
*/
this.context = data.context ?? null;
}
/**

View File

@@ -9,7 +9,6 @@ const Application = require('./interfaces/Application');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
const ApplicationEmojiManager = require('../managers/ApplicationEmojiManager');
const { EntitlementManager } = require('../managers/EntitlementManager');
const { SubscriptionManager } = require('../managers/SubscriptionManager');
const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField');
const { resolveImage } = require('../util/DataResolver');
const PermissionsBitField = require('../util/PermissionsBitField');
@@ -45,12 +44,6 @@ class ClientApplication extends Application {
* @type {EntitlementManager}
*/
this.entitlements = new EntitlementManager(this.client);
/**
* The subscription manager for this application
* @type {SubscriptionManager}
*/
this.subscriptions = new SubscriptionManager(this.client);
}
_patch(data) {
@@ -243,36 +236,6 @@ class ClientApplication extends Application {
this.roleConnectionsVerificationURL ??= null;
}
if ('event_webhooks_url' in data) {
/**
* This application's URL to receive event webhooks
* @type {?string}
*/
this.eventWebhooksURL = data.event_webhooks_url;
} else {
this.eventWebhooksURL ??= null;
}
if ('event_webhooks_status' in data) {
/**
* This application's event webhooks status
* @type {?ApplicationWebhookEventStatus}
*/
this.eventWebhooksStatus = data.event_webhooks_status;
} else {
this.eventWebhooksStatus ??= null;
}
if ('event_webhooks_types' in data) {
/**
* List of event webhooks types this application subscribes to
* @type {?ApplicationWebhookEventType[]}
*/
this.eventWebhooksTypes = data.event_webhooks_types;
} else {
this.eventWebhooksTypes ??= null;
}
/**
* The owner of this OAuth application
* @type {?(User|Team)}
@@ -314,10 +277,6 @@ class ClientApplication extends Application {
* @property {?(BufferResolvable|Base64Resolvable)} [icon] The application's icon
* @property {?(BufferResolvable|Base64Resolvable)} [coverImage] The application's cover image
* @property {string} [interactionsEndpointURL] The application's interaction endpoint URL
* @property {string} [eventWebhooksURL] The application's event webhooks URL
* @property {ApplicationWebhookEventStatus.Enabled|ApplicationWebhookEventStatus.Disabled} [eventWebhooksStatus]
* The application's event webhooks status.
* @property {ApplicationWebhookEventType[]} [eventWebhooksTypes] The application's event webhooks types
* @property {string[]} [tags] The application's tags
*/
@@ -335,9 +294,6 @@ class ClientApplication extends Application {
icon,
coverImage,
interactionsEndpointURL,
eventWebhooksURL,
eventWebhooksStatus,
eventWebhooksTypes,
tags,
} = {}) {
const data = await this.client.rest.patch(Routes.currentApplication(), {
@@ -350,9 +306,6 @@ class ClientApplication extends Application {
icon: icon && (await resolveImage(icon)),
cover_image: coverImage && (await resolveImage(coverImage)),
interactions_endpoint_url: interactionsEndpointURL,
event_webhooks_url: eventWebhooksURL,
event_webhooks_status: eventWebhooksStatus,
event_webhooks_types: eventWebhooksTypes,
tags,
},
});

View File

@@ -45,6 +45,21 @@ class CommandInteraction extends BaseInteraction {
*/
this.commandGuildId = data.data.guild_id ?? null;
/* eslint-disable max-len */
/**
* Mapping of installation contexts that the interaction was authorized for the related user or guild ids
* @type {APIAuthorizingIntegrationOwnersMap}
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object}
*/
this.authorizingIntegrationOwners = data.authorizing_integration_owners;
/* eslint-enable max-len */
/**
* Context where the interaction was triggered from
* @type {?InteractionContextType}
*/
this.context = data.context ?? null;
/**
* Whether the reply to this interaction has been deferred
* @type {boolean}

View File

@@ -73,9 +73,10 @@ class Entitlement extends Base {
if ('starts_at' in data) {
/**
* The timestamp at which this entitlement is valid
* <info>This is only `null` for test entitlements</info>
* @type {?number}
*/
this.startsTimestamp = data.starts_at ? Date.parse(data.starts_at) : null;
this.startsTimestamp = Date.parse(data.starts_at);
} else {
this.startsTimestamp ??= null;
}
@@ -83,9 +84,10 @@ class Entitlement extends Base {
if ('ends_at' in data) {
/**
* The timestamp at which this entitlement is no longer valid
* <info>This is only `null` for test entitlements</info>
* @type {?number}
*/
this.endsTimestamp = data.ends_at ? Date.parse(data.ends_at) : null;
this.endsTimestamp = Date.parse(data.ends_at);
} else {
this.endsTimestamp ??= null;
}
@@ -112,6 +114,7 @@ class Entitlement extends Base {
/**
* The start date at which this entitlement is valid
* <info>This is only `null` for test entitlements</info>
* @type {?Date}
*/
get startsAt() {
@@ -120,6 +123,7 @@ class Entitlement extends Base {
/**
* The end date at which this entitlement is no longer valid
* <info>This is only `null` for test entitlements</info>
* @type {?Date}
*/
get endsAt() {

View File

@@ -84,17 +84,6 @@ class GuildMember extends Base {
} else if (typeof this.avatar !== 'string') {
this.avatar = null;
}
if ('banner' in data) {
/**
* The guild member's banner hash.
* @type {?string}
*/
this.banner = data.banner;
} else {
this.banner ??= null;
}
if ('joined_at' in data) this.joinedTimestamp = Date.parse(data.joined_at);
if ('premium_since' in data) {
this.premiumSinceTimestamp = data.premium_since ? Date.parse(data.premium_since) : null;
@@ -166,15 +155,6 @@ class GuildMember extends Base {
return this.avatar && this.client.rest.cdn.guildMemberAvatar(this.guild.id, this.id, this.avatar, options);
}
/**
* A link to the member's banner.
* @param {ImageURLOptions} [options={}] Options for the banner URL
* @returns {?string}
*/
bannerURL(options = {}) {
return this.banner && this.client.rest.cdn.guildMemberBanner(this.guild.id, this.id, this.banner, options);
}
/**
* A link to the member's guild avatar if they have one.
* Otherwise, a link to their {@link User#displayAvatarURL} will be returned.
@@ -185,16 +165,6 @@ class GuildMember extends Base {
return this.avatarURL(options) ?? this.user.displayAvatarURL(options);
}
/**
* A link to the member's guild banner if they have one.
* Otherwise, a link to their {@link User#bannerURL} will be returned.
* @param {ImageURLOptions} [options={}] Options for the image URL
* @returns {?string}
*/
displayBannerURL(options) {
return this.bannerURL(options) ?? this.user.bannerURL(options);
}
/**
* The time this member joined the guild
* @type {?Date}
@@ -228,7 +198,7 @@ class GuildMember extends Base {
* @readonly
*/
get presence() {
return this.guild.presences.cache.get(this.id) ?? null;
return this.guild.presences.resolve(this.id);
}
/**
@@ -494,7 +464,6 @@ class GuildMember extends Base {
this.joinedTimestamp === member.joinedTimestamp &&
this.nickname === member.nickname &&
this.avatar === member.avatar &&
this.banner === member.banner &&
this.pending === member.pending &&
this.communicationDisabledUntilTimestamp === member.communicationDisabledUntilTimestamp &&
this.flags.bitfield === member.flags.bitfield &&
@@ -522,9 +491,7 @@ class GuildMember extends Base {
roles: true,
});
json.avatarURL = this.avatarURL();
json.bannerURL = this.bannerURL();
json.displayAvatarURL = this.displayAvatarURL();
json.displayBannerURL = this.displayBannerURL();
return json;
}
}

View File

@@ -79,7 +79,7 @@ class GuildOnboardingPromptOption extends Base {
*/
get emoji() {
if (!this._emoji.id && !this._emoji.name) return null;
return this.client.emojis.cache.get(this._emoji.id) ?? new Emoji(this.client, this._emoji);
return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji);
}
}

View File

@@ -189,56 +189,6 @@ class GuildScheduledEvent extends Base {
} else {
this.image ??= null;
}
/**
* Represents the recurrence rule for a {@link GuildScheduledEvent}.
* @typedef {Object} GuildScheduledEventRecurrenceRule
* @property {number} startTimestamp The timestamp the recurrence rule interval starts at
* @property {Date} startAt The time the recurrence rule interval starts at
* @property {?number} endTimestamp The timestamp the recurrence rule interval ends at
* @property {?Date} endAt The time the recurrence rule interval ends at
* @property {GuildScheduledEventRecurrenceRuleFrequency} frequency How often the event occurs
* @property {number} interval The spacing between the events
* @property {?GuildScheduledEventRecurrenceRuleWeekday[]} byWeekday The days within a week to recur on
* @property {?GuildScheduledEventRecurrenceRuleNWeekday[]} byNWeekday The days within a week to recur on
* @property {?GuildScheduledEventRecurrenceRuleMonth[]} byMonth The months to recur on
* @property {?number[]} byMonthDay The days within a month to recur on
* @property {?number[]} byYearDay The days within a year to recur on
* @property {?number} count The total amount of times the event is allowed to recur before stopping
*/
/**
* @typedef {Object} GuildScheduledEventRecurrenceRuleNWeekday
* @property {number} n The week to recur on
* @property {GuildScheduledEventRecurrenceRuleWeekday} day The day within the week to recur on
*/
if ('recurrence_rule' in data) {
/**
* The recurrence rule for this scheduled event
* @type {?GuildScheduledEventRecurrenceRule}
*/
this.recurrenceRule = data.recurrence_rule && {
startTimestamp: Date.parse(data.recurrence_rule.start),
get startAt() {
return new Date(this.startTimestamp);
},
endTimestamp: data.recurrence_rule.end && Date.parse(data.recurrence_rule.end),
get endAt() {
return this.endTimestamp && new Date(this.endTimestamp);
},
frequency: data.recurrence_rule.frequency,
interval: data.recurrence_rule.interval,
byWeekday: data.recurrence_rule.by_weekday,
byNWeekday: data.recurrence_rule.by_n_weekday,
byMonth: data.recurrence_rule.by_month,
byMonthDay: data.recurrence_rule.by_month_day,
byYearDay: data.recurrence_rule.by_year_day,
count: data.recurrence_rule.count,
};
} else {
this.recurrenceRule ??= null;
}
}
/**

View File

@@ -1,74 +0,0 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
/**
* Represents an interaction callback response from Discord
*/
class InteractionCallback {
constructor(client, data) {
/**
* The client that instantiated this.
* @name InteractionCallback#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The id of the original interaction response
* @type {Snowflake}
*/
this.id = data.id;
/**
* The type of the original interaction
* @type {InteractionType}
*/
this.type = data.type;
/**
* The instance id of the Activity if one was launched or joined
* @type {?string}
*/
this.activityInstanceId = data.activity_instance_id ?? null;
/**
* The id of the message that was created by the interaction
* @type {?Snowflake}
*/
this.responseMessageId = data.response_message_id ?? null;
/**
* Whether the message is in a loading state
* @type {?boolean}
*/
this.responseMessageLoading = data.response_message_loading ?? null;
/**
* Whether the response message was ephemeral
* @type {?boolean}
*/
this.responseMessageEphemeral = data.response_message_ephemeral ?? null;
}
/**
* The timestamp the original interaction was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return DiscordSnowflake.timestampFrom(this.id);
}
/**
* The time the original interaction was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
}
module.exports = InteractionCallback;

View File

@@ -1,52 +0,0 @@
'use strict';
const { lazy } = require('@discordjs/util');
const getMessage = lazy(() => require('./Message').Message);
/**
* Represents the resource that was created by the interaction response.
*/
class InteractionCallbackResource {
constructor(client, data) {
/**
* The client that instantiated this
* @name InteractionCallbackResource#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The interaction callback type
* @type {InteractionResponseType}
*/
this.type = data.type;
/**
* The Activity launched by an interaction
* @typedef {Object} ActivityInstance
* @property {string} id The instance id of the Activity
*/
/**
* Represents the Activity launched by this interaction
* @type {?ActivityInstance}
*/
this.activityInstance = data.activity_instance ?? null;
if ('message' in data) {
/**
* The message created by the interaction
* @type {?Message}
*/
this.message =
this.client.channels.cache.get(data.message.channel_id)?.messages._add(data.message) ??
new (getMessage())(client, data.message);
} else {
this.message = null;
}
}
}
module.exports = InteractionCallbackResource;

View File

@@ -1,33 +0,0 @@
'use strict';
const InteractionCallback = require('./InteractionCallback');
const InteractionCallbackResource = require('./InteractionCallbackResource');
/**
* Represents an interaction's response
*/
class InteractionCallbackResponse {
constructor(client, data) {
/**
* The client that instantiated this
* @name InteractionCallbackResponse#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The interaction object associated with the interaction callback response
* @type {InteractionCallback}
*/
this.interaction = new InteractionCallback(client, data.interaction);
/**
* The resource that was created by the interaction response
* @type {?InteractionCallbackResource}
*/
this.resource = data.resource ? new InteractionCallbackResource(client, data.resource) : null;
}
}
module.exports = InteractionCallbackResponse;

View File

@@ -153,8 +153,8 @@ class InteractionCollector extends Collector {
if (this.messageId && interaction.message?.id !== this.messageId) return null;
if (
this.messageInteractionId &&
interaction.message?.interactionMetadata?.id &&
interaction.message.interactionMetadata.id !== this.messageInteractionId
interaction.message?.interaction?.id &&
interaction.message.interaction.id !== this.messageInteractionId
) {
return null;
}
@@ -180,8 +180,8 @@ class InteractionCollector extends Collector {
if (this.messageId && interaction.message?.id !== this.messageId) return null;
if (
this.messageInteractionId &&
interaction.message?.interactionMetadata?.id &&
interaction.message.interactionMetadata.id !== this.messageInteractionId
interaction.message?.interaction?.id &&
interaction.message.interaction.id !== this.messageInteractionId
) {
return null;
}
@@ -224,7 +224,7 @@ class InteractionCollector extends Collector {
this.stop('messageDelete');
}
if (message.interactionMetadata?.id === this.messageInteractionId) {
if (message.interaction?.id === this.messageInteractionId) {
this.stop('messageDelete');
}
}

View File

@@ -40,7 +40,7 @@ class Invite extends Base {
*/
this.guild ??= null;
if (data.guild) {
this.guild = this.client.guilds.cache.get(data.guild.id) ?? new InviteGuild(this.client, data.guild);
this.guild = this.client.guilds.resolve(data.guild.id) ?? new InviteGuild(this.client, data.guild);
}
if ('code' in data) {

View File

@@ -359,12 +359,11 @@ class Message extends Base {
* * {@link MessageType.ChannelFollowAdd}
* * {@link MessageType.Reply}
* * {@link MessageType.ThreadStarterMessage}
* @see {@link https://discord.com/developers/docs/resources/message#message-object-message-types}
* @see {@link https://discord.com/developers/docs/resources/channel#message-types}
* @typedef {Object} MessageReference
* @property {Snowflake} channelId The channel id that was referenced
* @property {Snowflake|undefined} guildId The guild id that was referenced
* @property {Snowflake|undefined} messageId The message id that was referenced
* @property {MessageReferenceType} type The type of message reference
*/
if ('message_reference' in data) {
@@ -376,7 +375,6 @@ class Message extends Base {
channelId: data.message_reference.channel_id,
guildId: data.message_reference.guild_id,
messageId: data.message_reference.message_id,
type: data.message_reference.type,
};
} else {
this.reference ??= null;
@@ -428,7 +426,6 @@ class Message extends Base {
/**
* Partial data of the interaction that this message is a reply to
* @type {?MessageInteraction}
* @deprecated Use {@link Message#interactionMetadata} instead.
*/
this.interaction = {
id: data.interaction.id,
@@ -450,29 +447,6 @@ class Message extends Base {
this.poll ??= null;
}
if (data.message_snapshots) {
/**
* The message associated with the message reference
* @type {Collection<Snowflake, Message>}
*/
this.messageSnapshots = data.message_snapshots.reduce((coll, snapshot) => {
const channel = this.client.channels.resolve(this.reference.channelId);
const snapshotData = {
...snapshot.message,
id: this.reference.messageId,
channel_id: this.reference.channelId,
guild_id: this.reference.guildId,
};
return coll.set(
this.reference.messageId,
channel ? channel.messages._add(snapshotData) : new this.constructor(this.client, snapshotData),
);
}, new Collection());
} else {
this.messageSnapshots ??= new Collection();
}
/**
* A call associated with a message
* @typedef {Object} MessageCall
@@ -570,7 +544,7 @@ class Message extends Base {
* @readonly
*/
get thread() {
return this.channel?.threads?.cache.get(this.id) ?? null;
return this.channel?.threads?.resolve(this.id) ?? null;
}
/**

Some files were not shown because too many files have changed in this diff Show More