Compare commits

...

35 Commits

Author SHA1 Message Date
iCrawl
5ca3974301 chore(release): version 2022-06-06 14:44:25 +02:00
iCrawl
3202c91c5a chore: properly adapt changelog script 2022-06-06 14:43:15 +02:00
Jiralite
f3f34f07b3 docs(DirectoryChannel): Extend Channel (#8022) 2022-06-06 14:24:35 +02:00
Parbez
f8ed71bfca fix: readd isThread type guard (#8019) 2022-06-06 14:24:03 +02:00
Voxelli
e1176faa27 feat: backport handle zombie connection (#7626)
* feat: backport zombie connection fixes

* fix: enums

* fix: prettier

* feat: add zombie connection event to shard events

* Apply suggestions from code review

Co-authored-by: muchnameless <12682826+muchnameless@users.noreply.github.com>

* fix: prettier

* fix: handleZombieConnection

* feat: backport new logic of handling zombie connection

Co-authored-by: muchnameless <12682826+muchnameless@users.noreply.github.com>
2022-06-06 12:21:35 +02:00
Caleb Lam
aa59a409b3 feat(CommandInteraction): add 'commandGuildId' (#8018) 2022-06-06 10:07:37 +02:00
Rodry
7fa698d23e types(AttachmentBuilder): fix data type (#8016)
* types(AttachmentBuilder): fix data type

* types(MessageOptions): add AttachmentPayload
2022-06-06 10:07:15 +02:00
markisha64
b9df37a89f refact: Sticker's tags property (#8010) 2022-06-05 23:29:31 +02:00
Rodry
ad75be9a9c feat: allow builders to accept rest params and arrays (#7874)
Co-authored-by: Parbez <imranbarbhuiya.fsd@gmail.com>
Co-authored-by: Khafra <42794878+KhafraDev@users.noreply.github.com>
2022-06-05 23:29:16 +02:00
Suneet Tipirneni
70c733bb9a refactor(channel): remove redundant channel type guards (#8012) 2022-06-05 23:29:05 +02:00
Rodry
a3287782b5 feat(MessageReaction): add react method (#7810) 2022-06-05 23:28:44 +02:00
Suneet Tipirneni
0ccc243c8f types(modal): fix showModal() typings (#8014) 2022-06-05 23:28:28 +02:00
Almeida
0a7953e463 docs(Attachment): remove constructor doc (#8009) 2022-06-05 23:28:18 +02:00
Parbez
6aa623240e types: fix some attachment related typings (#8013) 2022-06-05 22:37:56 +02:00
n1ck_pro
6266b0c1e3 types(AttachmentBuilder): remove name parameter from constructor (#8008) 2022-06-05 22:37:01 +02:00
Suneet Tipirneni
9720e55534 refactor: always return Message instances in interactions (#7917)
Co-authored-by: Almeida <almeidx@pm.me>
2022-06-05 19:00:35 +02:00
Parbez
ad36c0be77 fix: add static method from in builders (#7990) 2022-06-05 18:33:19 +02:00
SpaceEEC
5987dbe5cf docs(VoiceChannel): annotate that it is implementing TextBasedChannel (#8007) 2022-06-05 18:30:14 +02:00
Rodry
5244fe3c1c feat(Collector): add ignore event (#7644)
Co-authored-by: Parbez <imranbarbhuiya.fsd@gmail.com>
2022-06-05 18:29:13 +02:00
Synbulat Biishev
349766dd69 feat(GuildMemberManager): add GuildMemberManager#fetchMe() (#7526) 2022-06-05 18:29:00 +02:00
markisha64
7a1095b66b fix: typings (#7965)
Co-authored-by: Almeida <almeidx@pm.me>
2022-06-05 09:36:59 +02:00
iCrawl
10009a43ee ci: remove cache-dir from test 2022-06-05 01:14:39 +02:00
iCrawl
8d8e6c03de feat: use vitest instead of jest for more speed 2022-06-05 01:07:10 +02:00
Suneet Tipirneni
dfadcbc2fd refactor(attachment): don't return attachment builders from API (#7852)
Co-authored-by: Almeida <almeidx@pm.me>
Co-authored-by: A. Román <kyradiscord@gmail.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2022-06-04 22:33:13 +02:00
n1ck_pro
546d48655f docs: add missing discord-api-types external types (#8001) 2022-06-04 22:27:01 +02:00
iCrawl
f8739bd9c0 fix(voice): re-add accidental removal of postbuild script 2022-06-04 19:15:28 +02:00
iCrawl
f2ae1f9348 feat: add scripts package for locally used scripts 2022-06-04 19:07:50 +02:00
iCrawl
3401fd4eb6 chore: remove leftover files from pre-monorepo era 2022-06-04 18:41:37 +02:00
iCrawl
fdbd229832 ci: equality check on tag and package name 2022-06-04 17:57:07 +02:00
iCrawl
9e8e2411c1 ci: check for the correct prop to be defined 2022-06-04 17:51:31 +02:00
iCrawl
c02ced9a22 ci: fix if checks for branch/tag when using workflow dispatch 2022-06-04 17:37:00 +02:00
iCrawl
8c5a7f80ef ci: fix inputs for workflow dispatches 2022-06-04 17:15:08 +02:00
iCrawl
14018b0118 ci: allow workflow dispatches 2022-06-04 17:13:40 +02:00
iCrawl
8095723604 chore: only build cjs for actions 2022-06-04 17:08:26 +02:00
iCrawl
e17bb54c85 chore: update dev versions 2022-06-04 17:01:47 +02:00
122 changed files with 1890 additions and 1320 deletions

View File

@@ -7,6 +7,11 @@ on:
- '!docs'
tags:
- '**'
workflow_dispatch:
inputs:
ref:
description: 'The branch, tag or SHA to checkout'
required: true
jobs:
build:
name: Build documentation
@@ -19,6 +24,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.ref || '' }}
- name: Install node.js v16
uses: actions/setup-node@v2
@@ -65,7 +72,7 @@ jobs:
package: ['builders', 'collection', 'discord.js', 'proxy', 'rest', 'voice']
runs-on: ubuntu-latest
env:
BRANCH_NAME: ${{ needs.build.outputs.BRANCH_NAME }}
BRANCH_NAME: ${{ github.event.inputs.ref || needs.build.outputs.BRANCH_NAME }}
BRANCH_OR_TAG: ${{ needs.build.outputs.BRANCH_OR_TAG }}
SHA: ${{ needs.build.outputs.SHA }}
steps:
@@ -108,14 +115,14 @@ jobs:
path: 'out'
- name: Extract package and semver from tag
if: env.BRANCH_OR_TAG == 'tag'
if: ${{ github.event.inputs.ref || env.BRANCH_OR_TAG == 'tag' }}
id: extract-tag
uses: ./packages/actions/src/formatTag
with:
tag: ${{ env.BRANCH_NAME }}
- name: Move docs to correct directory
if: env.BRANCH_OR_TAG == 'tag'
if: ${{ (github.event.inputs.ref || env.BRANCH_OR_TAG == 'tag') && matrix.package == steps.extract-tag.outputs.package }}
env:
PACKAGE: ${{ steps.extract-tag.outputs.package }}
SEMVER: ${{ steps.extract-tag.outputs.semver }}
@@ -124,7 +131,7 @@ jobs:
mv docs/${PACKAGE}/docs/docs.json out/${PACKAGE}/${SEMVER}.json
- name: Move docs to correct directory
if: env.BRANCH_OR_TAG == 'branch'
if: ${{ !github.event.inputs.ref && env.BRANCH_OR_TAG == 'branch' }}
env:
PACKAGE: ${{ matrix.package }}
run: |

View File

@@ -31,7 +31,7 @@ jobs:
run: yarn lint --cache-dir=".turbo"
- name: Tests
run: yarn test --cache-dir=".turbo"
run: yarn test
- name: Build
run: yarn build --cache-dir=".turbo"

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@ dist/
.DS_Store
.turbo
tsconfig.tsbuildinfo
coverage/
# yarn
.pnp.*

View File

@@ -5,7 +5,7 @@
"private": true,
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"test": "turbo run test && vitest run",
"lint": "turbo run lint",
"format": "turbo run format",
"fmt": "turbo run format",
@@ -41,10 +41,13 @@
"@commitlint/cli": "^17.0.2",
"@commitlint/config-angular": "^17.0.0",
"@favware/npm-deprecate": "^1.0.4",
"c8": "^7.11.3",
"conventional-changelog-cli": "^2.2.2",
"husky": "^8.0.1",
"is-ci": "^3.0.1",
"prettier": "^2.6.2",
"turbo": "^1.2.16"
"turbo": "^1.2.16",
"vitest": "^0.13.1"
},
"engines": {
"node": ">=16.9.0"

View File

@@ -6,45 +6,23 @@
<br />
<p>
<a href="https://discord.gg/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&logo=discord&logoColor=white" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/@discordjs/builders"><img src="https://img.shields.io/npm/v/@discordjs/builders.svg?maxAge=3600" alt="npm version" /></a>
<a href="https://www.npmjs.com/package/@discordjs/builders"><img src="https://img.shields.io/npm/dt/@discordjs/builders.svg?maxAge=3600" alt="npm downloads" /></a>
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/test.yml/badge.svg" alt="Build status" /></a>
<a href="https://codecov.io/gh/discordjs/builders"><img src="https://codecov.io/gh/discordjs/builders/branch/main/graph/badge.svg" alt="Code coverage" /></a>
</p>
</div>
## Installation
**Node.js 16.9.0 or newer is required.**
```sh-session
npm install @discordjs/builders
yarn add @discordjs/builders
pnpm add @discordjs/builders
```
## Examples
Here are some examples for the builders and utilities you can find in this package:
- [Slash Command Builders](https://github.com/discordjs/discord.js/blob/main/packages/builders/docs/examples/Slash%20Command%20Builders.md)
## Links
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
- [Documentation](https://discord.js.org/#/docs/builders)
- [Documentation](https://discord.js.org/#/docs)
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
- [discord.js Discord server](https://discord.gg/djs)
- [Discord API Discord server](https://discord.gg/discord-api)
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/builders)
- [npm](https://www.npmjs.com/package/@discordjs/builders)
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/scripts)
- [Related libraries](https://discord.com/developers/docs/topics/community-resources#libraries)
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs/builders).
See [the contribution guide](https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help

View File

@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest';
import { formatTag } from '../src';
describe('Format Tag', () => {

View File

@@ -1,18 +0,0 @@
/**
* @type {import('@babel/core').TransformOptions}
*/
module.exports = {
parserOpts: { strictMode: true },
sourceMaps: 'inline',
presets: [
[
'@babel/preset-env',
{
targets: { node: 'current' },
modules: 'commonjs',
},
],
'@babel/preset-typescript',
],
plugins: ['babel-plugin-transform-typescript-metadata', ['@babel/plugin-proposal-decorators', { legacy: true }]],
};

View File

@@ -1,10 +0,0 @@
coverage:
status:
project:
default:
target: 70%
threshold: 5%
patch:
default:
target: 70%
threshold: 5%

View File

@@ -1,19 +0,0 @@
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
module.exports = {
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'clover'],
coverageThreshold: {
global: {
branches: 70,
lines: 70,
statements: 70,
},
},
coveragePathIgnorePatterns: ['src/index.ts', 'src/formatTag/index.ts'],
};

View File

@@ -5,7 +5,6 @@
"private": true,
"scripts": {
"build": "tsup",
"test": "jest --pass-with-no-tests",
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix"
},
@@ -48,23 +47,15 @@
"tslib": "^2.4.0"
},
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/plugin-proposal-decorators": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-typescript": "^7.17.12",
"@types/jest": "^28.1.0",
"@types/node": "^16.11.38",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"babel-plugin-transform-typescript-metadata": "^0.3.2",
"eslint": "^8.17.0",
"eslint-config-marine": "^9.4.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"jest": "^28.1.0",
"prettier": "^2.6.2",
"tsup": "^6.0.1",
"typedoc": "^0.22.17",
"typescript": "^4.7.3"
},
"engines": {

View File

@@ -11,4 +11,4 @@ outputs:
description: 'The semver string that was extracted from this tag'
runs:
using: node16
main: ../../dist/formatTag/index.mjs
main: ../../dist/formatTag/index.js

View File

@@ -4,7 +4,7 @@ import { formatTag } from './formatTag';
const tag = getInput('tag', { required: true });
const parsed = formatTag(tag);
if (parsed?.groups) {
if (parsed) {
setOutput('package', parsed.package);
setOutput('semver', parsed.semver);
}

View File

@@ -4,7 +4,7 @@ export default defineConfig({
clean: true,
dts: true,
entryPoints: ['src/index.ts', 'src/formatTag/index.ts'],
format: ['esm'],
format: ['cjs'],
minify: true,
skipNodeModulesBundle: false,
noExternal: ['@actions/core'],

View File

@@ -1,3 +0,0 @@
{
"releaseCommitMessageFormat": "chore(Release): publish"
}

View File

@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
# [@discordjs/builders@0.15.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.14.0...@discordjs/builders@0.15.0) - (2022-06-05)
## Features
- Allow builders to accept rest params and arrays (#7874) ([ad75be9](https://github.com/discordjs/discord.js/commit/ad75be9a9cf90c8624495df99b75177e6c24022f))
- Use vitest instead of jest for more speed ([8d8e6c0](https://github.com/discordjs/discord.js/commit/8d8e6c03decd7352a2aa180f6e5bc1a13602539b))
- Add scripts package for locally used scripts ([f2ae1f9](https://github.com/discordjs/discord.js/commit/f2ae1f9348bfd893332a9060f71a8a5f272a1b8b))
# [@discordjs/builders@0.14.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.13.0...@discordjs/builders@0.14.0) - (2022-06-04)
## Bug Fixes

View File

@@ -9,7 +9,6 @@
<a href="https://www.npmjs.com/package/@discordjs/builders"><img src="https://img.shields.io/npm/v/@discordjs/builders.svg?maxAge=3600" alt="npm version" /></a>
<a href="https://www.npmjs.com/package/@discordjs/builders"><img src="https://img.shields.io/npm/dt/@discordjs/builders.svg?maxAge=3600" alt="npm downloads" /></a>
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/test.yml/badge.svg" alt="Build status" /></a>
<a href="https://codecov.io/gh/discordjs/builders"><img src="https://codecov.io/gh/discordjs/builders/branch/main/graph/badge.svg" alt="Code coverage" /></a>
</p>
</div>

View File

@@ -1,4 +1,5 @@
import { APIActionRowComponent, APIMessageActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
ActionRowBuilder,
ButtonBuilder,
@@ -44,6 +45,8 @@ const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent>
describe('Action Row Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid components THEN do not throw', () => {
expect(() => new ActionRowBuilder().addComponents(new ButtonBuilder())).not.toThrowError();
expect(() => new ActionRowBuilder().setComponents(new ButtonBuilder())).not.toThrowError();
expect(() => new ActionRowBuilder().addComponents([new ButtonBuilder()])).not.toThrowError();
expect(() => new ActionRowBuilder().setComponents([new ButtonBuilder()])).not.toThrowError();
});
@@ -82,6 +85,7 @@ describe('Action Row Components', () => {
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
});
test('GIVEN valid builder options THEN valid JSON output is given', () => {
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
type: ComponentType.ActionRow,
@@ -122,17 +126,24 @@ describe('Action Row Components', () => {
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
});
test('GIVEN valid builder options THEN valid JSON output is given', () => {
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
const selectMenu = new SelectMenuBuilder()
.setCustomId('1234')
.setMaxValues(10)
.setMinValues(12)
.setOptions(
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
)
.setOptions([
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
]);
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
expect(new ActionRowBuilder().addComponents([button]).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRowBuilder().addComponents([selectMenu]).toJSON()).toEqual(rowWithSelectMenuData);
});

View File

@@ -4,6 +4,7 @@ import {
ButtonStyle,
ComponentType,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions';
import { ButtonBuilder } from '../../src/components/button/Button';
@@ -124,7 +125,7 @@ describe('Button Components', () => {
expect(
buttonComponent()
.setCustomId(interactionData.custom_id)
.setLabel(interactionData.label)
.setLabel(interactionData.label!)
.setStyle(interactionData.style)
.setDisabled(interactionData.disabled)
.toJSON(),
@@ -140,7 +141,7 @@ describe('Button Components', () => {
expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData);
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
expect(buttonComponent().setLabel(linkData.label!).setDisabled(true).setURL(linkData.url));
});
});
});

View File

@@ -1,4 +1,5 @@
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index';
const selectMenu = () => new SelectMenuBuilder();
@@ -43,12 +44,15 @@ describe('Select Menu Components', () => {
.setDefault(true)
.setEmoji({ name: 'test' })
.setDescription('description');
expect(() => selectMenu().addOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError();
expect(() => selectMenu().addOptions([option])).not.toThrowError();
expect(() => selectMenu().setOptions([option])).not.toThrowError();
expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError();
expect(() =>
selectMenu().addOptions([
{
selectMenu()
.addOptions({
label: 'test',
value: 'test',
emoji: {
@@ -56,14 +60,31 @@ describe('Select Menu Components', () => {
name: 'test',
animated: true,
},
},
]),
})
.addOptions([
{
label: 'test',
value: 'test',
emoji: {
id: '123',
name: 'test',
animated: true,
},
},
]),
).not.toThrowError();
const options = new Array<APISelectMenuOption>(25).fill({ label: 'test', value: 'test' });
expect(() => selectMenu().addOptions(...options)).not.toThrowError();
expect(() => selectMenu().setOptions(...options)).not.toThrowError();
expect(() => selectMenu().addOptions(options)).not.toThrowError();
expect(() => selectMenu().setOptions(options)).not.toThrowError();
expect(() =>
selectMenu()
.addOptions({ label: 'test', value: 'test' })
.addOptions(...new Array<APISelectMenuOption>(24).fill({ label: 'test', value: 'test' })),
).not.toThrowError();
expect(() =>
selectMenu()
.addOptions([{ label: 'test', value: 'test' }])
@@ -79,6 +100,17 @@ describe('Select Menu Components', () => {
expect(() => selectMenu().setDisabled(0)).toThrowError();
expect(() => selectMenu().setPlaceholder(longStr)).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ label: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ label: longStr, value: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ value: longStr, label: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', description: longStr })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', default: 100 })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ value: 'test' })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ default: true })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions([{ label: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ label: longStr, value: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ value: longStr, label: 'test' }])).toThrowError();
@@ -91,8 +123,14 @@ describe('Select Menu Components', () => {
expect(() => selectMenu().addOptions([{ default: true }])).toThrowError();
const tooManyOptions = new Array<APISelectMenuOption>(26).fill({ label: 'test', value: 'test' });
expect(() => selectMenu().setOptions(...tooManyOptions)).toThrowError();
expect(() => selectMenu().setOptions(tooManyOptions)).toThrowError();
expect(() =>
selectMenu()
.addOptions({ label: 'test', value: 'test' })
.addOptions(...tooManyOptions),
).toThrowError();
expect(() =>
selectMenu()
.addOptions([{ label: 'test', value: 'test' }])
@@ -112,6 +150,11 @@ describe('Select Menu Components', () => {
});
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
expect(
new SelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions(new SelectMenuOptionBuilder(selectMenuOptionData))
.toJSON(),
).toEqual(selectMenuData);
expect(
new SelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions([new SelectMenuOptionBuilder(selectMenuOptionData)])

View File

@@ -1,4 +1,5 @@
import { APITextInputComponent, ComponentType, TextInputStyle } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
labelValidator,
maxLengthValidator,
@@ -45,7 +46,7 @@ describe('Text Input Components', () => {
expect(() => maxLengthValidator.parse(10)).not.toThrowError();
});
test('GIVEN invalid min length THEN validator does throw', () => {
test('GIVEN invalid min length THEN validator does throw 2', () => {
expect(() => maxLengthValidator.parse(4001)).toThrowError();
});
@@ -61,7 +62,7 @@ describe('Text Input Components', () => {
expect(() => placeholderValidator.parse('foobar')).not.toThrowError();
});
test('GIVEN invalid value THEN validator does throw', () => {
test('GIVEN invalid value THEN validator does throw 2', () => {
expect(() => placeholderValidator.parse(superLongStr)).toThrowError();
});
@@ -114,10 +115,10 @@ describe('Text Input Components', () => {
textInputComponent()
.setCustomId(textInputData.custom_id)
.setLabel(textInputData.label)
.setPlaceholder(textInputData.placeholder)
.setMaxLength(textInputData.max_length)
.setMinLength(textInputData.min_length)
.setValue(textInputData.value)
.setPlaceholder(textInputData.placeholder!)
.setMaxLength(textInputData.max_length!)
.setMinLength(textInputData.min_length!)
.setValue(textInputData.value!)
.setRequired(textInputData.required)
.setStyle(textInputData.style)
.toJSON(),

View File

@@ -1,4 +1,5 @@
import { PermissionFlagsBits } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index';
const getBuilder = () => new ContextMenuCommandBuilder();

View File

@@ -10,6 +10,7 @@ import {
ApplicationCommandOptionType,
ChannelType,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
SlashCommandBooleanOption,
SlashCommandChannelOption,

View File

@@ -1,4 +1,5 @@
import { APIApplicationCommandOptionChoice, ChannelType, PermissionFlagsBits } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
SlashCommandAssertions,
SlashCommandBooleanOption,
@@ -313,8 +314,10 @@ describe('Slash Commands', () => {
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(true)).toThrowError();
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(null)).toThrowError();
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(undefined)).toThrowError();
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error

View File

@@ -1,4 +1,5 @@
import { APIModalInteractionResponseCallbackData, ComponentType, TextInputStyle } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
ActionRowBuilder,
ButtonBuilder,
@@ -48,7 +49,11 @@ describe('Modals', () => {
test('GIVEN valid fields THEN builder does not throw', () => {
expect(() =>
modal().setTitle('test').setCustomId('foobar').setComponents([new ActionRowBuilder()]),
modal()
.setTitle('test')
.setCustomId('foobar')
.setComponents(new ActionRowBuilder())
.addComponents([new ActionRowBuilder()]),
).not.toThrowError();
});
@@ -74,6 +79,17 @@ describe('Modals', () => {
},
],
},
{
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.TextInput,
label: 'label',
style: TextInputStyle.Paragraph,
custom_id: 'custom id',
},
],
},
],
};
@@ -83,10 +99,15 @@ describe('Modals', () => {
modal()
.setTitle(modalData.title)
.setCustomId('custom id')
.setComponents([
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents([
.setComponents(
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
]),
),
)
.addComponents([
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
),
])
.toJSON(),
).toEqual(modalData);

View File

@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest';
import { EmbedBuilder, embedLength } from '../../src';
const alpha = 'abcdefghijklmnopqrstuvwxyz';
@@ -322,28 +323,29 @@ 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' }]);
expect(embed.toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'bar' }],
fields: [
{ name: 'foo', value: 'bar' },
{ name: 'foo', value: 'bar' },
],
});
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.addFields([
{ name: 'foo', value: 'bar' },
{ name: 'foo', value: 'baz' },
]);
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'baz' }],
});
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data 2', () => {
const embed = new EmbedBuilder();
embed.addFields(Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
expect(() =>
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' }))),
@@ -352,7 +354,7 @@ describe('Embed', () => {
test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => {
const embed = new EmbedBuilder();
embed.addFields(Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
expect(() =>
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))),
@@ -362,6 +364,9 @@ describe('Embed', () => {
test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
expect(() =>
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
expect(() =>
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
@@ -370,38 +375,43 @@ describe('Embed', () => {
test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => {
const embed = new EmbedBuilder();
expect(() =>
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
expect(() => embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError();
});
describe('GIVEN invalid field amount THEN throws error', () => {
test('', () => {
test('1', () => {
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('', () => {
test('2', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields([{ name: '', value: 'bar' }])).toThrowError();
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
});
});
describe('GIVEN invalid field name length THEN throws error', () => {
test('', () => {
test('3', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields([{ name: 'a'.repeat(257), value: 'bar' }])).toThrowError();
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
});
});
describe('GIVEN invalid field value length THEN throws error', () => {
test('', () => {
test('4', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields([{ name: '', value: 'a'.repeat(1025) }])).toThrowError();
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError();
});
});
});

View File

@@ -1,3 +1,4 @@
import { describe, test, expect, vitest } from 'vitest';
import {
blockQuote,
bold,
@@ -150,12 +151,12 @@ describe('Message formatters', () => {
describe('time', () => {
test('GIVEN no arguments THEN returns "<t:${bigint}>"', () => {
jest.useFakeTimers('modern');
jest.setSystemTime(1566424897579);
vitest.useFakeTimers();
vitest.setSystemTime(1566424897579);
expect<`<t:${bigint}>`>(time()).toEqual('<t:1566424897>');
jest.useRealTimers();
vitest.useRealTimers();
});
test('GIVEN a date THEN returns "<t:${bigint}>"', () => {

View File

@@ -1,18 +0,0 @@
/**
* @type {import('@babel/core').TransformOptions}
*/
module.exports = {
parserOpts: { strictMode: true },
sourceMaps: 'inline',
presets: [
[
'@babel/preset-env',
{
targets: { node: 'current' },
modules: 'commonjs',
},
],
'@babel/preset-typescript',
],
plugins: ['babel-plugin-transform-typescript-metadata', ['@babel/plugin-proposal-decorators', { legacy: true }]],
};

View File

@@ -1,10 +0,0 @@
coverage:
status:
project:
default:
target: 70%
threshold: 5%
patch:
default:
target: 70%
threshold: 5%

View File

@@ -1,19 +0,0 @@
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
module.exports = {
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'clover'],
coverageThreshold: {
global: {
branches: 70,
lines: 70,
statements: 70,
},
},
coveragePathIgnorePatterns: ['src/index.ts'],
};

View File

@@ -1,15 +1,14 @@
{
"name": "@discordjs/builders",
"version": "0.14.0",
"version": "0.15.0",
"description": "A set of builders that you can use when creating your bot",
"scripts": {
"build": "tsup",
"test": "jest --pass-with-no-tests",
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && node scripts/docs.mjs",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && ts-docgen -i docs/typedoc-out.json -c docs/index.yml -o docs/docs.json",
"prepublishOnly": "yarn build && yarn lint && yarn test",
"changelog": "git cliff --prepend ./CHANGELOG.md -l -c ./cliff.toml -r ../../ --include-path 'packages/builders/*'"
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/builders/*'"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -60,21 +59,14 @@
"tslib": "^2.4.0"
},
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/plugin-proposal-decorators": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-typescript": "^7.17.12",
"@discordjs/ts-docgen": "^0.4.1",
"@types/jest": "^28.1.0",
"@discordjs/scripts": "workspace:^",
"@types/node": "^16.11.38",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"babel-plugin-transform-typescript-metadata": "^0.3.2",
"eslint": "^8.17.0",
"eslint-config-marine": "^9.4.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"jest": "^28.1.0",
"prettier": "^2.6.2",
"tsup": "^6.0.1",
"typedoc": "^0.22.17",

View File

@@ -1,7 +0,0 @@
import { runGenerator } from '@discordjs/ts-docgen';
runGenerator({
existingOutput: 'docs/typedoc-out.json',
custom: 'docs/index.yml',
output: 'docs/docs.json',
});

View File

@@ -8,6 +8,7 @@ import {
import { ComponentBuilder } from './Component';
import { createComponentBuilder } from './Components';
import type { ButtonBuilder, SelectMenuBuilder, TextInputBuilder } from '..';
import { normalizeArray, type RestOrArray } from '../util/normalizeArray';
export type MessageComponentBuilder =
| MessageActionRowComponentBuilder
@@ -38,8 +39,8 @@ export class ActionRowBuilder<T extends AnyComponentBuilder> extends ComponentBu
* @param components The components to add to this action row.
* @returns
*/
public addComponents(components: T[]) {
this.components.push(...components);
public addComponents(...components: RestOrArray<T>) {
this.components.push(...normalizeArray(components));
return this;
}
@@ -47,8 +48,8 @@ export class ActionRowBuilder<T extends AnyComponentBuilder> extends ComponentBu
* Sets the components in this action row
* @param components The components to set this row to
*/
public setComponents(components: T[]) {
this.components.splice(0, this.components.length, ...components);
public setComponents(...components: RestOrArray<T>) {
this.components.splice(0, this.components.length, ...normalizeArray(components));
return this;
}

View File

@@ -1,6 +1,7 @@
import type { APISelectMenuComponent, APISelectMenuOption } from 'discord-api-types/v10';
import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu';
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray';
import {
customIdValidator,
disabledValidator,
@@ -35,7 +36,8 @@ export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
return super.setDisabled(disabledValidator.parse(disabled));
}
public override addOptions(options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
public override addOptions(...options: RestOrArray<UnsafeSelectMenuOptionBuilder | APISelectMenuOption>) {
options = normalizeArray(options);
optionsLengthValidator.parse(this.options.length + options.length);
this.options.push(
...options.map((option) =>
@@ -47,7 +49,8 @@ export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
return this;
}
public override setOptions(options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
public override setOptions(...options: RestOrArray<UnsafeSelectMenuOptionBuilder | APISelectMenuOption>) {
options = normalizeArray(options);
optionsLengthValidator.parse(options.length);
this.options.splice(
0,

View File

@@ -1,5 +1,6 @@
import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v10';
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray';
import { ComponentBuilder } from '../Component';
/**
@@ -67,9 +68,9 @@ export class UnsafeSelectMenuBuilder extends ComponentBuilder<APISelectMenuCompo
* @param options The options to add to this select menu
* @returns
*/
public addOptions(options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
public addOptions(...options: RestOrArray<UnsafeSelectMenuOptionBuilder | APISelectMenuOption>) {
this.options.push(
...options.map((option) =>
...normalizeArray(options).map((option) =>
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
),
);
@@ -80,11 +81,11 @@ export class UnsafeSelectMenuBuilder extends ComponentBuilder<APISelectMenuCompo
* Sets the options on this select menu
* @param options The options to set on this select menu
*/
public setOptions(options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
public setOptions(...options: RestOrArray<UnsafeSelectMenuOptionBuilder | APISelectMenuOption>) {
this.options.splice(
0,
this.options.length,
...options.map((option) =>
...normalizeArray(options).map((option) =>
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
),
);

View File

@@ -45,3 +45,4 @@ export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder';
export * from './util/jsonEncodable';
export * from './util/equatable';
export * from './util/componentUtil';
export * from './util/normalizeArray';

View File

@@ -4,6 +4,7 @@ import type {
APIModalInteractionResponseCallbackData,
} from 'discord-api-types/v10';
import { ActionRowBuilder, createComponentBuilder, JSONEncodable, ModalActionRowComponentBuilder } from '../../index';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray';
export class UnsafeModalBuilder implements JSONEncodable<APIModalInteractionResponseCallbackData> {
public readonly data: Partial<APIModalInteractionResponseCallbackData>;
@@ -38,13 +39,12 @@ export class UnsafeModalBuilder implements JSONEncodable<APIModalInteractionResp
* @param components The components to add to this modal
*/
public addComponents(
components: (
| ActionRowBuilder<ModalActionRowComponentBuilder>
| APIActionRowComponent<APIModalActionRowComponent>
)[],
...components: RestOrArray<
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIModalActionRowComponent>
>
) {
this.components.push(
...components.map((component) =>
...normalizeArray(components).map((component) =>
component instanceof ActionRowBuilder
? component
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
@@ -57,8 +57,8 @@ export class UnsafeModalBuilder implements JSONEncodable<APIModalInteractionResp
* Sets the components in this modal
* @param components The components to set this modal to
*/
public setComponents(components: ActionRowBuilder<ModalActionRowComponentBuilder>[]) {
this.components.splice(0, this.components.length, ...components);
public setComponents(...components: RestOrArray<ActionRowBuilder<ModalActionRowComponentBuilder>>) {
this.components.splice(0, this.components.length, ...normalizeArray(components));
return this;
}

View File

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

View File

@@ -1,4 +1,5 @@
import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray';
export type RGBTuple = [red: number, green: number, blue: number];
@@ -44,7 +45,8 @@ export class UnsafeEmbedBuilder {
*
* @param fields The fields to add
*/
public addFields(fields: APIEmbedField[]): this {
public addFields(...fields: RestOrArray<APIEmbedField>): this {
fields = normalizeArray(fields);
if (this.data.fields) this.data.fields.push(...fields);
else this.data.fields = fields;
return this;
@@ -67,8 +69,8 @@ export class UnsafeEmbedBuilder {
* Sets the embed's fields (max 25).
* @param fields The fields to set
*/
public setFields(fields: APIEmbedField[]) {
this.spliceFields(0, this.data.fields?.length ?? 0, ...fields);
public setFields(...fields: RestOrArray<APIEmbedField>) {
this.spliceFields(0, this.data.fields?.length ?? 0, ...normalizeArray(fields));
return this;
}

View File

@@ -0,0 +1,6 @@
export function normalizeArray<T>(arr: RestOrArray<T>): T[] {
if (Array.isArray(arr[0])) return arr[0];
return arr as T[];
}
export type RestOrArray<T> = T[] | [T[]];

View File

@@ -1,3 +0,0 @@
{
"releaseCommitMessageFormat": "chore(Release): publish"
}

View File

@@ -30,9 +30,13 @@ pnpm add @discordjs/collection
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
- [Documentation](https://discord.js.org/#/docs/collection)
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
- [discord.js Discord server](https://discord.gg/djs)
- [Discord API Discord server](https://discord.gg/discord-api)
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/collection)
- [npm](https://www.npmjs.com/package/@discordjs/collection)
- [Related libraries](https://discord.com/developers/docs/topics/community-resources#libraries)
## Contributing

View File

@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest';
import Collection from '../src';
type TestCollection = Collection<string, number>;
@@ -247,7 +248,7 @@ test('random select from a collection', () => {
const chars = 'abcdefghijklmnopqrstuvwxyz';
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26];
for (let i = 0; i < chars.length; i++) coll.set(chars[i], numbers[i]);
for (let i = 0; i < chars.length; i++) coll.set(chars[i]!, numbers[i]!);
const random = coll.random(5);
expect(random.length === 5).toBeTruthy();
@@ -357,7 +358,7 @@ describe('hasAny() tests', () => {
});
});
describe('reverse() tests', () => {
test('reverse() tests', () => {
const coll = new Collection();
coll.set('a', 1);
coll.set('b', 2);

View File

@@ -1,17 +0,0 @@
/**
* @type {import('@babel/core').TransformOptions}
*/
module.exports = {
parserOpts: { strictMode: true },
sourceMaps: 'inline',
presets: [
[
'@babel/preset-env',
{
targets: { node: 'current' },
modules: 'commonjs',
},
],
'@babel/preset-typescript',
],
};

View File

@@ -1,11 +0,0 @@
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
module.exports = {
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'clover'],
};

View File

@@ -1,15 +1,14 @@
{
"name": "@discordjs/collection",
"version": "0.7.0",
"version": "0.8.0-dev",
"description": "Utility data structure used in discord.js",
"scripts": {
"test": "jest --pass-with-no-tests",
"build": "tsup",
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && node scripts/docs.mjs",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && ts-docgen -i docs/typedoc-out.json -c docs/index.yml -o docs/docs.json",
"prepublishOnly": "yarn build && yarn lint && yarn test",
"changelog": "git cliff --prepend ./CHANGELOG.md -l -c ./cliff.toml -r ../../ --include-path 'packages/collection/*'"
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/collection/*'"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -48,11 +47,7 @@
},
"homepage": "https://discord.js.org",
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-typescript": "^7.17.12",
"@discordjs/ts-docgen": "^0.4.1",
"@types/jest": "^28.1.0",
"@discordjs/scripts": "workspace:^",
"@types/node": "^16.11.38",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
@@ -60,7 +55,6 @@
"eslint-config-marine": "^9.4.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"jest": "^28.1.0",
"prettier": "^2.6.2",
"tsup": "^6.0.1",
"typedoc": "^0.22.17",

View File

@@ -1,7 +0,0 @@
import { runGenerator } from '@discordjs/ts-docgen';
runGenerator({
existingOutput: 'docs/typedoc-out.json',
custom: 'docs/index.yml',
output: 'docs/docs.json',
});

View File

@@ -107,7 +107,7 @@ client.login('token');
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
- [discord.js Discord server](https://discord.gg/djs)
- [Discord API Discord server](https://discord.gg/discord-api)
- [GitHub](https://github.com/discordjs/discord.js)
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/discord.js)
- [npm](https://www.npmjs.com/package/discord.js)
- [Related libraries](https://discord.com/developers/docs/topics/community-resources#libraries)

View File

@@ -10,7 +10,7 @@
"docs": "docgen --source ./src --custom ./docs/index.yml --root ../../ --output ./docs/docs.json",
"docs:test": "docgen --source ./src --custom ./docs/index.yml --root ../../",
"prepublishOnly": "yarn lint && yarn test",
"changelog": "git cliff --prepend ./CHANGELOG.md -l -c ./cliff.toml -r ../../ --include-path 'packages/discord.js/*'"
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/discord.js/*'"
},
"main": "./src/index.js",
"types": "./typings/index.d.ts",

View File

@@ -220,13 +220,8 @@ class WebSocketManager extends EventEmitter {
this.shardQueue.add(shard);
if (shard.sessionId) {
this.debug(`Session id is present, attempting an immediate reconnect...`, shard);
this.reconnect();
} else {
shard.destroy({ reset: true, emit: false, log: false });
this.reconnect();
}
if (shard.sessionId) this.debug(`Session id is present, attempting an immediate reconnect...`, shard);
this.reconnect();
});
shard.on(ShardEvents.InvalidSession, () => {

View File

@@ -84,6 +84,13 @@ class WebSocketShard extends EventEmitter {
*/
this.lastHeartbeatAcked = true;
/**
* Used to prevent calling {@link WebSocketShard#event:close} twice while closing or terminating the WebSocket.
* @type {boolean}
* @private
*/
this.closeEmitted = false;
/**
* Contains the rate limit queue and metadata
* @name WebSocketShard#ratelimit
@@ -129,6 +136,14 @@ class WebSocketShard extends EventEmitter {
*/
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
/**
* The WebSocket timeout.
* @name WebSocketShard#wsCloseTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'wsCloseTimeout', { value: null, writable: true });
/**
* If the manager attached its event handlers on the shard
* @name WebSocketShard#eventsAttached
@@ -256,7 +271,8 @@ class WebSocketShard extends EventEmitter {
this.connectedAt = Date.now();
const ws = (this.connection = WebSocket.create(gateway, wsQuery));
// Adding a handshake timeout to just make sure no zombie connection appears.
const ws = (this.connection = WebSocket.create(gateway, wsQuery, { handshakeTimeout: 30_000 }));
ws.onopen = this.onOpen.bind(this);
ws.onmessage = this.onMessage.bind(this);
ws.onerror = this.onError.bind(this);
@@ -343,21 +359,35 @@ class WebSocketShard extends EventEmitter {
* @private
*/
onClose(event) {
this.closeEmitted = true;
if (this.sequence !== -1) this.closeSequence = this.sequence;
this.sequence = -1;
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
// Clearing the WebSocket close timeout as close was emitted.
this.setWsCloseTimeout(-1);
// If we still have a connection object, clean up its listeners
if (this.connection) this._cleanupConnection();
this.status = Status.DISCONNECTED;
this.emitClose(event);
}
/**
* This method is responsible to emit close event for this shard.
* This method helps the shard reconnect.
* @param {CloseEvent} [event] Close event that was received
*/
emitClose(
event = {
code: 1011,
reason: 'INTERNAL_ERROR',
wasClean: false,
},
) {
this.debug(`[CLOSE]
Event Code: ${event.code}
Clean : ${event.wasClean}
Reason : ${event.reason ?? 'No reason received'}`);
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
// If we still have a connection object, clean up its listeners
if (this.connection) this._cleanupConnection();
this.status = Status.Disconnected;
/**
* Emitted when a shard's WebSocket closes.
* @private
@@ -366,7 +396,6 @@ class WebSocketShard extends EventEmitter {
*/
this.emit(ShardEvents.Close, event);
}
/**
* Called whenever a packet is received.
* @param {Object} packet The received packet
@@ -526,6 +555,47 @@ class WebSocketShard extends EventEmitter {
}, 20_000).unref();
}
/**
* Sets the WebSocket Close timeout.
* This method is responsible for detecting any zombie connections if the WebSocket fails to close properly.
* @param {number} [time] If set to -1, it will clear the timeout
* @private
*/
setWsCloseTimeout(time) {
if (this.wsCloseTimeout) {
this.debug('[WebSocket] Clearing the close timeout.');
clearTimeout(this.wsCloseTimeout);
}
if (time === -1) {
this.wsCloseTimeout = null;
return;
}
this.wsCloseTimeout = setTimeout(() => {
this.setWsCloseTimeout(-1);
this.debug(`[WebSocket] Close Emitted: ${this.closeEmitted}`);
// Check if close event was emitted.
if (this.closeEmitted) {
this.debug(
`[WebSocket] was closed. | WS State: ${
CONNECTION_STATE[this.connection?.readyState ?? WebSocket.CLOSED]
} | Close Emitted: ${this.closeEmitted}`,
);
// Setting the variable false to check for zombie connections.
this.closeEmitted = false;
return;
}
this.debug(
// eslint-disable-next-line max-len
`[WebSocket] did not close properly, assuming a zombie connection.\nEmitting close and reconnecting again.`,
);
this.emitClose();
// Setting the variable false to check for zombie connections.
this.closeEmitted = false;
}, time).unref();
}
/**
* Sets the heartbeat timer for this shard.
* @param {number} time If -1, clears the interval, any other number sets an interval
@@ -567,7 +637,7 @@ class WebSocketShard extends EventEmitter {
Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`,
);
this.destroy({ closeCode: 4009, reset: true });
this.destroy({ reset: true, closeCode: 4009 });
return;
}
@@ -716,11 +786,17 @@ class WebSocketShard extends EventEmitter {
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
this.debug(
`[WebSocket] Destroy: Attempting to close the WebSocket. | WS State: ${
CONNECTION_STATE[this.connection?.readyState ?? WebSocket.CLOSED]
}`,
);
// Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED
if (this.connection) {
// If the connection is currently opened, we will (hopefully) receive close
if (this.connection.readyState === WebSocket.OPEN) {
this.connection.close(closeCode);
this.debug(`[WebSocket] Close: Tried closing. | WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
} else {
// Connection is not OPEN
this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`);
@@ -729,8 +805,13 @@ class WebSocketShard extends EventEmitter {
// Attempt to close the connection just in case
try {
this.connection.close(closeCode);
} catch {
// No-op
} catch (err) {
this.debug(
`[WebSocket] Close: Something went wrong while closing the WebSocket: ${
err.message || err
}. Forcefully terminating the connection | WS State: ${CONNECTION_STATE[this.connection.readyState]}`,
);
this.connection.terminate();
}
// Emit the destroyed event if needed
if (emit) this._emitDestroyed();
@@ -740,11 +821,20 @@ class WebSocketShard extends EventEmitter {
this._emitDestroyed();
}
if (this.connection?.readyState === WebSocket.CLOSING || this.connection?.readyState === WebSocket.CLOSED) {
this.closeEmitted = false;
this.debug(
`[WebSocket] Adding a WebSocket close timeout to ensure a correct WS reconnect.
Timeout: ${this.manager.client.options.closeTimeout}ms`,
);
this.setWsCloseTimeout(this.manager.client.options.closeTimeout);
}
// Step 2: Null the connection object
this.connection = null;
// Step 3: Set the shard status to Disconnected
this.status = Status.Disconnected;
// Step 3: Set the shard status to DISCONNECTED
this.status = Status.DISCONNECTED;
// Step 4: Cache the old sequence (use to attempt a resume)
if (this.sequence !== -1) this.closeSequence = this.sequence;

View File

@@ -123,6 +123,7 @@ exports.InviteStageInstance = require('./structures/InviteStageInstance');
exports.InviteGuild = require('./structures/InviteGuild');
exports.Message = require('./structures/Message').Message;
exports.Attachment = require('./structures/Attachment');
exports.AttachmentBuilder = require('./structures/AttachmentBuilder');
exports.ModalBuilder = require('./structures/ModalBuilder');
exports.MessageCollector = require('./structures/MessageCollector');
exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction');

View File

@@ -99,7 +99,7 @@ class GuildChannelManager extends CachedManager {
/**
* Options used to create a new channel in a guild.
* @typedef {CategoryCreateChannelOptions} GuildChannelCreateOptions
* @property {CategoryChannelResolvable} [parent] Parent of the new channel
* @property {?CategoryChannelResolvable} [parent] Parent of the new channel
*/
/**
@@ -201,7 +201,7 @@ class GuildChannelManager extends CachedManager {
* @property {string} [name] The name of the channel
* @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported)
* @property {number} [position] The position of the channel
* @property {string} [topic] The topic of the text channel
* @property {?string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the voice channel

View File

@@ -84,7 +84,7 @@ class GuildManager extends CachedManager {
* @property {Snowflake|number} [parentId] The parent id for this channel
* @property {ChannelType|number} [type] The type of the channel
* @property {string} name The name of the channel
* @property {string} [topic] The topic of the text channel
* @property {?string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the channel
@@ -137,22 +137,24 @@ class GuildManager extends CachedManager {
return super.resolveId(guild);
}
/* eslint-disable max-len */
/**
* Options used to create a guild.
* @typedef {Object} GuildCreateOptions
* @property {Snowflake|number} [afkChannelId] The AFK channel's id
* @property {number} [afkTimeout] The AFK timeout in seconds
* @property {PartialChannelData[]} [channels=[]] The channels for this guild
* @property {DefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notifications
* @property {GuildDefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notifications
* for the guild
* @property {ExplicitContentFilterLevel} [explicitContentFilter] The explicit content filter level for the guild
* @property {GuildExplicitContentFilterLevel} [explicitContentFilter] The explicit content filter level for the guild
* @property {?(BufferResolvable|Base64Resolvable)} [icon=null] The icon for the guild
* @property {PartialRoleData[]} [roles=[]] The roles for this guild,
* the first element of this array is used to change properties of the guild's everyone role.
* @property {Snowflake|number} [systemChannelId] The system channel's id
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The flags of the system channel
* @property {VerificationLevel} [verificationLevel] The verification level for the guild
* @property {GuildVerificationLevel} [verificationLevel] The verification level for the guild
*/
/* eslint-enable max-len */
/**
* Creates a guild.

View File

@@ -206,6 +206,15 @@ class GuildMemberManager extends CachedManager {
return this._fetchMany(options);
}
/**
* Fetches the client user as a GuildMember of the guild.
* @param {BaseFetchOptions} [options] The options for fetching the member
* @returns {Promise<GuildMember>}
*/
fetchMe(options) {
return this.fetch({ ...options, user: this.client.user.id });
}
/**
* Options used for searching guild members.
* @typedef {Object} GuildSearchMembersOptions

View File

@@ -43,7 +43,7 @@ class GuildScheduledEventManager extends CachedManager {
* @property {DateResolvable} scheduledStartTime The time to schedule the event at
* @property {DateResolvable} [scheduledEndTime] The time to end the event at
* <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn>
* @property {PrivacyLevel|number} privacyLevel The privacy level of the guild scheduled event
* @property {GuildScheduledEventPrivacyLevel|number} privacyLevel The privacy level of the guild scheduled event
* @property {GuildScheduledEventEntityType|number} entityType The scheduled entity type of the event
* @property {string} [description] The description of the guild scheduled event
* @property {GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event
@@ -167,7 +167,7 @@ class GuildScheduledEventManager extends CachedManager {
* @property {string} [name] The name of the guild scheduled event
* @property {DateResolvable} [scheduledStartTime] The time to schedule the event at
* @property {DateResolvable} [scheduledEndTime] The time to end the event at
* @property {PrivacyLevel|number} [privacyLevel] The privacy level of the guild scheduled event
* @property {GuildScheduledEventPrivacyLevel|number} [privacyLevel] The privacy level of the guild scheduled event
* @property {GuildScheduledEventEntityType|number} [entityType] The scheduled entity type of the event
* @property {string} [description] The description of the guild scheduled event
* @property {?GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event

View File

@@ -41,7 +41,7 @@ class GuildStickerManager extends CachedManager {
/**
* Creates a new custom sticker in the guild.
* @param {BufferResolvable|Stream|FileOptions|Attachment} file The file for the sticker
* @param {BufferResolvable|Stream|JSONEncodable<AttachmentPayload>} file The file for the sticker
* @param {string} name The name for the sticker
* @param {string} tags The Discord name of a unicode emoji representing the sticker's expression
* @param {GuildStickerCreateOptions} [options] Options

View File

@@ -30,7 +30,7 @@ class StageInstanceManager extends CachedManager {
* Options used to create a stage instance.
* @typedef {Object} StageInstanceCreateOptions
* @property {string} topic The topic of the stage instance
* @property {PrivacyLevel|number} [privacyLevel] The privacy level of the stage instance
* @property {StageInstancePrivacyLevel|number} [privacyLevel] The privacy level of the stage instance
* @property {boolean} [sendStartNotification] Whether to notify `@everyone` that the stage instance has started
*/
@@ -101,7 +101,7 @@ class StageInstanceManager extends CachedManager {
* Options used to edit an existing stage instance.
* @typedef {Object} StageInstanceEditOptions
* @property {string} [topic] The new topic of the stage instance
* @property {PrivacyLevel|number} [privacyLevel] The new privacy level of the stage instance
* @property {StageInstancePrivacyLevel|number} [privacyLevel] The new privacy level of the stage instance
*/
/**

View File

@@ -37,6 +37,15 @@ class ThreadMemberManager extends CachedManager {
return member;
}
/**
* Fetches the client user as a ThreadMember of the thread.
* @param {BaseFetchOptions} [options] The options for fetching the member
* @returns {Promise<ThreadMember>}
*/
fetchMe(options) {
return this.fetch({ ...options, member: this.client.user.id });
}
/**
* The client user as a ThreadMember of this ThreadChannel
* @type {?ThreadMember}

View File

@@ -1,6 +1,6 @@
'use strict';
const { ActionRowBuilder: BuildersActionRow, ComponentBuilder } = require('@discordjs/builders');
const { ActionRowBuilder: BuildersActionRow, ComponentBuilder, isJSONEncodable } = require('@discordjs/builders');
const Components = require('../util/Components');
const Transformers = require('../util/Transformers');
@@ -15,6 +15,19 @@ class ActionRowBuilder extends BuildersActionRow {
components: components?.map(c => (c instanceof ComponentBuilder ? c : Components.createComponentBuilder(c))),
});
}
/**
* Creates a new action row builder from JSON data
* @param {JSONEncodable<APIActionRowComponent<APIActionRowComponentTypes>>
* |APIActionRowComponent<APIActionRowComponentTypes>} other The other data
* @returns {ActionRowBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = ActionRowBuilder;

View File

@@ -3,74 +3,26 @@
const Util = require('../util/Util');
/**
* Represents an attachment.
* @typedef {Object} AttachmentPayload
* @property {?string} name The name of the attachment
* @property {Stream|BufferResolvable} attachment The attachment in this payload
* @property {?string} description The description of the attachment
*/
/**
* Represents an attachment
*/
class Attachment {
/**
* @param {BufferResolvable|Stream} attachment The file
* @param {string} [name=null] The name of the file, if any
* @param {APIAttachment} [data] Extra data
*/
constructor(attachment, name = null, data) {
this.attachment = attachment;
constructor({ url, filename, ...data }) {
this.attachment = url;
/**
* The name of this attachment
* @type {?string}
* @type {string}
*/
this.name = name;
this.name = filename;
if (data) this._patch(data);
}
/**
* Sets the description of this attachment.
* @param {string} description The description of the file
* @returns {Attachment} This attachment
*/
setDescription(description) {
this.description = description;
return this;
}
/**
* Sets the file of this attachment.
* @param {BufferResolvable|Stream} attachment The file
* @param {string} [name=null] The name of the file, if any
* @returns {Attachment} This attachment
*/
setFile(attachment, name = null) {
this.attachment = attachment;
this.name = name;
return this;
}
/**
* Sets the name of this attachment.
* @param {string} name The name of the file
* @returns {Attachment} This attachment
*/
setName(name) {
this.name = name;
return this;
}
/**
* Sets whether this attachment is a spoiler
* @param {boolean} [spoiler=true] Whether the attachment should be marked as a spoiler
* @returns {Attachment} This attachment
*/
setSpoiler(spoiler = true) {
if (spoiler === this.spoiler) return this;
if (!spoiler) {
while (this.spoiler) {
this.name = this.name.slice('SPOILER_'.length);
}
return this;
}
this.name = `SPOILER_${this.name}`;
return this;
}
_patch(data) {
/**
* The attachment's id
@@ -164,8 +116,3 @@ class Attachment {
}
module.exports = Attachment;
/**
* @external APIAttachment
* @see {@link https://discord.com/developers/docs/resources/channel#attachment-object}
*/

View File

@@ -0,0 +1,110 @@
'use strict';
const Util = require('../util/Util');
/**
* Represents an attachment builder
*/
class AttachmentBuilder {
/**
* @param {BufferResolvable|Stream} attachment The file
* @param {APIAttachment} [data] Extra data
*/
constructor(attachment, data = {}) {
/**
* The file associated with this attachment.
* @type {BufferResolvable|Stream}
*/
this.attachment = attachment;
/**
* The name of this attachment
* @type {?string}
*/
this.name = data.name;
/**
* The description of the attachment
* @type {?string}
*/
this.description = data.description;
}
/**
* Sets the description of this attachment.
* @param {string} description The description of the file
* @returns {AttachmentBuilder} This attachment
*/
setDescription(description) {
this.description = description;
return this;
}
/**
* Sets the file of this attachment.
* @param {BufferResolvable|Stream} attachment The file
* @returns {AttachmentBuilder} This attachment
*/
setFile(attachment) {
this.attachment = attachment;
return this;
}
/**
* Sets the name of this attachment.
* @param {string} name The name of the file
* @returns {AttachmentBuilder} This attachment
*/
setName(name) {
this.name = name;
return this;
}
/**
* Sets whether this attachment is a spoiler
* @param {boolean} [spoiler=true] Whether the attachment should be marked as a spoiler
* @returns {AttachmentBuilder} This attachment
*/
setSpoiler(spoiler = true) {
if (spoiler === this.spoiler) return this;
if (!spoiler) {
while (this.spoiler) {
this.name = this.name.slice('SPOILER_'.length);
}
return this;
}
this.name = `SPOILER_${this.name}`;
return this;
}
/**
* Whether or not this attachment has been marked as a spoiler
* @type {boolean}
* @readonly
*/
get spoiler() {
return Util.basename(this.name).startsWith('SPOILER_');
}
toJSON() {
return Util.flatten(this);
}
/**
* Makes a new builder instance from a preexisting attachment structure.
* @param {JSONEncodable<AttachmentPayload>} other The builder to construct a new instance from
* @returns {AttachmentBuilder}
*/
static from(other) {
return new AttachmentBuilder(other.attachment, {
name: other.name,
description: other.description,
});
}
}
module.exports = AttachmentBuilder;
/**
* @external APIAttachment
* @see {@link https://discord.com/developers/docs/resources/channel#attachment-object}
*/

View File

@@ -32,7 +32,7 @@ class AutocompleteInteraction extends Interaction {
/**
* The invoked application command's type
* @type {ApplicationCommandType.ChatInput}
* @type {ApplicationCommandType}
*/
this.commandType = data.data.type;

View File

@@ -110,54 +110,6 @@ class Channel extends Base {
return this.client.channels.fetch(this.id, { force });
}
/**
* Indicates whether this channel is a {@link TextChannel}.
* @returns {boolean}
*/
isText() {
return this.type === ChannelType.GuildText;
}
/**
* Indicates whether this channel is a {@link DMChannel}.
* @returns {boolean}
*/
isDM() {
return this.type === ChannelType.DM;
}
/**
* Indicates whether this channel is a {@link VoiceChannel}.
* @returns {boolean}
*/
isVoice() {
return this.type === ChannelType.GuildVoice;
}
/**
* Indicates whether this channel is a {@link PartialGroupDMChannel}.
* @returns {boolean}
*/
isGroupDM() {
return this.type === ChannelType.GroupDM;
}
/**
* Indicates whether this channel is a {@link CategoryChannel}.
* @returns {boolean}
*/
isCategory() {
return this.type === ChannelType.GuildCategory;
}
/**
* Indicates whether this channel is a {@link NewsChannel}.
* @returns {boolean}
*/
isNews() {
return this.type === ChannelType.GuildNews;
}
/**
* Indicates whether this channel is a {@link ThreadChannel}.
* @returns {boolean}
@@ -166,22 +118,6 @@ class Channel extends Base {
return ThreadChannelTypes.includes(this.type);
}
/**
* Indicates whether this channel is a {@link StageChannel}.
* @returns {boolean}
*/
isStage() {
return this.type === ChannelType.GuildStageVoice;
}
/**
* Indicates whether this channel is a {@link DirectoryChannel}
* @returns {boolean}
*/
isDirectory() {
return this.type === ChannelType.GuildDirectory;
}
/**
* Indicates whether this channel is {@link TextBasedChannels text-based}.
* @returns {boolean}

View File

@@ -40,6 +40,12 @@ class CommandInteraction extends Interaction {
*/
this.commandType = data.data.type;
/**
* The id of the guild the invoked application command is registered to
* @type {?Snowflake}
*/
this.commandGuildId = data.data.guild_id ?? null;
/**
* Whether the reply to this interaction has been deferred
* @type {boolean}
@@ -133,7 +139,7 @@ class CommandInteraction extends Interaction {
if (attachments) {
result.attachments = new Collection();
for (const attachment of Object.values(attachments)) {
const patched = new Attachment(attachment.url, attachment.filename, attachment);
const patched = new Attachment(attachment);
result.attachments.set(attachment.id, patched);
}
}
@@ -189,7 +195,7 @@ class CommandInteraction extends Interaction {
if (role) result.role = this.guild?.roles._add(role) ?? role;
const attachment = resolved.attachments?.[option.value];
if (attachment) result.attachment = new Attachment(attachment.url, attachment.filename, attachment);
if (attachment) result.attachment = new Attachment(attachment);
}
return result;

View File

@@ -248,7 +248,7 @@ class CommandInteractionOptionResolver {
* Gets a message option.
* @param {string} name The name of the option.
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
* @returns {?(Message|APIMessage)}
* @returns {?Message}
* The value of the option, or null if not set and not required.
*/
getMessage(name, required = false) {

View File

@@ -3,6 +3,9 @@
const { ApplicationCommandOptionType } = require('discord-api-types/v10');
const CommandInteraction = require('./CommandInteraction');
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
const { lazy } = require('../util/Util');
const getMessage = lazy(() => require('./Message').Message);
/**
* Represents a context menu interaction.
@@ -48,7 +51,9 @@ class ContextMenuCommandInteraction extends CommandInteraction {
name: 'message',
type: '_MESSAGE',
value: target_id,
message: this.channel?.messages._add(resolved.messages[target_id]) ?? resolved.messages[target_id],
message:
this.channel?.messages._add(resolved.messages[target_id]) ??
new (getMessage())(this.client, resolved.messages[target_id]),
});
}

View File

@@ -3,7 +3,8 @@
const { Channel } = require('./Channel');
/**
* Represents a channel that displays a directory of guilds
* Represents a channel that displays a directory of guilds.
* @extends {Channel}
*/
class DirectoryChannel extends Channel {
_patch(data) {

View File

@@ -237,7 +237,7 @@ class Guild extends AnonymousGuild {
if ('mfa_level' in data) {
/**
* The required MFA level for this guild
* @type {MFALevel}
* @type {GuildMFALevel}
*/
this.mfaLevel = data.mfa_level;
}
@@ -713,12 +713,13 @@ class Guild extends AnonymousGuild {
return new GuildAuditLogs(this, data);
}
/* eslint-disable max-len */
/**
* The data for editing a guild.
* @typedef {Object} GuildEditData
* @property {string} [name] The name of the guild
* @property {?(VerificationLevel|number)} [verificationLevel] The verification level of the guild
* @property {?(ExplicitContentFilterLevel|number)} [explicitContentFilter] The level of the explicit content filter
* @property {?(GuildVerificationLevel|number)} [verificationLevel] The verification level of the guild
* @property {?(GuildExplicitContentFilterLevel|number)} [explicitContentFilter] The level of the explicit content filter
* @property {?VoiceChannelResolvable} [afkChannel] The AFK channel of the guild
* @property {?TextChannelResolvable} [systemChannel] The system channel of the guild
* @property {number} [afkTimeout] The AFK timeout of the guild
@@ -727,7 +728,7 @@ class Guild extends AnonymousGuild {
* @property {?(BufferResolvable|Base64Resolvable)} [splash] The invite splash image of the guild
* @property {?(BufferResolvable|Base64Resolvable)} [discoverySplash] The discovery splash image of the guild
* @property {?(BufferResolvable|Base64Resolvable)} [banner] The banner of the guild
* @property {?(DefaultMessageNotificationLevel|number)} [defaultMessageNotifications] The default message
* @property {?(GuildDefaultMessageNotificationLevel|number)} [defaultMessageNotifications] The default message
* notification level of the guild
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild
* @property {?TextChannelResolvable} [rulesChannel] The rules channel of the guild
@@ -737,6 +738,7 @@ class Guild extends AnonymousGuild {
* @property {?string} [description] The discovery description of the guild
* @property {GuildFeature[]} [features] The features of the guild
*/
/* eslint-enable max-len */
/**
* Data that can be resolved to a Text Channel object. This can be:
@@ -882,9 +884,10 @@ class Guild extends AnonymousGuild {
return new WelcomeScreen(this, patchData);
}
/* eslint-disable max-len */
/**
* Edits the level of the explicit content filter.
* @param {?(ExplicitContentFilterLevel|number)} explicitContentFilter The new level of the explicit content filter
* @param {?(GuildExplicitContentFilterLevel|number)} explicitContentFilter The new level of the explicit content filter
* @param {string} [reason] Reason for changing the level of the guild's explicit content filter
* @returns {Promise<Guild>}
*/
@@ -892,10 +895,9 @@ class Guild extends AnonymousGuild {
return this.edit({ explicitContentFilter }, reason);
}
/* eslint-disable max-len */
/**
* Edits the setting of the default message notifications of the guild.
* @param {?(DefaultMessageNotificationLevel|number)} defaultMessageNotifications The new default message notification level of the guild
* @param {?(GuildDefaultMessageNotificationLevel|number)} defaultMessageNotifications The new default message notification level of the guild
* @param {string} [reason] Reason for changing the setting of the default message notifications
* @returns {Promise<Guild>}
*/
@@ -931,7 +933,7 @@ class Guild extends AnonymousGuild {
/**
* Edits the verification level of the guild.
* @param {?VerificationLevel} verificationLevel The new verification level of the guild
* @param {?GuildVerificationLevel} verificationLevel The new verification level of the guild
* @param {string} [reason] Reason for changing the guild's verification level
* @returns {Promise<Guild>}
* @example

View File

@@ -159,7 +159,7 @@ class Message extends Base {
this.attachments = new Collection();
if (data.attachments) {
for (const attachment of data.attachments) {
this.attachments.set(attachment.id, new Attachment(attachment.url, attachment.filename, attachment));
this.attachments.set(attachment.id, new Attachment(attachment));
}
}
} else {
@@ -644,7 +644,8 @@ class Message extends Base {
* Only `MessageFlags.SuppressEmbeds` can be edited.
* @property {Attachment[]} [attachments] An array of attachments to keep,
* all attachments will be kept if omitted
* @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files to add to the message
* @property {Array<JSONEncodable<AttachmentPayload>>|BufferResolvable[]|Attachment[]|AttachmentBuilder[]} [files]
* Files to add to the message
* @property {ActionRow[]|ActionRowOptions[]} [components]
* Action rows containing interactive components for the message (buttons, select menus)
*/

View File

@@ -3,6 +3,9 @@
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { lazy } = require('../util/Util');
const getMessage = lazy(() => require('./Message').Message);
/**
* Represents a message component interaction.
@@ -21,9 +24,9 @@ class MessageComponentInteraction extends Interaction {
/**
* The message to which the component was attached
* @type {Message|APIMessage}
* @type {Message}
*/
this.message = this.channel?.messages._add(data.message) ?? data.message;
this.message = this.channel?.messages._add(data.message) ?? new (getMessage())(client, data.message);
/**
* The custom id of the component which was interacted with

View File

@@ -224,7 +224,8 @@ class MessagePayload {
/**
* Resolves a single file into an object sendable to the API.
* @param {BufferResolvable|Stream|FileOptions|Attachment} fileLike Something that could be resolved to a file
* @param {BufferResolvable|Stream|JSONEncodable<AttachmentPayload>} fileLike Something that could
* be resolved to a file
* @returns {Promise<RawFile>}
*/
static async resolveFile(fileLike) {

View File

@@ -52,6 +52,14 @@ class MessageReaction {
}
}
/**
* Makes the client user react with this reaction
* @returns {Promise<MessageReaction>}
*/
react() {
return this.message.react(this.emoji);
}
/**
* Removes all users from this reaction.
* @returns {Promise<MessageReaction>}

View File

@@ -4,6 +4,9 @@ const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const ModalSubmitFields = require('./ModalSubmitFields');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { lazy } = require('../util/Util');
const getMessage = lazy(() => require('./Message').Message);
/**
* @typedef {Object} ModalData
@@ -34,9 +37,9 @@ class ModalSubmitInteraction extends Interaction {
if ('message' in data) {
/**
* The message associated with this interaction
* @type {?(Message|APIMessage)}
* @type {?Message}
*/
this.message = this.channel?.messages._add(data.message) ?? data.message;
this.message = this.channel?.messages._add(data.message) ?? new (getMessage())(this.client, data.message);
} else {
this.message = null;
}

View File

@@ -1,6 +1,6 @@
'use strict';
const { SelectMenuOptionBuilder: BuildersSelectMenuOption } = require('@discordjs/builders');
const { SelectMenuOptionBuilder: BuildersSelectMenuOption, isJSONEncodable } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
const Util = require('../util/Util');
@@ -28,6 +28,18 @@ class SelectMenuOptionBuilder extends BuildersSelectMenuOption {
}
return super.setEmoji(emoji);
}
/**
* Creates a new select menu option builder from JSON data
* @param {JSONEncodable<APISelectMenuOption>|APISelectMenuOption} other The other data
* @returns {SelectMenuOptionBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
}
}
module.exports = SelectMenuOptionBuilder;

View File

@@ -70,10 +70,10 @@ class Sticker extends Base {
if ('tags' in sticker) {
/**
* An array of tags for the sticker
* @type {?string[]}
* Autocomplete/suggestions for the sticker
* @type {?string}
*/
this.tags = sticker.tags.split(', ');
this.tags = sticker.tags;
} else {
this.tags ??= null;
}
@@ -246,8 +246,7 @@ class Sticker extends Base {
other.format === this.format &&
other.name === this.name &&
other.packId === this.packId &&
other.tags.length === this.tags.length &&
other.tags.every(tag => this.tags.includes(tag)) &&
other.tags === this.tags &&
other.available === this.available &&
other.guildId === this.guildId &&
other.sortValue === this.sortValue
@@ -257,7 +256,7 @@ class Sticker extends Base {
other.id === this.id &&
other.description === this.description &&
other.name === this.name &&
other.tags === this.tags.join(', ')
other.tags === this.tags
);
}
}

View File

@@ -8,6 +8,7 @@ const MessageManager = require('../managers/MessageManager');
/**
* Represents a guild voice channel on Discord.
* @extends {BaseGuildVoiceChannel}
* @implements {TextBasedChannel}
*/
class VoiceChannel extends BaseGuildVoiceChannel {
constructor(guild, data, client) {

View File

@@ -6,6 +6,9 @@ const { Routes, WebhookType } = require('discord-api-types/v10');
const MessagePayload = require('./MessagePayload');
const { Error } = require('../errors');
const DataResolver = require('../util/DataResolver');
const { lazy } = require('../util/Util');
const getMessage = lazy(() => require('./Message').Message);
/**
* Represents a webhook.
@@ -132,7 +135,8 @@ class Webhook {
* @typedef {Object} WebhookEditMessageOptions
* @property {Embed[]|APIEmbed[]} [embeds] See {@link WebhookMessageOptions#embeds}
* @property {string} [content] See {@link BaseMessageOptions#content}
* @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] See {@link BaseMessageOptions#files}
* @property {JSONEncodable<AttachmentPayload>|BufferResolvable[]|Attachment[]|AttachmentBuilder[]} [files]
* See {@link BaseMessageOptions#files}
* @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions}
* @property {Attachment[]} [attachments] Attachments to send with the message
* @property {ActionRow[]|ActionRowOptions[]} [components]
@@ -144,7 +148,7 @@ class Webhook {
/**
* Sends a message with this webhook.
* @param {string|MessagePayload|WebhookMessageOptions} options The options to provide
* @returns {Promise<Message|APIMessage>}
* @returns {Promise<Message>}
* @example
* // Send a basic message
* webhook.send('hello!')
@@ -207,7 +211,7 @@ class Webhook {
const { body, files } = await messagePayload.resolveFiles();
const d = await this.client.rest.post(Routes.webhook(this.id, this.token), { body, files, query, auth: false });
return this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? d;
return this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? new (getMessage())(this.client, d);
}
/**
@@ -282,8 +286,7 @@ class Webhook {
* Gets a message that was sent by this webhook.
* @param {Snowflake|'@original'} message The id of the message to fetch
* @param {WebhookFetchMessageOptions} [options={}] The options to provide to fetch the message.
* @returns {Promise<Message|APIMessage>} Returns the raw message data if the webhook was instantiated as a
* {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned
* @returns {Promise<Message>} Returns the message sent by this webhook
*/
async fetchMessage(message, { cache = true, threadId } = {}) {
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
@@ -292,15 +295,17 @@ class Webhook {
query: threadId ? makeURLSearchParams({ thread_id: threadId }) : undefined,
auth: false,
});
return this.client.channels?.cache.get(data.channel_id)?.messages._add(data, cache) ?? data;
return (
this.client.channels?.cache.get(data.channel_id)?.messages._add(data, cache) ??
new (getMessage())(this.client, data)
);
}
/**
* Edits a message that was sent by this webhook.
* @param {MessageResolvable|'@original'} message The message to edit
* @param {string|MessagePayload|WebhookEditMessageOptions} options The options to provide
* @returns {Promise<Message|APIMessage>} Returns the raw message data if the webhook was instantiated as a
* {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned
* @returns {Promise<Message>} Returns the message edited by this webhook
*/
async editMessage(message, options) {
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
@@ -325,7 +330,7 @@ class Webhook {
);
const messageManager = this.client.channels?.cache.get(d.channel_id)?.messages;
if (!messageManager) return d;
if (!messageManager) return new (getMessage())(this.client, d);
const existing = messageManager.cache.get(d.id);
if (!existing) return messageManager._add(d);

View File

@@ -103,21 +103,31 @@ class Collector extends EventEmitter {
* @emits Collector#collect
*/
async handleCollect(...args) {
const collect = await this.collect(...args);
const collectedId = await this.collect(...args);
if (collect && (await this.filter(...args, this.collected))) {
this.collected.set(collect, args[0]);
if (collectedId) {
const filterResult = await this.filter(...args, this.collected);
if (filterResult) {
this.collected.set(collectedId, args[0]);
/**
* Emitted whenever an element is collected.
* @event Collector#collect
* @param {...*} args The arguments emitted by the listener
*/
this.emit('collect', ...args);
/**
* Emitted whenever an element is collected.
* @event Collector#collect
* @param {...*} args The arguments emitted by the listener
*/
this.emit('collect', ...args);
if (this._idletimeout) {
clearTimeout(this._idletimeout);
this._idletimeout = setTimeout(() => this.stop('idle'), this.options.idle).unref();
if (this._idletimeout) {
clearTimeout(this._idletimeout);
this._idletimeout = setTimeout(() => this.stop('idle'), this.options.idle).unref();
}
} else {
/**
* Emitted whenever an element is not collected by the collector.
* @event Collector#ignore
* @param {...*} args The arguments emitted by the listener
*/
this.emit('ignore', ...args);
}
}
this.checkEnd();

View File

@@ -8,10 +8,10 @@ const InteractionResponse = require('../InteractionResponse');
const MessagePayload = require('../MessagePayload');
/**
* @typedef {Object} ModalData
* @typedef {Object} ModalComponentData
* @property {string} title The title of the modal
* @property {string} customId The custom id of the modal
* @property {ActionRowData[]} components The components within this modal
* @property {ActionRow[]} components The components within this modal
*/
/**

View File

@@ -64,7 +64,7 @@ class TextBasedChannel {
* @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files to send with the message
* @property {ActionRow[]|ActionRowOptions[]} [components]
* Action rows containing interactive components for the message (buttons, select menus)
* @property {Attachment[]} [attachments] Attachments to send in the message
* @property {Array<JSONEncodable<AttachmentPayload>>} [attachments] Attachments to send in the message
*/
/**

View File

@@ -1,3 +1,13 @@
/**
* @external ActivityFlags
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ActivityFlags}
*/
/**
* @external ActivityType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ActivityType}
*/
/**
* @external APIActionRowComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIActionRowComponent}
@@ -13,21 +23,6 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIApplicationCommand}
*/
/**
* @external ApplicationCommandType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationCommandType}
*/
/**
* @external ApplicationCommandOptionType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationCommandOptionType}
*/
/**
* @external ApplicationCommandPermissionType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationCommandPermissionType}
*/
/**
* @external APIApplicationCommandOption
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIApplicationCommandOption}
@@ -48,6 +43,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmbed}
*/
/**
* @external APIEmbedField
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmbedField}
*/
/**
* @external APIEmoji
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmoji}
@@ -88,11 +88,21 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMessageComponent}
*/
/**
* @external APIMessageComponentEmoji
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMessageComponentEmoji}
*/
/**
* @external APIModalInteractionResponse
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIModalInteractionResponse}
*/
/**
* @external APIModalComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIModalComponent}
*/
/**
* @external APIModalSubmission
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIModalSubmission}
@@ -113,11 +123,36 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuOption}
*/
/**
* @external APITextInputComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APITextInputComponent}
*/
/**
* @external APIUser
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIUser}
*/
/**
* @external ApplicationCommandType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationCommandType}
*/
/**
* @external ApplicationCommandOptionType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationCommandOptionType}
*/
/**
* @external ApplicationCommandPermissionType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationCommandPermissionType}
*/
/**
* @external ApplicationFlags
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationFlags}
*/
/**
* @external AuditLogEvent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/AuditLogEvent}
@@ -238,6 +273,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-rest/common/enum/Locale}
*/
/**
* @external LocaleString
* @see {@link https://discord-api-types.dev/api/discord-api-types-rest/common#LocaleString}
*/
/**
* @external MessageActivityType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/MessageActivityType}
@@ -258,6 +298,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/OAuth2Scopes}
*/
/**
* @external OverwriteType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/OverwriteType}
*/
/**
* @external ChannelType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ChannelType}
*/
/**
* @external PermissionFlagsBits
* @see {@link https://discord-api-types.dev/api/discord-api-types-payloads/common#PermissionFlagsBits}

View File

@@ -18,7 +18,7 @@ const { ComponentType } = require('discord-api-types/v10');
* @property {ButtonStyle} style The style of the button
* @property {?boolean} disabled Whether this button is disabled
* @property {string} label The label of this button
* @property {?APIComponentEmoji} emoji The emoji on this button
* @property {?APIMessageComponentEmoji} emoji The emoji on this button
* @property {?string} customId The custom id of the button
* @property {?string} url The URL of the button
*/
@@ -28,7 +28,7 @@ const { ComponentType } = require('discord-api-types/v10');
* @property {string} label The label of the option
* @property {string} value The value of the option
* @property {?string} description The description of the option
* @property {?APIComponentEmoji} emoji The emoji on the option
* @property {?APIMessageComponentEmoji} emoji The emoji on the option
* @property {?boolean} default Whether this option is selected by default
*/

View File

@@ -17,6 +17,8 @@ const Transformers = require('./Transformers');
* @property {number|number[]|string} [shards] The shard's id to run, or an array of shard ids. If not specified,
* the client will spawn {@link ClientOptions#shardCount} shards. If set to `auto`, it will fetch the
* recommended amount of shards from Discord and spawn that amount
* @property {number} [closeTimeout=1] The amount of time in milliseconds to wait for the close frame to be received
* from the WebSocket. Don't have this too high/low. Its best to have it between 2_000-6_000 ms.
* @property {number} [shardCount=1] The total amount of shards used by all processes of this bot
* (e.g. recommended shard count, shard count of the ShardingManager)
* @property {CacheFactory} [makeCache] Function to create a cache.
@@ -72,6 +74,7 @@ class Options extends null {
*/
static createDefault() {
return {
closeTimeout: 5_000,
waitGuildTimeout: 15_000,
shardCount: 1,
makeCache: this.cacheWithLimits(this.DefaultMakeCacheSettings),

View File

@@ -521,6 +521,16 @@ class Util extends null {
static cleanCodeBlockContent(text) {
return text.replaceAll('```', '`\u200b``');
}
/**
* Lazily evaluates a callback function
* @param {Function} cb The callback to lazily evaluate
* @returns {Function}
*/
static lazy(cb) {
let defaultValue;
return () => (defaultValue ??= cb());
}
}
module.exports = Util;

View File

@@ -29,6 +29,7 @@ import {
ModalBuilder as BuildersModal,
AnyComponentBuilder,
ComponentBuilder,
type RestOrArray,
} from '@discordjs/builders';
import { Collection } from '@discordjs/collection';
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
@@ -117,6 +118,7 @@ import {
LocalizationMap,
LocaleString,
MessageActivityType,
APIAttachment,
} from 'discord-api-types/v10';
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
@@ -390,16 +392,19 @@ export interface InteractionResponseFields<Cached extends CacheType = CacheType>
ephemeral: boolean | null;
replied: boolean;
webhook: InteractionWebhook;
reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<Message>;
reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
deleteReply(): Promise<void>;
editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise<GuildCacheMessage<Cached>>;
deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise<Message>;
deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise<Message>;
deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
fetchReply(): Promise<GuildCacheMessage<Cached>>;
followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
fetchReply(): Promise<Message>;
followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<Message>;
showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
modal:
| JSONEncodable<APIModalInteractionResponseCallbackData>
| ModalComponentData
| APIModalInteractionResponseCallbackData,
): Promise<void>;
awaitModalSubmit(options: AwaitModalSubmitOptions<ModalSubmitInteraction>): Promise<ModalSubmitInteraction<Cached>>;
}
@@ -427,6 +432,7 @@ export abstract class CommandInteraction<Cached extends CacheType = CacheType> e
public commandId: Snowflake;
public commandName: string;
public commandType: ApplicationCommandType;
public commandGuildId: Snowflake | null;
public deferred: boolean;
public ephemeral: boolean | null;
public replied: boolean;
@@ -434,18 +440,23 @@ export abstract class CommandInteraction<Cached extends CacheType = CacheType> e
public inGuild(): this is CommandInteraction<'raw' | 'cached'>;
public inCachedGuild(): this is CommandInteraction<'cached'>;
public inRawGuild(): this is CommandInteraction<'raw'>;
public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public deferReply(
options: InteractionDeferReplyOptions & { fetchReply: true },
): Promise<Message<BooleanCache<Cached>>>;
public deferReply(options?: InteractionDeferReplyOptions): Promise<InteractionResponse<BooleanCache<Cached>>>;
public deleteReply(): Promise<void>;
public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise<GuildCacheMessage<Cached>>;
public fetchReply(): Promise<GuildCacheMessage<Cached>>;
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise<Message>;
public fetchReply(): Promise<Message>;
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<Message>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<Message<BooleanCache<Cached>>>;
public reply(
options: string | MessagePayload | InteractionReplyOptions,
): Promise<InteractionResponse<BooleanCache<Cached>>>;
public showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
modal:
| JSONEncodable<APIModalInteractionResponseCallbackData>
| ModalComponentData
| APIModalInteractionResponseCallbackData,
): Promise<void>;
public awaitModalSubmit(
options: AwaitModalSubmitOptions<ModalSubmitInteraction>,
@@ -599,17 +610,18 @@ export class ButtonBuilder extends BuilderButtonComponent {
export class SelectMenuBuilder extends BuilderSelectMenuComponent {
public constructor(data?: Partial<SelectMenuComponentData | APISelectMenuComponent>);
public override addOptions(
options: (BuildersSelectMenuOption | SelectMenuComponentOptionData | APISelectMenuOption)[],
...options: RestOrArray<BuildersSelectMenuOption | SelectMenuComponentOptionData | APISelectMenuOption>
): this;
public override setOptions(
options: (BuildersSelectMenuOption | SelectMenuComponentOptionData | APISelectMenuOption)[],
...options: RestOrArray<BuildersSelectMenuOption | SelectMenuComponentOptionData | APISelectMenuOption>
): this;
public static from(other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent): SelectMenuBuilder;
}
export class SelectMenuOptionBuilder extends BuildersSelectMenuOption {
public constructor(data?: SelectMenuComponentOptionData | APISelectMenuOption);
public setEmoji(emoji: ComponentEmojiResolvable): this;
public override setEmoji(emoji: ComponentEmojiResolvable): this;
public static from(other: JSONEncodable<APISelectMenuOption> | APISelectMenuOption): SelectMenuOptionBuilder;
}
export class ModalBuilder extends BuildersModal {
@@ -738,15 +750,7 @@ export abstract class Channel extends Base {
public get url(): string;
public delete(): Promise<this>;
public fetch(force?: boolean): Promise<this>;
public isText(): this is TextChannel;
public isDM(): this is DMChannel;
public isVoice(): this is VoiceChannel;
public isGroupDM(): this is PartialGroupDMChannel;
public isCategory(): this is CategoryChannel;
public isNews(): this is NewsChannel;
public isThread(): this is ThreadChannel;
public isStage(): this is StageChannel;
public isDirectory(): this is DirectoryChannel;
public isTextBased(): this is TextBasedChannel;
public isDMBased(): this is PartialGroupDMChannel | DMChannel | PartialDMChannel;
public isVoiceBased(): this is VoiceBasedChannel;
@@ -872,6 +876,7 @@ export { Collection } from '@discordjs/collection';
export interface CollectorEventTypes<K, V, F extends unknown[] = []> {
collect: [V, ...F];
ignore: [V, ...F];
dispose: [V, ...F];
end: [collected: Collection<K, V>, reason: string];
}
@@ -1112,6 +1117,7 @@ export class Guild extends AnonymousGuild {
options?: GuildAuditLogsFetchOptions<T>,
): Promise<GuildAuditLogs<T>>;
public fetchIntegrations(): Promise<Collection<Snowflake | string, Integration>>;
public fetchMe(options?: BaseFetchOptions): Promise<GuildMember>;
public fetchOwner(options?: BaseFetchOptions): Promise<GuildMember>;
public fetchPreview(): Promise<GuildPreview>;
public fetchTemplates(): Promise<Collection<GuildTemplate['code'], GuildTemplate>>;
@@ -1520,11 +1526,11 @@ export class InteractionCollector<T extends Interaction> extends Collector<Snowf
public collect(interaction: Interaction): Snowflake;
public empty(): void;
public dispose(interaction: Interaction): Snowflake;
public on(event: 'collect' | 'dispose', listener: (interaction: T) => Awaitable<void>): this;
public on(event: 'collect' | 'dispose' | 'ignore', listener: (interaction: T) => Awaitable<void>): this;
public on(event: 'end', listener: (collected: Collection<Snowflake, T>, reason: string) => Awaitable<void>): this;
public on(event: string, listener: (...args: any[]) => Awaitable<void>): this;
public once(event: 'collect' | 'dispose', listener: (interaction: T) => Awaitable<void>): this;
public once(event: 'collect' | 'dispose' | 'ignore', listener: (interaction: T) => Awaitable<void>): this;
public once(event: 'end', listener: (collected: Collection<Snowflake, T>, reason: string) => Awaitable<void>): this;
public once(event: string, listener: (...args: any[]) => Awaitable<void>): this;
}
@@ -1532,7 +1538,7 @@ export class InteractionCollector<T extends Interaction> extends Collector<Snowf
export class InteractionWebhook extends PartialWebhookMixin() {
public constructor(client: Client, id: Snowflake, token: string);
public token: string;
public send(options: string | MessagePayload | InteractionReplyOptions): Promise<Message | APIMessage>;
public send(options: string | MessagePayload | InteractionReplyOptions): Promise<Message>;
}
export class Invite extends Base {
@@ -1696,9 +1702,22 @@ export class Message<Cached extends boolean = boolean> extends Base {
public inGuild(): this is Message<true> & this;
}
export class Attachment {
public constructor(attachment: BufferResolvable | Stream, name?: string, data?: RawAttachmentData);
export class AttachmentBuilder {
public constructor(attachment: BufferResolvable | Stream, data?: AttachmentData);
public attachment: BufferResolvable | Stream;
public description: string | null;
public name: string | null;
public get spoiler(): boolean;
public setDescription(description: string): this;
public setFile(attachment: BufferResolvable | Stream, name?: string): this;
public setName(name: string): this;
public setSpoiler(spoiler?: boolean): this;
public toJSON(): unknown;
public static from(other: JSONEncodable<AttachmentPayload>): AttachmentBuilder;
}
export class Attachment {
private constructor(data: APIAttachment);
public attachment: BufferResolvable | Stream;
public contentType: string | null;
public description: string | null;
@@ -1711,10 +1730,6 @@ export class Attachment {
public get spoiler(): boolean;
public url: string;
public width: number | null;
public setDescription(description: string): this;
public setFile(attachment: BufferResolvable | Stream, name?: string): this;
public setName(name: string): this;
public setSpoiler(spoiler?: boolean): this;
public toJSON(): unknown;
}
@@ -1745,30 +1760,37 @@ export class MessageComponentInteraction<Cached extends CacheType = CacheType> e
public channelId: Snowflake;
public deferred: boolean;
public ephemeral: boolean | null;
public message: GuildCacheMessage<Cached>;
public message: Message<BooleanCache<Cached>>;
public replied: boolean;
public webhook: InteractionWebhook;
public inGuild(): this is MessageComponentInteraction<'raw' | 'cached'>;
public inCachedGuild(): this is MessageComponentInteraction<'cached'>;
public inRawGuild(): this is MessageComponentInteraction<'raw'>;
public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public deferReply(
options: InteractionDeferReplyOptions & { fetchReply: true },
): Promise<Message<BooleanCache<Cached>>>;
public deferReply(options?: InteractionDeferReplyOptions): Promise<InteractionResponse<BooleanCache<Cached>>>;
public deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public deferUpdate(
options: InteractionDeferUpdateOptions & { fetchReply: true },
): Promise<Message<BooleanCache<Cached>>>;
public deferUpdate(options?: InteractionDeferUpdateOptions): Promise<InteractionResponse<BooleanCache<Cached>>>;
public deleteReply(): Promise<void>;
public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise<GuildCacheMessage<Cached>>;
public fetchReply(): Promise<GuildCacheMessage<Cached>>;
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise<Message>;
public fetchReply(): Promise<Message>;
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<Message>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<Message<BooleanCache<Cached>>>;
public reply(
options: string | MessagePayload | InteractionReplyOptions,
): Promise<InteractionResponse<BooleanCache<Cached>>>;
public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<Message>;
public update(
options: string | MessagePayload | InteractionUpdateOptions,
): Promise<InteractionResponse<BooleanCache<Cached>>>;
public showModal(
modal: JSONEncodable<APIModalInteractionResponseCallbackData> | ModalData | APIModalInteractionResponseCallbackData,
modal:
| JSONEncodable<APIModalInteractionResponseCallbackData>
| ModalComponentData
| APIModalInteractionResponseCallbackData,
): Promise<void>;
public awaitModalSubmit(
options: AwaitModalSubmitOptions<ModalSubmitInteraction>,
@@ -1838,7 +1860,9 @@ export class MessagePayload {
options: string | MessageOptions | WebhookMessageOptions,
extra?: MessageOptions | WebhookMessageOptions,
): MessagePayload;
public static resolveFile(fileLike: BufferResolvable | Stream | FileOptions | Attachment): Promise<RawFile>;
public static resolveFile(
fileLike: BufferResolvable | Stream | AttachmentPayload | JSONEncodable<AttachmentPayload>,
): Promise<RawFile>;
public makeContent(): string | undefined;
public resolveBody(): this;
@@ -1856,11 +1880,18 @@ export class MessageReaction {
public message: Message | PartialMessage;
public get partial(): false;
public users: ReactionUserManager;
public react(): Promise<MessageReaction>;
public remove(): Promise<MessageReaction>;
public fetch(): Promise<MessageReaction>;
public toJSON(): unknown;
}
export interface ModalComponentData {
customId: string;
title: string;
components: (ActionRow<ModalActionRowComponent> | ActionRowData<ModalActionRowComponentData>)[];
}
export interface BaseModalData {
customId: string;
type: ComponentType;
@@ -1889,12 +1920,12 @@ export class ModalSubmitFields {
export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = CacheType>
extends ModalSubmitInteraction<Cached> {
message: GuildCacheMessage<Cached>;
update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
message: Message<BooleanCache<Cached>>;
update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<Message>;
update(
options: string | MessagePayload | InteractionUpdateOptions,
): Promise<InteractionResponse<BooleanCache<Cached>>>;
deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise<Message>;
deferUpdate(options?: InteractionDeferUpdateOptions): Promise<InteractionResponse<BooleanCache<Cached>>>;
inGuild(): this is ModalMessageModalSubmitInteraction<'raw' | 'cached'>;
inCachedGuild(): this is ModalMessageModalSubmitInteraction<'cached'>;
@@ -1908,19 +1939,23 @@ export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extend
public readonly fields: ModalSubmitFields;
public deferred: boolean;
public ephemeral: boolean | null;
public message: GuildCacheMessage<Cached> | null;
public message: Message<BooleanCache<Cached>> | null;
public replied: boolean;
public readonly webhook: InteractionWebhook;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<Message>;
public reply(
options: string | MessagePayload | InteractionReplyOptions,
): Promise<InteractionResponse<BooleanCache<Cached>>>;
public deleteReply(): Promise<void>;
public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise<GuildCacheMessage<Cached>>;
public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public editReply(
options: string | MessagePayload | WebhookEditMessageOptions,
): Promise<Message<BooleanCache<Cached>>>;
public deferReply(
options: InteractionDeferReplyOptions & { fetchReply: true },
): Promise<Message<BooleanCache<Cached>>>;
public deferReply(options?: InteractionDeferReplyOptions): Promise<InteractionResponse<BooleanCache<Cached>>>;
public fetchReply(): Promise<GuildCacheMessage<Cached>>;
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
public fetchReply(): Promise<Message<BooleanCache<Cached>>>;
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<Message<BooleanCache<Cached>>>;
public inGuild(): this is ModalSubmitInteraction<'raw' | 'cached'>;
public inCachedGuild(): this is ModalSubmitInteraction<'cached'>;
public inRawGuild(): this is ModalSubmitInteraction<'raw'>;
@@ -2011,11 +2046,17 @@ export class ReactionCollector extends Collector<Snowflake | string, MessageReac
public dispose(reaction: MessageReaction, user: User): Snowflake | string | null;
public empty(): void;
public on(event: 'collect' | 'dispose' | 'remove', listener: (reaction: MessageReaction, user: User) => void): this;
public on(
event: 'collect' | 'dispose' | 'remove' | 'ignore',
listener: (reaction: MessageReaction, user: User) => void,
): this;
public on(event: 'end', listener: (collected: Collection<Snowflake, MessageReaction>, reason: string) => void): this;
public on(event: string, listener: (...args: any[]) => void): this;
public once(event: 'collect' | 'dispose' | 'remove', listener: (reaction: MessageReaction, user: User) => void): this;
public once(
event: 'collect' | 'dispose' | 'remove' | 'ignore',
listener: (reaction: MessageReaction, user: User) => void,
): this;
public once(
event: 'end',
listener: (collected: Collection<Snowflake, MessageReaction>, reason: string) => void,
@@ -2264,7 +2305,7 @@ export class Sticker extends Base {
public packId: Snowflake | null;
public get partial(): boolean;
public sortValue: number | null;
public tags: string[] | null;
public tags: string | null;
public type: StickerType | null;
public user: User | null;
public get url(): string;
@@ -2719,9 +2760,9 @@ export class WebhookClient extends WebhookMixin(BaseClient) {
public editMessage(
message: MessageResolvable,
options: string | MessagePayload | WebhookEditMessageOptions,
): Promise<APIMessage>;
public fetchMessage(message: Snowflake, options?: WebhookFetchMessageOptions): Promise<APIMessage>;
public send(options: string | MessagePayload | WebhookMessageOptions): Promise<APIMessage>;
): Promise<Message>;
public fetchMessage(message: Snowflake, options?: WebhookFetchMessageOptions): Promise<Message>;
public send(options: string | MessagePayload | WebhookMessageOptions): Promise<Message>;
}
export class WebSocketManager extends EventEmitter {
@@ -2779,6 +2820,8 @@ export class WebSocketShard extends EventEmitter {
private eventsAttached: boolean;
private expectedGuilds: Set<Snowflake> | null;
private readyTimeout: NodeJS.Timeout | null;
private closeEmitted: boolean;
private wsCloseTimeout: NodeJS.Timeout | null;
public manager: WebSocketManager;
public id: number;
@@ -2794,6 +2837,7 @@ export class WebSocketShard extends EventEmitter {
private onPacket(packet: unknown): void;
private checkReady(): void;
private setHelloTimeout(time?: number): void;
private setWsCloseTimeout(time?: number): void;
private setHeartbeatTimer(time: number): void;
private sendHeartbeat(): void;
private ackHeartbeat(): void;
@@ -2803,6 +2847,7 @@ export class WebSocketShard extends EventEmitter {
private _send(data: unknown): void;
private processQueue(): void;
private destroy(destroyOptions?: { closeCode?: number; reset?: boolean; emit?: boolean; log?: boolean }): void;
private emitClose(event?: CloseEvent): void;
private _cleanupConnection(): void;
private _emitDestroyed(): void;
@@ -3199,7 +3244,7 @@ export class GuildStickerManager extends CachedManager<Snowflake, Sticker, Stick
private constructor(guild: Guild, iterable?: Iterable<RawStickerData>);
public guild: Guild;
public create(
file: BufferResolvable | Stream | FileOptions | Attachment,
file: BufferResolvable | Stream | AttachmentPayload | JSONEncodable<AttachmentBuilder>,
name: string,
tags: string,
options?: GuildStickerCreateOptions,
@@ -3336,6 +3381,7 @@ export class ThreadMemberManager extends CachedManager<Snowflake, ThreadMember,
public add(member: UserResolvable | '@me', reason?: string): Promise<Snowflake>;
public fetch(options?: ThreadMemberFetchOptions): Promise<ThreadMember>;
public fetch(cache?: boolean): Promise<Collection<Snowflake, ThreadMember>>;
public fetchMe(options?: BaseFetchOptions): Promise<ThreadMember>;
public remove(id: Snowflake | '@me', reason?: string): Promise<Snowflake>;
}
@@ -3406,9 +3452,9 @@ export interface PartialWebhookFields {
editMessage(
message: MessageResolvable | '@original',
options: string | MessagePayload | WebhookEditMessageOptions,
): Promise<Message | APIMessage>;
fetchMessage(message: Snowflake | '@original', options?: WebhookFetchMessageOptions): Promise<Message | APIMessage>;
send(options: string | MessagePayload | Omit<WebhookMessageOptions, 'flags'>): Promise<Message | APIMessage>;
): Promise<Message>;
fetchMessage(message: Snowflake | '@original', options?: WebhookFetchMessageOptions): Promise<Message>;
send(options: string | MessagePayload | Omit<WebhookMessageOptions, 'flags'>): Promise<Message>;
}
export interface WebhookFields extends PartialWebhookFields {
@@ -3454,6 +3500,11 @@ export interface BaseApplicationCommandData {
defaultPermission?: boolean;
}
export interface AttachmentData {
name?: string;
description?: string;
}
export type CommandOptionDataTypeResolvable = ApplicationCommandOptionType;
export type CommandOptionChannelResolvableType = ApplicationCommandOptionType.Channel;
@@ -3751,7 +3802,7 @@ export interface ChannelData {
name?: string;
type?: Pick<typeof ChannelType, 'GuildText' | 'GuildNews'>;
position?: number;
topic?: string;
topic?: string | null;
nsfw?: boolean;
bitrate?: number;
userLimit?: number;
@@ -3874,6 +3925,7 @@ export interface ClientFetchInviteOptions {
export interface ClientOptions {
shards?: number | number[] | 'auto';
shardCount?: number;
closeTimeout?: number;
makeCache?: CacheFactory;
allowedMentions?: MessageMentionOptions;
partials?: Partials[];
@@ -3940,7 +3992,7 @@ export interface CommandInteractionOption<Cached extends CacheType = CacheType>
channel?: CacheTypeReducer<Cached, GuildBasedChannel, APIInteractionDataResolvedChannel>;
role?: CacheTypeReducer<Cached, Role, APIRole>;
attachment?: Attachment;
message?: GuildCacheMessage<Cached>;
message?: Message<BooleanCache<Cached>>;
}
export interface CommandInteractionResolvedData<Cached extends CacheType = CacheType> {
@@ -4241,7 +4293,7 @@ export interface FetchThreadsOptions {
active?: boolean;
}
export interface FileOptions {
export interface AttachmentPayload {
attachment: BufferResolvable | Stream;
name?: string;
description?: string;
@@ -4368,7 +4420,7 @@ export type GuildBanResolvable = GuildBan | UserResolvable;
export type GuildChannelResolvable = Snowflake | GuildBasedChannel;
export interface GuildChannelCreateOptions extends Omit<CategoryCreateChannelOptions, 'type'> {
parent?: CategoryChannelResolvable;
parent?: CategoryChannelResolvable | null;
type?: Exclude<
ChannelType,
| ChannelType.DM
@@ -4672,10 +4724,17 @@ export type MessageChannelComponentCollectorOptions<T extends MessageComponentIn
>;
export interface MessageEditOptions {
attachments?: Attachment[];
attachments?: JSONEncodable<AttachmentPayload>[];
content?: string | null;
embeds?: (JSONEncodable<APIEmbed> | APIEmbed)[] | null;
files?: (FileOptions | BufferResolvable | Stream | Attachment)[];
files?: (
| BufferResolvable
| Stream
| JSONEncodable<APIAttachment>
| Attachment
| AttachmentBuilder
| AttachmentPayload
)[];
flags?: BitFieldResolvable<MessageFlagsString, number>;
allowedMentions?: MessageMentionOptions;
components?: (
@@ -4726,10 +4785,17 @@ export interface MessageOptions {
| APIActionRowComponent<APIMessageActionRowComponent>
)[];
allowedMentions?: MessageMentionOptions;
files?: (FileOptions | BufferResolvable | Stream | Attachment)[];
files?: (
| BufferResolvable
| Stream
| JSONEncodable<APIAttachment>
| Attachment
| AttachmentBuilder
| AttachmentPayload
)[];
reply?: ReplyOptions;
stickers?: StickerResolvable[];
attachments?: Attachment[];
attachments?: JSONEncodable<AttachmentPayload>[];
flags?: BitFieldResolvable<Extract<MessageFlagsString, 'SuppressEmbeds'>, number>;
}
@@ -4858,7 +4924,7 @@ export interface PartialChannelData {
| ChannelType.GuildStageVoice
>;
name: string;
topic?: string;
topic?: string | null;
nsfw?: boolean;
bitrate?: number;
userLimit?: number;

View File

@@ -1,7 +1,6 @@
import type { ChildProcess } from 'child_process';
import {
APIInteractionGuildMember,
APIMessage,
APIPartialChannel,
APIPartialGuild,
APIInteractionDataResolvedGuildMember,
@@ -57,7 +56,7 @@ import {
Interaction,
InteractionCollector,
Message,
Attachment,
AttachmentBuilder,
MessageCollector,
MessageComponentInteraction,
MessageReaction,
@@ -123,6 +122,7 @@ import {
ChannelMention,
UserMention,
PartialGroupDMChannel,
Attachment,
} from '.';
import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd';
import { UnsafeButtonBuilder, UnsafeEmbedBuilder, UnsafeSelectMenuBuilder } from '@discordjs/builders';
@@ -529,7 +529,7 @@ client.on('guildCreate', async g => {
const channel = g.channels.cache.random();
if (!channel) return;
if (channel.isText()) {
if (channel.type === ChannelType.GuildText) {
const row: ActionRowData<MessageActionRowComponentData> = {
type: ComponentType.ActionRow,
components: [
@@ -615,7 +615,7 @@ client.on('messageCreate', async message => {
assertIsMessage(channel.send({}));
assertIsMessage(channel.send({ embeds: [] }));
const attachment = new Attachment('file.png');
const attachment = new AttachmentBuilder('file.png');
const embed = new EmbedBuilder();
assertIsMessage(channel.send({ files: [attachment] }));
assertIsMessage(channel.send({ embeds: [embed] }));
@@ -1186,20 +1186,20 @@ client.on('interactionCreate', async interaction => {
}
if (interaction.isMessageContextMenuCommand()) {
expectType<Message | APIMessage>(interaction.targetMessage);
expectType<Message>(interaction.targetMessage);
if (interaction.inCachedGuild()) {
expectType<Message<true>>(interaction.targetMessage);
} else if (interaction.inRawGuild()) {
expectType<APIMessage>(interaction.targetMessage);
expectType<Message<false>>(interaction.targetMessage);
} else if (interaction.inGuild()) {
expectType<Message | APIMessage>(interaction.targetMessage);
expectType<Message>(interaction.targetMessage);
}
}
if (interaction.isButton()) {
expectType<ButtonInteraction>(interaction);
expectType<ButtonComponent | APIButtonComponent>(interaction.component);
expectType<Message | APIMessage>(interaction.message);
expectType<Message>(interaction.message);
if (interaction.inCachedGuild()) {
expectAssignable<ButtonInteraction>(interaction);
expectType<ButtonComponent>(interaction.component);
@@ -1209,22 +1209,22 @@ client.on('interactionCreate', async interaction => {
} else if (interaction.inRawGuild()) {
expectAssignable<ButtonInteraction>(interaction);
expectType<APIButtonComponent>(interaction.component);
expectType<APIMessage>(interaction.message);
expectType<Message<false>>(interaction.message);
expectType<null>(interaction.guild);
expectType<Promise<APIMessage>>(interaction.reply({ fetchReply: true }));
expectType<Promise<Message<false>>>(interaction.reply({ fetchReply: true }));
} else if (interaction.inGuild()) {
expectAssignable<ButtonInteraction>(interaction);
expectType<ButtonComponent | APIButtonComponent>(interaction.component);
expectType<Message | APIMessage>(interaction.message);
expectType<Message>(interaction.message);
expectAssignable<Guild | null>(interaction.guild);
expectType<Promise<APIMessage | Message>>(interaction.reply({ fetchReply: true }));
expectType<Promise<Message>>(interaction.reply({ fetchReply: true }));
}
}
if (interaction.isMessageComponent()) {
expectType<MessageComponentInteraction>(interaction);
expectType<MessageActionRowComponent | APIButtonComponent | APISelectMenuComponent>(interaction.component);
expectType<Message | APIMessage>(interaction.message);
expectType<Message>(interaction.message);
if (interaction.inCachedGuild()) {
expectAssignable<MessageComponentInteraction>(interaction);
expectType<MessageActionRowComponent>(interaction.component);
@@ -1234,22 +1234,22 @@ client.on('interactionCreate', async interaction => {
} else if (interaction.inRawGuild()) {
expectAssignable<MessageComponentInteraction>(interaction);
expectType<APIButtonComponent | APISelectMenuComponent>(interaction.component);
expectType<APIMessage>(interaction.message);
expectType<Message<false>>(interaction.message);
expectType<null>(interaction.guild);
expectType<Promise<APIMessage>>(interaction.reply({ fetchReply: true }));
expectType<Promise<Message<false>>>(interaction.reply({ fetchReply: true }));
} else if (interaction.inGuild()) {
expectAssignable<MessageComponentInteraction>(interaction);
expectType<MessageActionRowComponent | APIButtonComponent | APISelectMenuComponent>(interaction.component);
expectType<Message | APIMessage>(interaction.message);
expectType<Message>(interaction.message);
expectType<Guild | null>(interaction.guild);
expectType<Promise<APIMessage | Message>>(interaction.reply({ fetchReply: true }));
expectType<Promise<Message>>(interaction.reply({ fetchReply: true }));
}
}
if (interaction.isSelectMenu()) {
expectType<SelectMenuInteraction>(interaction);
expectType<SelectMenuComponent | APISelectMenuComponent>(interaction.component);
expectType<Message | APIMessage>(interaction.message);
expectType<Message>(interaction.message);
if (interaction.inCachedGuild()) {
expectAssignable<SelectMenuInteraction>(interaction);
expectType<SelectMenuComponent>(interaction.component);
@@ -1259,15 +1259,15 @@ client.on('interactionCreate', async interaction => {
} else if (interaction.inRawGuild()) {
expectAssignable<SelectMenuInteraction>(interaction);
expectType<APISelectMenuComponent>(interaction.component);
expectType<APIMessage>(interaction.message);
expectType<Message<false>>(interaction.message);
expectType<null>(interaction.guild);
expectType<Promise<APIMessage>>(interaction.reply({ fetchReply: true }));
expectType<Promise<Message<false>>>(interaction.reply({ fetchReply: true }));
} else if (interaction.inGuild()) {
expectAssignable<SelectMenuInteraction>(interaction);
expectType<SelectMenuComponent | APISelectMenuComponent>(interaction.component);
expectType<Message | APIMessage>(interaction.message);
expectType<Message>(interaction.message);
expectType<Guild | null>(interaction.guild);
expectType<Promise<Message | APIMessage>>(interaction.reply({ fetchReply: true }));
expectType<Promise<Message>>(interaction.reply({ fetchReply: true }));
}
}
@@ -1275,7 +1275,7 @@ client.on('interactionCreate', async interaction => {
if (interaction.inRawGuild()) {
expectNotAssignable<Interaction<'cached'>>(interaction);
expectAssignable<ChatInputCommandInteraction>(interaction);
expectType<Promise<APIMessage>>(interaction.reply({ fetchReply: true }));
expectType<Promise<Message<false>>>(interaction.reply({ fetchReply: true }));
expectType<APIInteractionDataResolvedGuildMember | null>(interaction.options.getMember('test'));
expectType<APIInteractionDataResolvedChannel>(interaction.options.getChannel('test', true));
@@ -1297,7 +1297,7 @@ client.on('interactionCreate', async interaction => {
// @ts-expect-error
consumeCachedCommand(interaction);
expectType<ChatInputCommandInteraction>(interaction);
expectType<Promise<Message | APIMessage>>(interaction.reply({ fetchReply: true }));
expectType<Promise<Message>>(interaction.reply({ fetchReply: true }));
expectType<APIInteractionDataResolvedGuildMember | GuildMember | null>(interaction.options.getMember('test'));
expectType<GuildBasedChannel | APIInteractionDataResolvedChannel>(interaction.options.getChannel('test', true));

View File

@@ -30,9 +30,13 @@ pnpm add @discordjs/proxy
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
- [Documentation](https://discord.js.org/#/docs/proxy)
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
- [discord.js Discord server](https://discord.gg/djs)
- [Discord API Discord server](https://discord.gg/discord-api)
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/proxy)
- [npm](https://www.npmjs.com/package/@discordjs/proxy)
- [Related libraries](https://discord.com/developers/docs/topics/community-resources#libraries)
## Contributing

View File

@@ -3,6 +3,7 @@ import { REST } from '@discordjs/rest';
import supertest from 'supertest';
import { MockAgent, Interceptable, setGlobalDispatcher } from 'undici';
import type { MockInterceptor } from 'undici/types/mock-interceptor';
import { beforeEach, afterAll, afterEach, test, expect } from 'vitest';
import { proxyRequests } from '../src';
let mockAgent: MockAgent;

View File

@@ -1,17 +0,0 @@
/**
* @type {import('@babel/core').TransformOptions}
*/
module.exports = {
parserOpts: { strictMode: true },
sourceMaps: 'inline',
presets: [
[
'@babel/preset-env',
{
targets: { node: 'current' },
modules: 'commonjs',
},
],
'@babel/preset-typescript',
],
};

View File

@@ -1,11 +0,0 @@
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
module.exports = {
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
testEnvironment: 'node',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'clover'],
};

View File

@@ -4,10 +4,9 @@
"description": "Tools for running an HTTP proxy for Discord's API",
"scripts": {
"build": "tsup && tsc --emitDeclarationOnly --incremental",
"test": "jest --pass-with-no-tests --collect-coverage",
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && node scripts/docs.mjs",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && ts-docgen -i docs/typedoc-out.json -c docs/index.yml -o docs/docs.json",
"prepublishOnly": "yarn build && yarn lint && yarn test",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/proxy/*'"
},
@@ -57,23 +56,15 @@
"undici": "^5.4.0"
},
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/plugin-proposal-decorators": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-typescript": "^7.17.12",
"@discordjs/ts-docgen": "^0.4.1",
"@types/jest": "^28.1.0",
"@discordjs/scripts": "workspace:^",
"@types/node": "^16.11.38",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"babel-plugin-const-enum": "^1.2.0",
"babel-plugin-transform-typescript-metadata": "^0.3.2",
"eslint": "^8.17.0",
"eslint-config-marine": "^9.4.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"jest": "^28.1.0",
"prettier": "^2.6.2",
"supertest": "^6.2.3",
"tsup": "^6.0.1",

View File

@@ -1,7 +0,0 @@
import { runGenerator } from '@discordjs/ts-docgen';
runGenerator({
existingOutput: 'docs/typedoc-out.json',
custom: 'docs/index.yml',
output: 'docs/docs.json',
});

View File

@@ -74,7 +74,9 @@ try {
## Links
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
- [Dev documentation](https://discord.js.org/#/docs/rest/main/general/welcome) (stable coming soon)
- [Documentation](https://discord.js.org/#/docs/rest)
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
- [discord.js Discord server](https://discord.gg/djs)
- [Discord API Discord server](https://discord.gg/discord-api)
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/rest)
@@ -84,7 +86,7 @@ try {
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs/rest/main/general/welcome).
[documentation](https://discord.js.org/#/docs/rest).
See [the contribution guide](https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help

View File

@@ -1,3 +1,4 @@
import { test, expect } from 'vitest';
import { CDN } from '../src';
const base = 'https://discord.com';

View File

@@ -1,3 +1,4 @@
import { test, expect } from 'vitest';
import { DiscordAPIError } from '../src';
test('Unauthorized', () => {

View File

@@ -2,6 +2,7 @@ import { DiscordSnowflake } from '@sapphire/snowflake';
import { Routes, Snowflake } from 'discord-api-types/v10';
import { File, FormData, MockAgent, setGlobalDispatcher } from 'undici';
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor';
import { beforeEach, afterEach, test, expect } from 'vitest';
import { genPath } from './util';
import { REST } from '../src';
@@ -107,7 +108,7 @@ test('simple POST', async () => {
expect(await api.post('/simplePost')).toStrictEqual({ test: true });
});
test('simple PUT', async () => {
test('simple PUT 2', async () => {
mockPool
.intercept({
path: genPath('/simplePut'),
@@ -285,7 +286,7 @@ test('Old Message Delete Edge-Case: Old message', async () => {
});
});
test('Old Message Delete Edge-Case: Old message', async () => {
test('Old Message Delete Edge-Case: Old message 2', async () => {
mockPool
.intercept({
path: genPath(`/channels/339942739275677727/messages/${newSnowflake}`),

View File

@@ -1,6 +1,7 @@
import { performance } from 'node:perf_hooks';
import { MockAgent, setGlobalDispatcher } from 'undici';
import type { Interceptable, MockInterceptor } from 'undici/types/mock-interceptor';
import { beforeEach, afterEach, test, expect, vitest } from 'vitest';
import { genPath } from './util';
import { DiscordAPIError, HTTPError, RateLimitError, REST, RESTEvents } from '../src';
@@ -82,8 +83,8 @@ test('Significant Invalid Requests', async () => {
.reply(403, { message: 'Missing Permissions', code: 50013 }, responseOptions)
.times(10);
const invalidListener = jest.fn();
const invalidListener2 = jest.fn();
const invalidListener = vitest.fn();
const invalidListener2 = vitest.fn();
api.on(RESTEvents.InvalidRequestWarning, invalidListener);
// Ensure listeners on REST do not get double added
api.on(RESTEvents.InvalidRequestWarning, invalidListener2);
@@ -364,6 +365,7 @@ test('Handle unexpected 429', async () => {
expect(await unexepectedSublimit).toStrictEqual({ test: true });
expect(await queuedSublimit).toStrictEqual({ test: true });
expect(performance.now()).toBeGreaterThanOrEqual(previous + 1000);
// @ts-expect-error
expect(secondResolvedTime).toBeGreaterThan(firstResolvedTime);
});
@@ -495,7 +497,7 @@ test('Unauthorized', async () => {
.reply(401, { message: '401: Unauthorized', code: 0 }, responseOptions)
.times(2);
const setTokenSpy = jest.spyOn(invalidAuthApi.requestManager, 'setToken');
const setTokenSpy = vitest.spyOn(invalidAuthApi.requestManager, 'setToken');
// Ensure authless requests don't reset the token
const promiseWithoutTokenClear = invalidAuthApi.get('/unauthorized', { auth: false });

View File

@@ -1,5 +1,6 @@
import { MockAgent, setGlobalDispatcher } from 'undici';
import { Interceptable } from 'undici/types/mock-interceptor';
import type { Interceptable } from 'undici/types/mock-interceptor';
import { beforeEach, afterEach, test, expect } from 'vitest';
import { genPath } from './util';
import { REST } from '../src';

View File

@@ -1,4 +1,5 @@
import { Blob } from 'node:buffer';
import { test, expect } from 'vitest';
import { resolveBody, parseHeader } from '../src/lib/utils/utils';
test('GIVEN string parseHeader returns string', () => {

View File

@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest';
import { makeURLSearchParams } from '../src';
describe('makeURLSearchParams', () => {

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