mirror of
https://github.com/discordjs/discord.js.git
synced 2026-05-23 03:50:09 +00:00
Compare commits
228 Commits
@discordjs
...
@discordjs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fa92ac0f9 | ||
|
|
2c2f88cd43 | ||
|
|
93defeccce | ||
|
|
443533ba99 | ||
|
|
3cc96d7940 | ||
|
|
b94a8761f8 | ||
|
|
9bf2a0d5cb | ||
|
|
9917981f24 | ||
|
|
ab8b946276 | ||
|
|
bcf7f1cfad | ||
|
|
a58556adc0 | ||
|
|
40b9a1d67d | ||
|
|
ab4c608b97 | ||
|
|
1b2d8decb6 | ||
|
|
a674f64e1d | ||
|
|
54e5629986 | ||
|
|
75b6770933 | ||
|
|
c2866504a3 | ||
|
|
f1d0084da2 | ||
|
|
b01f4147d4 | ||
|
|
fc2a8bb675 | ||
|
|
f094e33861 | ||
|
|
446eb390ce | ||
|
|
96a0d83a13 | ||
|
|
01a423d110 | ||
|
|
f0d0242c76 | ||
|
|
b577bcc1df | ||
|
|
0faac04b69 | ||
|
|
9ff54254d8 | ||
|
|
fd1dc72c0a | ||
|
|
29f8807955 | ||
|
|
6f4e97bfaf | ||
|
|
3582fe917d | ||
|
|
78a3afcd7f | ||
|
|
3db20abdd2 | ||
|
|
ebb4dfa262 | ||
|
|
8eaec114a9 | ||
|
|
8625d81714 | ||
|
|
3037fca196 | ||
|
|
e4f27051ca | ||
|
|
25fdb3894d | ||
|
|
a1329bd3eb | ||
|
|
3c0bbac82f | ||
|
|
3f3e4327c8 | ||
|
|
6fec25239d | ||
|
|
aedddb875e | ||
|
|
402514ff32 | ||
|
|
3b3dabf3da | ||
|
|
eb6b472f72 | ||
|
|
f88e1ac4be | ||
|
|
905a6a1166 | ||
|
|
5748dbe087 | ||
|
|
ac4bc3a6c8 | ||
|
|
520f471ac5 | ||
|
|
6708533376 | ||
|
|
9afc03054e | ||
|
|
74bf7d57ab | ||
|
|
8e3b2d7abd | ||
|
|
8880de0cec | ||
|
|
cedd0536ba | ||
|
|
85e531f22d | ||
|
|
07b23a99c7 | ||
|
|
0c32332a5a | ||
|
|
d5369a56e3 | ||
|
|
9a6e691eaa | ||
|
|
4d2b55955d | ||
|
|
cd79bef254 | ||
|
|
c684ac55e1 | ||
|
|
fb9a9c2211 | ||
|
|
daf2829cb5 | ||
|
|
98177aa38d | ||
|
|
b1d63d919a | ||
|
|
b520c3df3c | ||
|
|
e805777a7a | ||
|
|
72577c4bfd | ||
|
|
9b0d8cb2d8 | ||
|
|
8fb98165a9 | ||
|
|
f4729759f6 | ||
|
|
2297c2b947 | ||
|
|
87a6b8445b | ||
|
|
549716e4fc | ||
|
|
230c0c4cb1 | ||
|
|
dcd479767b | ||
|
|
3dff31f63f | ||
|
|
cbdb408dff | ||
|
|
e787cd5fa5 | ||
|
|
b162f27e46 | ||
|
|
b9ff7b0573 | ||
|
|
c12d61a342 | ||
|
|
e71c76c7f7 | ||
|
|
851f380eb1 | ||
|
|
10607dbdaf | ||
|
|
79d6c0489c | ||
|
|
89073903a2 | ||
|
|
8f1986a6aa | ||
|
|
0d7e4edd96 | ||
|
|
fac55bcfd1 | ||
|
|
4b08d9b376 | ||
|
|
93854a8013 | ||
|
|
cb566c8b6a | ||
|
|
6f7a366956 | ||
|
|
ed92015634 | ||
|
|
53defb82e3 | ||
|
|
8478d2f4de | ||
|
|
2d4971b032 | ||
|
|
c6cb5e9ebb | ||
|
|
a8321d8026 | ||
|
|
1a14c0ca56 | ||
|
|
44a57a1b0c | ||
|
|
0aa48516a4 | ||
|
|
83460037be | ||
|
|
8203c5d843 | ||
|
|
51583320d3 | ||
|
|
cf669301c7 | ||
|
|
d1d1b076be | ||
|
|
00728f72b3 | ||
|
|
4f306521d8 | ||
|
|
6a2fa70b8e | ||
|
|
46b53f4365 | ||
|
|
78aa36f9f5 | ||
|
|
3baa340821 | ||
|
|
ba31203a0a | ||
|
|
8dbd34544c | ||
|
|
942ea1acbf | ||
|
|
b3fa2ece40 | ||
|
|
ffecf08495 | ||
|
|
3e105a0bbb | ||
|
|
b12214922c | ||
|
|
71f4fa82ed | ||
|
|
f7257f0765 | ||
|
|
395a68ff49 | ||
|
|
dee27db35a | ||
|
|
d32db8833e | ||
|
|
5cf5071061 | ||
|
|
2d4554440e | ||
|
|
7959a68d8e | ||
|
|
d2bc9d444f | ||
|
|
868e2f3230 | ||
|
|
c1b27f8eed | ||
|
|
9311fa7b42 | ||
|
|
6d3da226d3 | ||
|
|
a8106f7c58 | ||
|
|
32985109c3 | ||
|
|
2d2de1d3fd | ||
|
|
d1bb36256f | ||
|
|
36173590a7 | ||
|
|
003439671d | ||
|
|
0dfdb2cf11 | ||
|
|
ae0f35f51d | ||
|
|
0af9bc841f | ||
|
|
e8252ed3b9 | ||
|
|
3ae6f3c313 | ||
|
|
6ce906a02f | ||
|
|
532846b1f8 | ||
|
|
94bf727cc3 | ||
|
|
f4953647ff | ||
|
|
861f0e2134 | ||
|
|
81d8b54ff6 | ||
|
|
fa97a31504 | ||
|
|
55b388a763 | ||
|
|
fbc71ef6b6 | ||
|
|
b97aedd8e1 | ||
|
|
298b22604b | ||
|
|
fe11ff5f6e | ||
|
|
dd751ae19d | ||
|
|
f59d6305cb | ||
|
|
09098240bf | ||
|
|
cc25455d2c | ||
|
|
3d8c77600b | ||
|
|
83458ff7c7 | ||
|
|
b936103395 | ||
|
|
a921ec7dc5 | ||
|
|
aadfbda586 | ||
|
|
538e9cef45 | ||
|
|
f2a7a9f1b3 | ||
|
|
2800e07e59 | ||
|
|
d8184f94dd | ||
|
|
cd2f566052 | ||
|
|
8bb3751340 | ||
|
|
733ac82d5d | ||
|
|
a7b80b9d9b | ||
|
|
5f4b44d580 | ||
|
|
c15100574b | ||
|
|
1c186fabeb | ||
|
|
6faeddcd0d | ||
|
|
87ca2854c2 | ||
|
|
741452b9be | ||
|
|
37c1cb4495 | ||
|
|
cd5c7fa20e | ||
|
|
6b6222bf51 | ||
|
|
04502ce702 | ||
|
|
51beda56f7 | ||
|
|
92a04f4d98 | ||
|
|
0b866c9fb2 | ||
|
|
4abb28c0a1 | ||
|
|
34120bba97 | ||
|
|
088394367b | ||
|
|
0803665183 | ||
|
|
875c86a4ef | ||
|
|
e6a26d25b3 | ||
|
|
388f53550c | ||
|
|
567db60475 | ||
|
|
fbb1d0328b | ||
|
|
74f627c379 | ||
|
|
3a5ab2c4e5 | ||
|
|
00ce1c56ac | ||
|
|
72767a1059 | ||
|
|
1a6c5ab145 | ||
|
|
355f579771 | ||
|
|
e4bd07b239 | ||
|
|
e1ecc1a80a | ||
|
|
11e5e5ac5b | ||
|
|
ec0fba1ed0 | ||
|
|
ac26d9b130 | ||
|
|
2db0cdd357 | ||
|
|
9a566e8068 | ||
|
|
d6b56d0080 | ||
|
|
b6402723c3 | ||
|
|
706db9228a | ||
|
|
47633f0fd2 | ||
|
|
67250382f9 | ||
|
|
5ccdb0ab26 | ||
|
|
9a1623425a | ||
|
|
c05b38873b | ||
|
|
31768fcd69 | ||
|
|
bcc5cda8a9 | ||
|
|
d2d3a80c55 | ||
|
|
2f16f879aa |
@@ -5,6 +5,7 @@
|
||||
2,
|
||||
"always",
|
||||
["chore", "build", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "types", "typings"]
|
||||
]
|
||||
],
|
||||
"scope-case": [0]
|
||||
}
|
||||
}
|
||||
|
||||
8
.github/COMMIT_CONVENTION.md
vendored
8
.github/COMMIT_CONVENTION.md
vendored
@@ -15,13 +15,13 @@ Messages must be matched by the following regex:
|
||||
Appears under "Features" header, `GuildMember` subheader:
|
||||
|
||||
```
|
||||
feat(guildmember): add 'tag' method
|
||||
feat(GuildMember): add 'tag' method
|
||||
```
|
||||
|
||||
Appears under "Bug Fixes" header, `Guild` subheader, with a link to issue #28:
|
||||
|
||||
```
|
||||
fix(guild): handle events correctly
|
||||
fix(Guild): handle events correctly
|
||||
|
||||
close #28
|
||||
```
|
||||
@@ -37,7 +37,7 @@ BREAKING CHANGE: The 'bar' option has been removed.
|
||||
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
|
||||
|
||||
```
|
||||
revert: feat(managers): add Managers
|
||||
revert: feat(Managers): add Managers
|
||||
|
||||
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
|
||||
```
|
||||
@@ -68,7 +68,7 @@ Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`
|
||||
|
||||
### Scope
|
||||
|
||||
The scope could be anything specifying the place of the commit change. For example `GuildMember`, `Guild`, `Message`, `MessageEmbed` etc...
|
||||
The scope could be anything specifying the place of the commit change. For example `GuildMember`, `Guild`, `Message`, `TextChannel` etc...
|
||||
|
||||
### Subject
|
||||
|
||||
|
||||
9
.github/CONTRIBUTING.md
vendored
9
.github/CONTRIBUTING.md
vendored
@@ -11,7 +11,8 @@ is a great boon to your development process.
|
||||
To get ready to work on the codebase, please do the following:
|
||||
|
||||
1. Fork & clone the repository, and make sure you're on the **main** branch
|
||||
2. Run `npm ci`
|
||||
3. Code your heart out!
|
||||
4. Run `npm test` to run ESLint and ensure any JSDoc changes are valid
|
||||
5. [Submit a pull request](https://github.com/discordjs/discord.js/compare) (Make sure you follow the [conventional commit format](https://github.com/discordjs/discord.js/blob/main/.github/COMMIT_CONVENTION.md))
|
||||
2. Run `yarn --immutable` ([install](https://yarnpkg.com/getting-started/install))
|
||||
3. Run `yarn build` to build local packages
|
||||
4. Code your heart out!
|
||||
5. Run `yarn test` to run ESLint and ensure any JSDoc changes are valid
|
||||
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare) (Make sure you follow the [conventional commit format](https://github.com/discordjs/discord.js/blob/main/.github/COMMIT_CONVENTION.md))
|
||||
|
||||
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -90,12 +90,13 @@ body:
|
||||
options:
|
||||
- Not applicable (subpackage bug)
|
||||
- No Partials
|
||||
- USER
|
||||
- CHANNEL
|
||||
- GUILD_MEMBER
|
||||
- MESSAGE
|
||||
- REACTION
|
||||
- GUILD_SCHEDULED_EVENT
|
||||
- User
|
||||
- Channel
|
||||
- GuildMember
|
||||
- Message
|
||||
- Reaction
|
||||
- GuildScheduledEvent
|
||||
- ThreadMember
|
||||
multiple: true
|
||||
validations:
|
||||
required: true
|
||||
@@ -110,22 +111,22 @@ body:
|
||||
options:
|
||||
- Not applicable (subpackage bug)
|
||||
- No Intents
|
||||
- GUILDS
|
||||
- GUILD_MEMBERS
|
||||
- GUILD_BANS
|
||||
- GUILD_EMOJIS_AND_STICKERS
|
||||
- GUILD_INTEGRATIONS
|
||||
- GUILD_WEBHOOKS
|
||||
- GUILD_INVITES
|
||||
- GUILD_VOICE_STATES
|
||||
- GUILD_PRESENCES
|
||||
- GUILD_MESSAGES
|
||||
- GUILD_MESSAGE_REACTIONS
|
||||
- GUILD_MESSAGE_TYPING
|
||||
- DIRECT_MESSAGES
|
||||
- DIRECT_MESSAGE_REACTIONS
|
||||
- DIRECT_MESSAGE_TYPING
|
||||
- GUILD_SCHEDULED_EVENTS
|
||||
- Guilds
|
||||
- GuildMembers
|
||||
- GuildBans
|
||||
- GuildEmojisAndStickers
|
||||
- GuildIntegrations
|
||||
- GuildWebhooks
|
||||
- GuildInvites
|
||||
- GuildVoiceStates
|
||||
- GuildPresences
|
||||
- GuildMessages
|
||||
- GuildMessageReactions
|
||||
- GuildMessageTyping
|
||||
- DirectMessages
|
||||
- DirectMessageReactions
|
||||
- DirectMessageTyping
|
||||
- GuildScheduledEvents
|
||||
multiple: true
|
||||
validations:
|
||||
required: true
|
||||
|
||||
2
.github/workflows/auto-deprecate.yml
vendored
2
.github/workflows/auto-deprecate.yml
vendored
@@ -23,6 +23,6 @@ jobs:
|
||||
run: yarn --immutable
|
||||
|
||||
- name: Deprecate versions
|
||||
run: 'yarn npm-deprecate --name "*dev*" --package "discord.js"'
|
||||
run: 'yarn npm-deprecate --name "*dev*" --package @discordjs/builders @discordjs/collection discord.js @discordjs/rest @discordjs/voice'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
30
.github/workflows/documentation.yml
vendored
30
.github/workflows/documentation.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- 'stable'
|
||||
- '!docs'
|
||||
tags:
|
||||
- '*'
|
||||
- '**'
|
||||
jobs:
|
||||
build:
|
||||
name: Build documentation
|
||||
@@ -82,7 +82,35 @@ jobs:
|
||||
token: ${{ secrets.DJS_DOCS }}
|
||||
path: 'out'
|
||||
|
||||
- name: 'Extract package from tag'
|
||||
if: env.BRANCH_OR_TAG == 'tag'
|
||||
id: package-name
|
||||
uses: frabert/replace-string-action@v2.0
|
||||
with:
|
||||
pattern: '(^@.*\\/(?<package>.*)@v?)?(?<semver>\d+.\d+.\d+)-?.*'
|
||||
string: ${{ env.BRANCH_NAME }}
|
||||
replace-with: '$<package>'
|
||||
|
||||
- name: 'Extract semver from tag'
|
||||
if: env.BRANCH_OR_TAG == 'tag'
|
||||
id: semver
|
||||
uses: frabert/replace-string-action@v2.0
|
||||
with:
|
||||
pattern: '(^@.*\\/(?<package>.*)@v?)?(?<semver>\d+.\d+.\d+)-?.*'
|
||||
string: ${{ env.BRANCH_NAME }}
|
||||
replace-with: '$<semver>'
|
||||
|
||||
- name: Move docs to correct directory
|
||||
if: env.BRANCH_OR_TAG == 'tag'
|
||||
env:
|
||||
PACKAGE: ${{ steps.package-name.outputs.replaced }}
|
||||
SEMVER: ${{ steps.semver.outputs.replaced }}
|
||||
run: |
|
||||
mkdir -p out/${PACKAGE}
|
||||
mv docs/${PACKAGE}/docs/docs.json out/${PACKAGE}/${SEMVER}.json
|
||||
|
||||
- name: Move docs to correct directory
|
||||
if: env.BRANCH_OR_TAG == 'branch'
|
||||
env:
|
||||
PACKAGE: ${{ matrix.package }}
|
||||
run: |
|
||||
|
||||
24
.github/workflows/publish-dev.yml
vendored
24
.github/workflows/publish-dev.yml
vendored
@@ -6,6 +6,20 @@ on:
|
||||
jobs:
|
||||
npm:
|
||||
name: npm
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- package: '@discordjs/builders'
|
||||
folder: 'builders'
|
||||
- package: '@discordjs/collection'
|
||||
folder: 'collection'
|
||||
- package: 'discord.js'
|
||||
folder: 'discord.js'
|
||||
- package: '@discordjs/rest'
|
||||
folder: 'rest'
|
||||
- package: '@discordjs/voice'
|
||||
folder: 'voice'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'discordjs'
|
||||
steps:
|
||||
@@ -32,7 +46,7 @@ jobs:
|
||||
- name: Check previous released version
|
||||
id: pre-release
|
||||
run: |
|
||||
if [[ $(npm view discord.js@dev version | grep -e "$(jq --raw-output '.version' packages/discord.js/package.json).*.$(git rev-parse --short HEAD | cut -b1-3)") ]]; \
|
||||
if [[ $(npm view ${{ matrix.package }}@dev version | grep -e "$(jq --raw-output '.version' packages/${{ matrix.folder }}/package.json)\..*-$(git rev-parse --short HEAD)") ]]; \
|
||||
then echo '::set-output name=release::false'; \
|
||||
else echo '::set-output name=release::true'; fi
|
||||
|
||||
@@ -46,14 +60,14 @@ jobs:
|
||||
|
||||
- name: Deprecate old versions
|
||||
if: steps.pre-release.outputs.release == 'true'
|
||||
run: npm deprecate discord.js@"~$(jq --raw-output '.version' packages/discord.js/package.json)" "no longer supported" || true
|
||||
run: npm deprecate ${{ matrix.package }}@"~$(jq --raw-output '.version' packages/${{ matrix.folder }}/package.json)" "no longer supported" || true
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
if: steps.pre-release.outputs.release == 'true'
|
||||
run: |
|
||||
yarn workspace discord.js version --no-git-tag-version --new-version $(jq --raw-output '.version' packages/discord.js/package.json).$(date +%s).$(git rev-parse --short HEAD)
|
||||
yarn workspace discord.js publish --tag dev || true
|
||||
yarn workspace ${{ matrix.package }} version $(jq --raw-output '.version' packages/${{ matrix.folder }}/package.json).$(date +%s)-$(git rev-parse --short HEAD)
|
||||
yarn workspace ${{ matrix.package }} npm publish --tag dev || true
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -26,3 +26,12 @@ dist/
|
||||
.DS_Store
|
||||
.turbo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# yarn
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
768
.yarn/releases/yarn-3.1.1.cjs
vendored
Normal file
768
.yarn/releases/yarn-3.1.1.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
11
.yarnrc.yml
Normal file
11
.yarnrc.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
|
||||
spec: "@yarnpkg/plugin-version"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.1.1.cjs
|
||||
@@ -54,7 +54,7 @@ Register a slash command against the Discord API:
|
||||
|
||||
```js
|
||||
const { REST } = require('@discordjs/rest');
|
||||
const { Routes } = require('discord-api-types/v9');
|
||||
const { Routes } = require('discord-api-types/v10');
|
||||
|
||||
const commands = [
|
||||
{
|
||||
@@ -63,7 +63,7 @@ const commands = [
|
||||
},
|
||||
];
|
||||
|
||||
const rest = new REST({ version: '9' }).setToken('token');
|
||||
const rest = new REST({ version: '10' }).setToken('token');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@@ -81,8 +81,8 @@ const rest = new REST({ version: '9' }).setToken('token');
|
||||
Afterwards we can create a quite simple example bot:
|
||||
|
||||
```js
|
||||
const { Client, Intents } = require('discord.js');
|
||||
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
|
||||
const { Client, GatewayIntentBits } = require('discord.js');
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
|
||||
58
package.json
58
package.json
@@ -12,7 +12,7 @@
|
||||
"postinstall": "is-ci || husky install",
|
||||
"docs": "turbo run docs",
|
||||
"changelog": "turbo run changelog",
|
||||
"update": "yarn upgrade-interactive --latest"
|
||||
"update": "yarn upgrade-interactive"
|
||||
},
|
||||
"contributors": [
|
||||
"Crawl <icrawltogo@gmail.com>",
|
||||
@@ -38,13 +38,13 @@
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.1.0",
|
||||
"@commitlint/config-angular": "^16.0.0",
|
||||
"@commitlint/cli": "^16.2.3",
|
||||
"@commitlint/config-angular": "^16.2.3",
|
||||
"@favware/npm-deprecate": "^1.0.4",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"husky": "^7.0.4",
|
||||
"prettier": "^2.5.1",
|
||||
"turbo": "1.0.28"
|
||||
"prettier": "^2.6.2",
|
||||
"turbo": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -52,51 +52,5 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"turbo": {
|
||||
"baseBranch": "origin/main",
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**",
|
||||
"docs/docs.json"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"coverage/**"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": []
|
||||
},
|
||||
"format": {
|
||||
"outputs": []
|
||||
},
|
||||
"docs": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"docs/docs.json"
|
||||
]
|
||||
},
|
||||
"changelog": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"packageManager": "yarn@3.1.1"
|
||||
}
|
||||
|
||||
3
packages/builders/.gitignore
vendored
3
packages/builders/.gitignore
vendored
@@ -17,9 +17,12 @@ pids
|
||||
# Dist
|
||||
dist/
|
||||
typings/
|
||||
|
||||
docs/**/*
|
||||
!docs/index.yml
|
||||
!docs/README.md
|
||||
!docs/examples/
|
||||
!docs/examples/*.md
|
||||
|
||||
# Miscellaneous
|
||||
.tmp/
|
||||
|
||||
@@ -2,6 +2,61 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# [0.13.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.12.0...@discordjs/builders@0.13.0) (2022-04-17)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Validate select menu options (#7566) ([b1d63d9](https://github.com/discordjs/discord.js/commit/b1d63d919a61f309ac89f27016b0f148678dac2b))
|
||||
- **SelectMenu:** Set `placeholder` max to 150 (#7538) ([dcd4797](https://github.com/discordjs/discord.js/commit/dcd479767b6ec980a373f2ea1f22754f41661c1e))
|
||||
- Only check `instanceof Component` once (#7546) ([0aa4851](https://github.com/discordjs/discord.js/commit/0aa48516a4e33497e8e8dc50da164a57cdee09d3))
|
||||
- **builders:** Allow negative min/max value of number/integer option (#7484) ([3baa340](https://github.com/discordjs/discord.js/commit/3baa340821b8ecf8a16253bc0917a1033250d7c9))
|
||||
- **components:** SetX should take rest parameters (#7461) ([3617359](https://github.com/discordjs/discord.js/commit/36173590a712f041b087b7882054805a8bd42dae))
|
||||
- Unsafe embed builder field normalization (#7418) ([b936103](https://github.com/discordjs/discord.js/commit/b936103395121cb21a8c616f669ddab1d2efb0f1))
|
||||
- Fix some typos (#7393) ([92a04f4](https://github.com/discordjs/discord.js/commit/92a04f4d98f6c6760214034cc8f5a1eaa78893c7))
|
||||
- **builders:** Make type optional in constructor (#7391) ([4abb28c](https://github.com/discordjs/discord.js/commit/4abb28c0a1256c57a60369a6b8ec9e98c265b489))
|
||||
- Don't create new instances of builders classes (#7343) ([d6b56d0](https://github.com/discordjs/discord.js/commit/d6b56d0080c4c5f8ace731f1e8bcae0c9d3fb5a5))
|
||||
- **builders:** Dont export `Button` component stuff twice (#7289) ([86d9d06](https://github.com/discordjs/discord.js/commit/86d9d0674347c08d056cd054cb4ce4253195bf94))
|
||||
|
||||
## Documentation
|
||||
|
||||
- Completely fix builders example link (#7543) ([1a14c0c](https://github.com/discordjs/discord.js/commit/1a14c0ca562ea173d363a770a0437209f461fd23))
|
||||
- Add slash command builders example, fixes #7338 (#7339) ([3ae6f3c](https://github.com/discordjs/discord.js/commit/3ae6f3c313091151245d6e6b52337b459ecfc765))
|
||||
- **SlashCommandSubcommands:** Updating old links from Discord developer portal (#7224) ([bd7a6f2](https://github.com/discordjs/discord.js/commit/bd7a6f265212624199fb0b2ddc8ece39759c63de))
|
||||
|
||||
## Features
|
||||
|
||||
- Slash command localization for builders (#7683) ([40b9a1d](https://github.com/discordjs/discord.js/commit/40b9a1d67d0b508ec593e030913acd8161cd17f8))
|
||||
- Add API v10 support (#7477) ([72577c4](https://github.com/discordjs/discord.js/commit/72577c4bfd02524a27afb6ff4aebba9301a690d3))
|
||||
- Add support for module: NodeNext in TS and ESM (#7598) ([8f1986a](https://github.com/discordjs/discord.js/commit/8f1986a6aa98365e09b00e84ad5f9f354ab61f3d))
|
||||
- Add Modals and Text Inputs (#7023) ([ed92015](https://github.com/discordjs/discord.js/commit/ed920156344233241a21b0c0b99736a3a855c23c))
|
||||
- Add missing `v13` component methods (#7466) ([f7257f0](https://github.com/discordjs/discord.js/commit/f7257f07655076eabfe355cb6a53260b39ca9670))
|
||||
- **builders:** Add attachment command option type (#7203) ([ae0f35f](https://github.com/discordjs/discord.js/commit/ae0f35f51d68dfa5a7dc43d161ef9365171debdb))
|
||||
- **components:** Add unsafe message component builders (#7387) ([6b6222b](https://github.com/discordjs/discord.js/commit/6b6222bf513d1ee8cd98fba0ad313def560b864f))
|
||||
- **embed:** Add setFields (#7322) ([bcc5cda](https://github.com/discordjs/discord.js/commit/bcc5cda8a902ddb28c7e3578e0f29b4272832624))
|
||||
- Add components to /builders (#7195) ([2bb40fd](https://github.com/discordjs/discord.js/commit/2bb40fd767cf5918e3ba422ff73082734bfa05b0))
|
||||
|
||||
## Refactor
|
||||
|
||||
- Remove nickname parsing (#7736) ([78a3afc](https://github.com/discordjs/discord.js/commit/78a3afcd7fdac358e06764cc0d675e1215c785f3))
|
||||
- Replace zod with shapeshift (#7547) ([3c0bbac](https://github.com/discordjs/discord.js/commit/3c0bbac82fa9988af4a62ff00c66d149fbe6b921))
|
||||
- Remove store channels (#7634) ([aedddb8](https://github.com/discordjs/discord.js/commit/aedddb875e740e1f1bd77f06ce1b361fd3b7bc36))
|
||||
- Allow builders to accept emoji strings (#7616) ([fb9a9c2](https://github.com/discordjs/discord.js/commit/fb9a9c221121ee1c7986f9c775b77b9691a0ae15))
|
||||
- Don't return builders from API data (#7584) ([549716e](https://github.com/discordjs/discord.js/commit/549716e4fcec89ca81216a6d22aa8e623175e37a))
|
||||
- Remove obsolete builder methods (#7590) ([10607db](https://github.com/discordjs/discord.js/commit/10607dbdafe257c5cbf5b952b7eecec4919e8b4a))
|
||||
- **Embed:** Remove add field (#7522) ([8478d2f](https://github.com/discordjs/discord.js/commit/8478d2f4de9ac013733850cbbc67902f7c5abc55))
|
||||
- Make `data` public in builders (#7486) ([ba31203](https://github.com/discordjs/discord.js/commit/ba31203a0ad96e0a00f8312c397889351e4c5cfd))
|
||||
- **embed:** Remove array support in favor of rest params (#7498) ([b3fa2ec](https://github.com/discordjs/discord.js/commit/b3fa2ece402839008738ad3adce3db958445838d))
|
||||
- **components:** Default set boolean methods to true (#7502) ([b122149](https://github.com/discordjs/discord.js/commit/b12214922cea2f43afbe6b1555a74a3c8e16f798))
|
||||
- Make public builder props getters (#7422) ([e8252ed](https://github.com/discordjs/discord.js/commit/e8252ed3b981a4b7e4013f12efadd2f5d9318d3e))
|
||||
- **builders-methods:** Make methods consistent (#7395) ([f495364](https://github.com/discordjs/discord.js/commit/f4953647ff9f39127978c73bf8a62c08462802ca))
|
||||
- Remove conditional autocomplete option return types (#7396) ([0909824](https://github.com/discordjs/discord.js/commit/09098240bfb13b8afafa4ab549f06d236e0ff1c9))
|
||||
- **embed:** Mark properties as readonly (#7332) ([31768fc](https://github.com/discordjs/discord.js/commit/31768fcd69ed5b4566a340bda89ce881418e8272))
|
||||
|
||||
## Typings
|
||||
|
||||
- Fix regressions (#7649) ([5748dbe](https://github.com/discordjs/discord.js/commit/5748dbe08783beb80c526de38ccd105eb0e82664))
|
||||
- Make `required` a boolean (#7307) ([c10afea](https://github.com/discordjs/discord.js/commit/c10afeadc702ab98bec5e077b3b92494a9596f9c))
|
||||
|
||||
# [0.12.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.11.0...@discordjs/builders@0.12.0) (2021-12-08)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
@@ -27,7 +27,7 @@ pnpm add @discordjs/builders
|
||||
|
||||
Here are some examples for the builders and utilities you can find in this package:
|
||||
|
||||
- [Slash Command Builders](./docs/examples/Slash%20Command%20Builders.md)
|
||||
- [Slash Command Builders](https://github.com/discordjs/discord.js/blob/main/packages/builders/docs/examples/Slash%20Command%20Builders.md)
|
||||
|
||||
## Links
|
||||
|
||||
|
||||
@@ -1,15 +1,55 @@
|
||||
import { APIActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9';
|
||||
import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src';
|
||||
import { APIActionRowComponent, APIMessageActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
createComponentBuilder,
|
||||
SelectMenuBuilder,
|
||||
SelectMenuOptionBuilder,
|
||||
} from '../../src';
|
||||
|
||||
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
custom_id: '123',
|
||||
style: ButtonStyle.Primary,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.SelectMenu,
|
||||
custom_id: '1234',
|
||||
options: [
|
||||
{
|
||||
label: 'one',
|
||||
value: 'one',
|
||||
},
|
||||
{
|
||||
label: 'two',
|
||||
value: 'two',
|
||||
},
|
||||
],
|
||||
max_values: 10,
|
||||
min_values: 12,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Action Row Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid components THEN do not throw', () => {
|
||||
expect(() => new ActionRow().addComponents(new ButtonComponent())).not.toThrowError();
|
||||
expect(() => new ActionRow().setComponents([new ButtonComponent()])).not.toThrowError();
|
||||
expect(() => new ActionRowBuilder().addComponents(new ButtonBuilder())).not.toThrowError();
|
||||
expect(() => new ActionRowBuilder().setComponents(new ButtonBuilder())).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const actionRowData: APIActionRowComponent = {
|
||||
const actionRowData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -38,14 +78,12 @@ describe('Action Row Components', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(new ActionRow(actionRowData).toJSON()).toEqual(actionRowData);
|
||||
expect(new ActionRow().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
|
||||
expect(() => createComponent({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => createComponent({ type: 42, components: [] })).toThrowError();
|
||||
expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData);
|
||||
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
|
||||
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
||||
});
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const rowWithButtonData: APIActionRowComponent = {
|
||||
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -57,7 +95,7 @@ describe('Action Row Components', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const rowWithSelectMenuData: APIActionRowComponent = {
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -79,18 +117,24 @@ describe('Action Row Components', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||
const selectMenu = new SelectMenuComponent()
|
||||
expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData);
|
||||
expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData);
|
||||
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
|
||||
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
||||
});
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||
const selectMenu = new SelectMenuBuilder()
|
||||
.setCustomId('1234')
|
||||
.setMaxValues(10)
|
||||
.setMinValues(12)
|
||||
.setOptions([
|
||||
new SelectMenuOption().setLabel('one').setValue('one'),
|
||||
new SelectMenuOption().setLabel('two').setValue('two'),
|
||||
]);
|
||||
.setOptions(
|
||||
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
|
||||
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
||||
);
|
||||
|
||||
expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData);
|
||||
expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
|
||||
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);
|
||||
expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
APIButtonComponentWithURL,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
} from 'discord-api-types/v9';
|
||||
} from 'discord-api-types/v10';
|
||||
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions';
|
||||
import { ButtonComponent } from '../../src/components/Button';
|
||||
import { ButtonBuilder } from '../../src/components/button/Button';
|
||||
|
||||
const buttonComponent = () => new ButtonComponent();
|
||||
const buttonComponent = () => new ButtonBuilder();
|
||||
|
||||
const longStr =
|
||||
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
||||
@@ -119,7 +119,7 @@ describe('Button Components', () => {
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
expect(new ButtonComponent(interactionData).toJSON()).toEqual(interactionData);
|
||||
expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
|
||||
|
||||
expect(
|
||||
buttonComponent()
|
||||
@@ -138,7 +138,7 @@ describe('Button Components', () => {
|
||||
url: 'https://google.com',
|
||||
};
|
||||
|
||||
expect(new ButtonComponent(linkData).toJSON()).toEqual(linkData);
|
||||
expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData);
|
||||
|
||||
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
|
||||
});
|
||||
|
||||
@@ -1,21 +1,42 @@
|
||||
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v9';
|
||||
import { SelectMenuComponent, SelectMenuOption } from '../../src/index';
|
||||
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v10';
|
||||
import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index';
|
||||
|
||||
const selectMenu = () => new SelectMenuComponent();
|
||||
const selectMenuOption = () => new SelectMenuOption();
|
||||
const selectMenu = () => new SelectMenuBuilder();
|
||||
const selectMenuOption = () => new SelectMenuOptionBuilder();
|
||||
|
||||
const longStr =
|
||||
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
||||
const longStr = 'a'.repeat(256);
|
||||
|
||||
describe('Button Components', () => {
|
||||
const selectMenuOptionData: APISelectMenuOption = {
|
||||
label: 'test',
|
||||
value: 'test',
|
||||
emoji: { name: 'test' },
|
||||
default: true,
|
||||
description: 'test',
|
||||
};
|
||||
|
||||
const selectMenuDataWithoutOptions = {
|
||||
type: ComponentType.SelectMenu,
|
||||
custom_id: 'test',
|
||||
max_values: 10,
|
||||
min_values: 3,
|
||||
disabled: true,
|
||||
placeholder: 'test',
|
||||
} as const;
|
||||
|
||||
const selectMenuData: APISelectMenuComponent = {
|
||||
...selectMenuDataWithoutOptions,
|
||||
options: [selectMenuOptionData],
|
||||
};
|
||||
|
||||
describe('Select Menu Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid inputs THEN Select Menu does not throw', () => {
|
||||
expect(() => selectMenu().setCustomId('foo')).not.toThrowError();
|
||||
expect(() => selectMenu().setMaxValues(10)).not.toThrowError();
|
||||
expect(() => selectMenu().setMinValues(3)).not.toThrowError();
|
||||
expect(() => selectMenu().setDisabled(true)).not.toThrowError();
|
||||
expect(() => selectMenu().setDisabled()).not.toThrowError();
|
||||
expect(() => selectMenu().setPlaceholder('description')).not.toThrowError();
|
||||
|
||||
const option = selectMenuOption()
|
||||
.setLabel('test')
|
||||
.setValue('test')
|
||||
@@ -23,7 +44,29 @@ describe('Button Components', () => {
|
||||
.setEmoji({ name: 'test' })
|
||||
.setDescription('description');
|
||||
expect(() => selectMenu().addOptions(option)).not.toThrowError();
|
||||
expect(() => selectMenu().setOptions([option])).not.toThrowError();
|
||||
expect(() => selectMenu().setOptions(option)).not.toThrowError();
|
||||
expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError();
|
||||
expect(() =>
|
||||
selectMenu().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({ label: 'test', value: 'test' })
|
||||
.addOptions(...new Array<APISelectMenuOption>(24).fill({ label: 'test', value: 'test' })),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
|
||||
@@ -33,6 +76,26 @@ describe('Button Components', () => {
|
||||
// @ts-expect-error
|
||||
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();
|
||||
|
||||
const tooManyOptions = new Array<APISelectMenuOption>(26).fill({ label: 'test', value: 'test' });
|
||||
expect(() => selectMenu().setOptions(...tooManyOptions)).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
selectMenu()
|
||||
.addOptions({ label: 'test', value: 'test' })
|
||||
.addOptions(...tooManyOptions),
|
||||
).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
selectMenuOption()
|
||||
@@ -47,26 +110,12 @@ describe('Button Components', () => {
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
|
||||
const selectMenuOptionData: APISelectMenuOption = {
|
||||
label: 'test',
|
||||
value: 'test',
|
||||
emoji: { name: 'test' },
|
||||
default: true,
|
||||
description: 'test',
|
||||
};
|
||||
|
||||
const selectMenuData: APISelectMenuComponent = {
|
||||
type: ComponentType.SelectMenu,
|
||||
custom_id: 'test',
|
||||
max_values: 10,
|
||||
min_values: 3,
|
||||
disabled: true,
|
||||
options: [selectMenuOptionData],
|
||||
placeholder: 'test',
|
||||
};
|
||||
|
||||
expect(new SelectMenuComponent(selectMenuData).toJSON()).toEqual(selectMenuData);
|
||||
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
|
||||
expect(
|
||||
new SelectMenuBuilder(selectMenuDataWithoutOptions)
|
||||
.addOptions(new SelectMenuOptionBuilder(selectMenuOptionData))
|
||||
.toJSON(),
|
||||
).toEqual(selectMenuData);
|
||||
expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
126
packages/builders/__tests__/components/textInput.test.ts
Normal file
126
packages/builders/__tests__/components/textInput.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { APITextInputComponent, ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import {
|
||||
labelValidator,
|
||||
maxLengthValidator,
|
||||
minLengthValidator,
|
||||
placeholderValidator,
|
||||
valueValidator,
|
||||
textInputStyleValidator,
|
||||
} from '../../src/components/textInput/Assertions';
|
||||
import { TextInputBuilder } from '../../src/components/textInput/TextInput';
|
||||
|
||||
const superLongStr = 'a'.repeat(5000);
|
||||
|
||||
const textInputComponent = () => new TextInputBuilder();
|
||||
|
||||
describe('Text Input Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid label THEN validator does not throw', () => {
|
||||
expect(() => labelValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid label THEN validator does throw', () => {
|
||||
expect(() => labelValidator.parse(24)).toThrowError();
|
||||
expect(() => labelValidator.parse(undefined)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid style THEN validator does not throw', () => {
|
||||
expect(() => textInputStyleValidator.parse(TextInputStyle.Paragraph)).not.toThrowError();
|
||||
expect(() => textInputStyleValidator.parse(TextInputStyle.Short)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid style THEN validator does throw', () => {
|
||||
expect(() => textInputStyleValidator.parse(24)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid min length THEN validator does not throw', () => {
|
||||
expect(() => minLengthValidator.parse(10)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid min length THEN validator does throw', () => {
|
||||
expect(() => minLengthValidator.parse(-1)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid max length THEN validator does not throw', () => {
|
||||
expect(() => maxLengthValidator.parse(10)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid min length THEN validator does throw', () => {
|
||||
expect(() => maxLengthValidator.parse(4001)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid value THEN validator does not throw', () => {
|
||||
expect(() => valueValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid value THEN validator does throw', () => {
|
||||
expect(() => valueValidator.parse(superLongStr)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid placeholder THEN validator does not throw', () => {
|
||||
expect(() => placeholderValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid value THEN validator does throw', () => {
|
||||
expect(() => placeholderValidator.parse(superLongStr)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||
expect(() => {
|
||||
textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON();
|
||||
}).not.toThrowError();
|
||||
|
||||
expect(() => {
|
||||
textInputComponent()
|
||||
.setCustomId('foobar')
|
||||
.setLabel('test')
|
||||
.setMaxLength(100)
|
||||
.setMinLength(1)
|
||||
.setPlaceholder('bar')
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.toJSON();
|
||||
}).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN invalid fields THEN builder throws', () => {
|
||||
expect(() => textInputComponent().toJSON()).toThrowError();
|
||||
expect(() => {
|
||||
textInputComponent()
|
||||
.setCustomId('test')
|
||||
.setMaxLength(100)
|
||||
.setPlaceholder('hello')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.toJSON();
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const textInputData: APITextInputComponent = {
|
||||
type: ComponentType.TextInput,
|
||||
label: 'label',
|
||||
custom_id: 'custom id',
|
||||
placeholder: 'placeholder',
|
||||
max_length: 100,
|
||||
min_length: 10,
|
||||
value: 'value',
|
||||
required: false,
|
||||
style: TextInputStyle.Paragraph,
|
||||
};
|
||||
|
||||
expect(new TextInputBuilder(textInputData).toJSON()).toEqual(textInputData);
|
||||
expect(
|
||||
textInputComponent()
|
||||
.setCustomId(textInputData.custom_id)
|
||||
.setLabel(textInputData.label)
|
||||
.setPlaceholder(textInputData.placeholder)
|
||||
.setMaxLength(textInputData.max_length)
|
||||
.setMinLength(textInputData.min_length)
|
||||
.setValue(textInputData.value)
|
||||
.setRequired(textInputData.required)
|
||||
.setStyle(textInputData.style)
|
||||
.toJSON(),
|
||||
).toEqual(textInputData);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
APIApplicationCommandUserOption,
|
||||
ApplicationCommandOptionType,
|
||||
ChannelType,
|
||||
} from 'discord-api-types/v9';
|
||||
} from 'discord-api-types/v10';
|
||||
import {
|
||||
SlashCommandBooleanOption,
|
||||
SlashCommandChannelOption,
|
||||
@@ -29,7 +29,7 @@ const getChannelOption = () =>
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.addChannelType(ChannelType.GuildText);
|
||||
.addChannelTypes(ChannelType.GuildText);
|
||||
|
||||
const getStringOption = () =>
|
||||
new SlashCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
@@ -39,7 +39,7 @@ const getIntegerOption = () =>
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMinValue(-1)
|
||||
.setMaxValue(10);
|
||||
|
||||
const getNumberOption = () =>
|
||||
@@ -47,7 +47,7 @@ const getNumberOption = () =>
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMinValue(-1.23)
|
||||
.setMaxValue(10);
|
||||
|
||||
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
@@ -84,30 +84,30 @@ describe('Application Command toJSON() results', () => {
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
min_value: -1,
|
||||
});
|
||||
|
||||
expect(getIntegerOption().setAutocomplete(true).setChoices().toJSON()).toEqual<APIApplicationCommandIntegerOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: -1,
|
||||
autocomplete: true,
|
||||
// @ts-expect-error TODO: you *can* send an empty array with autocomplete: true, should correct that in types
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(
|
||||
getIntegerOption().setAutocomplete(true).setChoices([]).toJSON(),
|
||||
getIntegerOption().addChoices({ name: 'uwu', value: 1 }).toJSON(),
|
||||
).toEqual<APIApplicationCommandIntegerOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
autocomplete: true,
|
||||
// @ts-expect-error TODO: you *can* send an empty array with autocomplete: true, should correct that in types
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(getIntegerOption().addChoice('uwu', 1).toJSON()).toEqual<APIApplicationCommandIntegerOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
min_value: -1,
|
||||
choices: [{ name: 'uwu', value: 1 }],
|
||||
});
|
||||
});
|
||||
@@ -128,30 +128,32 @@ describe('Application Command toJSON() results', () => {
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
min_value: -1.23,
|
||||
});
|
||||
|
||||
expect(getNumberOption().setAutocomplete(true).setChoices([]).toJSON()).toEqual<APIApplicationCommandNumberOption>({
|
||||
expect(getNumberOption().setAutocomplete(true).setChoices().toJSON()).toEqual<APIApplicationCommandNumberOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
min_value: -1.23,
|
||||
autocomplete: true,
|
||||
// @ts-expect-error TODO: you *can* send an empty array with autocomplete: true, should correct that in types
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(getNumberOption().addChoice('uwu', 1).toJSON()).toEqual<APIApplicationCommandNumberOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
choices: [{ name: 'uwu', value: 1 }],
|
||||
});
|
||||
expect(getNumberOption().addChoices({ name: 'uwu', value: 1 }).toJSON()).toEqual<APIApplicationCommandNumberOption>(
|
||||
{
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: -1.23,
|
||||
choices: [{ name: 'uwu', value: 1 }],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN a role option THEN calling toJSON should return a valid JSON', () => {
|
||||
@@ -171,7 +173,7 @@ describe('Application Command toJSON() results', () => {
|
||||
required: true,
|
||||
});
|
||||
|
||||
expect(getStringOption().setAutocomplete(true).setChoices([]).toJSON()).toEqual<APIApplicationCommandStringOption>({
|
||||
expect(getStringOption().setAutocomplete(true).setChoices().toJSON()).toEqual<APIApplicationCommandStringOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
@@ -181,7 +183,9 @@ describe('Application Command toJSON() results', () => {
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(getStringOption().addChoice('uwu', '1').toJSON()).toEqual<APIApplicationCommandStringOption>({
|
||||
expect(
|
||||
getStringOption().addChoices({ name: 'uwu', value: '1' }).toJSON(),
|
||||
).toEqual<APIApplicationCommandStringOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandOptionChoice, ChannelType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandOptionChoice, ChannelType } from 'discord-api-types/v10';
|
||||
import {
|
||||
SlashCommandAssertions,
|
||||
SlashCommandBooleanOption,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SlashCommandMentionableOption,
|
||||
SlashCommandNumberOption,
|
||||
SlashCommandRoleOption,
|
||||
SlashCommandAttachmentOption,
|
||||
SlashCommandStringOption,
|
||||
SlashCommandSubcommandBuilder,
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
@@ -25,6 +26,7 @@ const getBooleanOption = () => new SlashCommandBooleanOption().setName('owo').se
|
||||
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123');
|
||||
const getChannelOption = () => new SlashCommandChannelOption().setName('owo').setDescription('Testing 123');
|
||||
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123');
|
||||
const getAttachmentOption = () => new SlashCommandAttachmentOption().setName('owo').setDescription('Testing 123');
|
||||
const getMentionableOption = () => new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123');
|
||||
const getSubcommandGroup = () => new SlashCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123');
|
||||
const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
|
||||
@@ -85,18 +87,16 @@ describe('Slash Commands', () => {
|
||||
test('GIVEN valid array of options or choices THEN does not throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength([])).not.toThrowError();
|
||||
|
||||
expect(() => SlashCommandAssertions.validateMaxChoicesLength([])).not.toThrowError();
|
||||
expect(() => SlashCommandAssertions.validateChoicesLength(25, [])).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid options or choices THEN throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength(null)).toThrowError();
|
||||
|
||||
expect(() => SlashCommandAssertions.validateMaxChoicesLength(null)).toThrowError();
|
||||
|
||||
// Given an array that's too big
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength(largeArray)).toThrowError();
|
||||
|
||||
expect(() => SlashCommandAssertions.validateMaxChoicesLength(largeArray)).toThrowError();
|
||||
expect(() => SlashCommandAssertions.validateChoicesLength(1, largeArray)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid required parameters THEN does not throw error', () => {
|
||||
@@ -138,23 +138,23 @@ describe('Slash Commands', () => {
|
||||
integer
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.addChoices([['Very cool', 1_000]]),
|
||||
.addChoices({ name: 'Very cool', value: 1_000 }),
|
||||
)
|
||||
.addNumberOption((number) =>
|
||||
number
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.addChoices([['Very cool', 1.5]]),
|
||||
.addChoices({ name: 'Very cool', value: 1.5 }),
|
||||
)
|
||||
.addStringOption((string) =>
|
||||
string
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.addChoices([
|
||||
['Fancy Pants', 'fp_1'],
|
||||
['Fancy Shoes', 'fs_1'],
|
||||
['The Whole shebang', 'all'],
|
||||
]),
|
||||
.addChoices(
|
||||
{ name: 'Fancy Pants', value: 'fp_1' },
|
||||
{ name: 'Fancy Shoes', value: 'fs_1' },
|
||||
{ name: 'The Whole shebang', value: 'all' },
|
||||
),
|
||||
)
|
||||
.addIntegerOption((integer) =>
|
||||
integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
||||
@@ -177,31 +177,25 @@ describe('Slash Commands', () => {
|
||||
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
// @ts-expect-error Checking if check works JS-side too
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||
getStringOption().setAutocomplete(true).addChoice('Fancy Pants', 'fp_1'),
|
||||
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||
getStringOption()
|
||||
.setAutocomplete(true)
|
||||
// @ts-expect-error Checking if check works JS-side too
|
||||
.addChoices([
|
||||
['Fancy Pants', 'fp_1'],
|
||||
['Fancy Shoes', 'fs_1'],
|
||||
['The Whole shebang', 'all'],
|
||||
]),
|
||||
.addChoices(
|
||||
{ name: 'Fancy Pants', value: 'fp_1' },
|
||||
{ name: 'Fancy Shoes', value: 'fs_1' },
|
||||
{ name: 'The Whole shebang', value: 'all' },
|
||||
),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
// @ts-expect-error Checking if check works JS-side too
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||
getStringOption().addChoice('Fancy Pants', 'fp_1').setAutocomplete(true),
|
||||
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
@@ -229,20 +223,20 @@ describe('Slash Commands', () => {
|
||||
|
||||
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addChannelOption(getChannelOption().addChannelType(ChannelType.GuildText)),
|
||||
getBuilder().addChannelOption(getChannelOption().addChannelTypes(ChannelType.GuildText)),
|
||||
).not.toThrowError();
|
||||
|
||||
expect(() => {
|
||||
getBuilder().addChannelOption(
|
||||
getChannelOption().addChannelTypes([ChannelType.GuildNews, ChannelType.GuildText]),
|
||||
getChannelOption().addChannelTypes(ChannelType.GuildNews, ChannelType.GuildText),
|
||||
);
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => {
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(100))).toThrowError();
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100))).toThrowError();
|
||||
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes([100, 200]))).toThrowError();
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100, 200))).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
|
||||
@@ -286,6 +280,8 @@ describe('Slash Commands', () => {
|
||||
|
||||
expect(() => getBuilder().addRoleOption(getRoleOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addAttachmentOption(getAttachmentOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addMentionableOption(getMentionableOption())).not.toThrowError();
|
||||
});
|
||||
|
||||
@@ -331,24 +327,24 @@ describe('Slash Commands', () => {
|
||||
expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an option that is autocompletable and has choices, THEN setting choices to an empty array should not throw an error', () => {
|
||||
test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices([])),
|
||||
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices()),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
getStringOption()
|
||||
.setAutocomplete(true)
|
||||
.setChoices([['owo', 'uwu']]),
|
||||
getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }),
|
||||
),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an option, THEN setting choices should not throw an error', () => {
|
||||
expect(() => getBuilder().addStringOption(getStringOption().setChoices([['owo', 'uwu']]))).not.toThrowError();
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(getStringOption().setChoices({ name: 'owo', value: 'uwu' })),
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -424,5 +420,61 @@ describe('Slash Commands', () => {
|
||||
expect(() => getSubcommand().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slash command localizations', () => {
|
||||
const expectedSingleLocale = { 'en-US': 'foobar' };
|
||||
const expectedMultipleLocales = {
|
||||
...expectedSingleLocale,
|
||||
bg: 'test',
|
||||
};
|
||||
|
||||
test('GIVEN valid name localizations THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
|
||||
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid name localizations THEN does not throw error', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid name localizations THEN valid data is stored', () => {
|
||||
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
|
||||
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
|
||||
expectedMultipleLocales,
|
||||
);
|
||||
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
|
||||
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
|
||||
'en-US': null,
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN valid description localizations THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setDescriptionLocalization('en-US', 'foobar')).not.toThrowError();
|
||||
expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid description localizations THEN does not throw error', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' })).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid description localizations THEN valid data is stored', () => {
|
||||
expect(getBuilder().setDescriptionLocalization('en-US', 'foobar').description_localizations).toEqual(
|
||||
expectedSingleLocale,
|
||||
);
|
||||
expect(
|
||||
getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).description_localizations,
|
||||
).toEqual(expectedMultipleLocales);
|
||||
expect(getBuilder().setDescriptionLocalizations(null).description_localizations).toBeNull();
|
||||
expect(getBuilder().setDescriptionLocalization('en-US', null).description_localizations).toEqual({
|
||||
'en-US': null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
98
packages/builders/__tests__/interactions/modal.test.ts
Normal file
98
packages/builders/__tests__/interactions/modal.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { APIModalInteractionResponseCallbackData, ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ModalBuilder,
|
||||
ModalActionRowComponentBuilder,
|
||||
TextInputBuilder,
|
||||
} from '../../src';
|
||||
import {
|
||||
componentsValidator,
|
||||
titleValidator,
|
||||
validateRequiredParameters,
|
||||
} from '../../src/interactions/modals/Assertions';
|
||||
|
||||
const modal = () => new ModalBuilder();
|
||||
|
||||
describe('Modals', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid title THEN validator does not throw', () => {
|
||||
expect(() => titleValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid title THEN validator does throw', () => {
|
||||
expect(() => titleValidator.parse(42)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid components THEN validator does not throw', () => {
|
||||
expect(() => componentsValidator.parse([new ActionRowBuilder(), new ActionRowBuilder()])).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid components THEN validator does throw', () => {
|
||||
expect(() => componentsValidator.parse([new ButtonBuilder(), new TextInputBuilder()])).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid required parameters THEN validator does not throw', () => {
|
||||
expect(() =>
|
||||
validateRequiredParameters('123', 'title', [new ActionRowBuilder(), new ActionRowBuilder()]),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid required parameters THEN validator does throw', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]),
|
||||
).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||
expect(() =>
|
||||
modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRowBuilder()),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid fields THEN builder does throw', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
modal().setTitle('test').setCustomId('foobar').setComponents([new ActionRowBuilder()]).toJSON(),
|
||||
).toThrowError();
|
||||
expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const modalData: APIModalInteractionResponseCallbackData = {
|
||||
title: 'title',
|
||||
custom_id: 'custom id',
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextInput,
|
||||
label: 'label',
|
||||
style: TextInputStyle.Paragraph,
|
||||
custom_id: 'custom id',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(new ModalBuilder(modalData).toJSON()).toEqual(modalData);
|
||||
|
||||
expect(
|
||||
modal()
|
||||
.setTitle(modalData.title)
|
||||
.setCustomId('custom id')
|
||||
.setComponents(
|
||||
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
|
||||
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
|
||||
),
|
||||
)
|
||||
.toJSON(),
|
||||
).toEqual(modalData);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,11 @@
|
||||
import { Embed } from '../../src';
|
||||
import type { APIEmbed } from 'discord-api-types/v9';
|
||||
|
||||
const emptyEmbed: APIEmbed = {
|
||||
author: undefined,
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
fields: [],
|
||||
footer: undefined,
|
||||
image: undefined,
|
||||
provider: undefined,
|
||||
thumbnail: undefined,
|
||||
title: undefined,
|
||||
url: undefined,
|
||||
video: undefined,
|
||||
};
|
||||
import { EmbedBuilder, embedLength } from '../../src';
|
||||
|
||||
const alpha = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
describe('Embed', () => {
|
||||
describe('Embed getters', () => {
|
||||
test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => {
|
||||
const embed = new Embed({
|
||||
const embed = new EmbedBuilder({
|
||||
title: alpha,
|
||||
description: alpha,
|
||||
fields: [{ name: alpha, value: alpha }],
|
||||
@@ -28,38 +13,38 @@ describe('Embed', () => {
|
||||
footer: { text: alpha },
|
||||
});
|
||||
|
||||
expect(embed.length).toBe(alpha.length * 6);
|
||||
expect(embedLength(embed.data)).toBe(alpha.length * 6);
|
||||
});
|
||||
|
||||
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(embed.length).toBe(0);
|
||||
expect(embedLength(embed.data)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed title', () => {
|
||||
test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ title: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: 'foo' });
|
||||
const embed = new EmbedBuilder({ title: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTitle('foo');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ title: 'foo' });
|
||||
const embed = new EmbedBuilder({ title: 'foo' });
|
||||
embed.setTitle(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ title: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid title THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setTitle('a'.repeat(257))).toThrowError();
|
||||
});
|
||||
@@ -67,26 +52,26 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed description', () => {
|
||||
test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ ...emptyEmbed, description: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: 'foo' });
|
||||
const embed = new EmbedBuilder({ description: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setDescription('foo');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ description: 'foo' });
|
||||
const embed = new EmbedBuilder({ description: 'foo' });
|
||||
embed.setDescription(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ description: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid description THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setDescription('a'.repeat(4097))).toThrowError();
|
||||
});
|
||||
@@ -94,62 +79,61 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed URL', () => {
|
||||
test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ url: 'https://discord.js.org/' });
|
||||
const embed = new EmbedBuilder({ url: 'https://discord.js.org/' });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
url: 'https://discord.js.org/',
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setURL('https://discord.js.org/');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
url: 'https://discord.js.org/',
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ url: 'https://discord.js.org' });
|
||||
const embed = new EmbedBuilder({ url: 'https://discord.js.org' });
|
||||
embed.setURL(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ url: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid URL THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setURL('owo')).toThrowError();
|
||||
expect(() => embed.setURL(input)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Color', () => {
|
||||
test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ color: 0xff0000 });
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: 0xff0000 });
|
||||
const embed = new EmbedBuilder({ color: 0xff0000 });
|
||||
expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
embed.setColor(0xff0000);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: 0xff0000 });
|
||||
expect(new EmbedBuilder().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
|
||||
expect(new EmbedBuilder().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ color: 0xff0000 });
|
||||
const embed = new EmbedBuilder({ color: 0xff0000 });
|
||||
embed.setColor(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ color: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid color THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => embed.setColor('RED')).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => embed.setColor([42, 36])).toThrowError();
|
||||
expect(() => embed.setColor([42, 36, 1000])).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,67 +141,65 @@ describe('Embed', () => {
|
||||
const now = new Date();
|
||||
|
||||
test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ timestamp: now.toISOString() });
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
|
||||
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
||||
});
|
||||
|
||||
test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTimestamp(now);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTimestamp(now.getTime());
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTimestamp();
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: embed.timestamp });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: embed.data.timestamp });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ timestamp: now.toISOString() });
|
||||
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
|
||||
embed.setTimestamp(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: undefined });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Thumbnail', () => {
|
||||
test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setThumbnail('https://discord.js.org/static/logo.svg');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
embed.setThumbnail(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setThumbnail('owo')).toThrowError();
|
||||
});
|
||||
@@ -225,32 +207,30 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed Image', () => {
|
||||
test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ image: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
image: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setImage('https://discord.js.org/static/logo.svg');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
image: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ image: { url: 'https://discord.js/org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } });
|
||||
embed.setImage(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ image: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid image THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setImage('owo')).toThrowError();
|
||||
});
|
||||
@@ -258,17 +238,16 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed Author', () => {
|
||||
test('GIVEN an embed with a pre-defined author THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({
|
||||
const embed = new EmbedBuilder({
|
||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||
});
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setAuthor({
|
||||
name: 'Wumpus',
|
||||
iconURL: 'https://discord.js.org/static/logo.svg',
|
||||
@@ -276,22 +255,21 @@ describe('Embed', () => {
|
||||
});
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({
|
||||
const embed = new EmbedBuilder({
|
||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||
});
|
||||
embed.setAuthor(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ author: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid author name THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError();
|
||||
});
|
||||
@@ -299,34 +277,34 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed Footer', () => {
|
||||
test('GIVEN an embed with a pre-defined footer THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({
|
||||
const embed = new EmbedBuilder({
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' });
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
embed.setFooter(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ footer: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with invalid footer text THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setFooter({ text: 'a'.repeat(2049) })).toThrowError();
|
||||
});
|
||||
@@ -334,47 +312,34 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed Fields', () => {
|
||||
test('GIVEN an embed with a pre-defined field THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
const embed = new EmbedBuilder({
|
||||
fields: [{ name: 'foo', value: 'bar' }],
|
||||
});
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#addField THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
embed.addField({ name: 'foo', value: 'bar' });
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
fields: [{ name: 'foo', value: 'bar' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields({ name: 'foo', value: 'bar' });
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
|
||||
|
||||
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
fields: [{ name: 'foo', value: 'baz', inline: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
||||
|
||||
expect(() =>
|
||||
@@ -383,7 +348,7 @@ describe('Embed', () => {
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
||||
|
||||
expect(() =>
|
||||
@@ -391,9 +356,25 @@ describe('Embed', () => {
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field amount THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() =>
|
||||
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
@@ -403,7 +384,7 @@ describe('Embed', () => {
|
||||
|
||||
describe('GIVEN invalid field name THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
|
||||
});
|
||||
@@ -411,7 +392,7 @@ describe('Embed', () => {
|
||||
|
||||
describe('GIVEN invalid field name length THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
|
||||
});
|
||||
@@ -419,7 +400,7 @@ describe('Embed', () => {
|
||||
|
||||
describe('GIVEN invalid field value length THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError();
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
hyperlink,
|
||||
inlineCode,
|
||||
italic,
|
||||
memberNicknameMention,
|
||||
quote,
|
||||
roleMention,
|
||||
spoiler,
|
||||
@@ -122,12 +121,6 @@ describe('Message formatters', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('memberNicknameMention', () => {
|
||||
test('GIVEN memberId THEN returns "<@![memberId]>"', () => {
|
||||
expect(memberNicknameMention('139836912335716352')).toBe('<@!139836912335716352>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('channelMention', () => {
|
||||
test('GIVEN channelId THEN returns "<#[channelId]>"', () => {
|
||||
expect(channelMention('829924760309334087')).toBe('<#829924760309334087>');
|
||||
|
||||
99
packages/builders/docs/examples/Slash Command Builders.md
Normal file
99
packages/builders/docs/examples/Slash Command Builders.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Slash Command Builders
|
||||
|
||||
## Ping command
|
||||
|
||||
```ts
|
||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||
|
||||
// Create a slash command builder
|
||||
const pingCommand = new SlashCommandBuilder().setName('ping').setDescription('Check if this interaction is responsive');
|
||||
|
||||
// Get the raw data that can be sent to Discord
|
||||
const rawData = pingCommand.toJSON();
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
```ts
|
||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||
|
||||
// Creates a boop command
|
||||
const boopCommand = new SlashCommandBuilder()
|
||||
.setName('boop')
|
||||
.setDescription('Boops the specified user, as many times as you want')
|
||||
.addUserOption((option) => option.setName('user').setDescription('The user to boop').setRequired(true))
|
||||
|
||||
// Adds an integer option
|
||||
.addIntegerOption((option) =>
|
||||
option.setName('boop_amount').setDescription('How many times should the user be booped (defaults to 1)'),
|
||||
)
|
||||
|
||||
// Supports choices too!
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('boop_reminder')
|
||||
.setDescription('How often should we remind you to boop the user')
|
||||
.addChoices({ name: 'Every day', value: 1 }, { name: 'Weekly', value: 7 }),
|
||||
);
|
||||
|
||||
// Get the final raw data that can be sent to Discord
|
||||
const rawData = boopCommand.toJSON();
|
||||
```
|
||||
|
||||
## Subcommands and subcommand groups
|
||||
|
||||
```ts
|
||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||
|
||||
const pointsCommand = new SlashCommandBuilder()
|
||||
.setName('points')
|
||||
.setDescription('Lists or manages user points')
|
||||
|
||||
// Add a manage group
|
||||
.addSubcommandGroup((group) =>
|
||||
group
|
||||
.setName('manage')
|
||||
.setDescription('Shows or manages points in the server')
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName('user_points')
|
||||
.setDescription("Alters a user's points")
|
||||
.addUserOption((option) =>
|
||||
option.setName('user').setDescription('The user whose points to alter').setRequired(true),
|
||||
)
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('action')
|
||||
.setDescription('What action should be taken with the users points?')
|
||||
.addChoices(
|
||||
{ name: 'Add points', value: 'add' },
|
||||
{ name: 'Remove points', value: 'remove' },
|
||||
{ name: 'Reset points', value: 'reset' },
|
||||
)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addIntegerOption((option) => option.setName('points').setDescription('Points to add or remove')),
|
||||
),
|
||||
)
|
||||
|
||||
// Add an information group
|
||||
.addSubcommandGroup((group) =>
|
||||
group
|
||||
.setName('info')
|
||||
.setDescription('Shows information about points in the guild')
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand.setName('total').setDescription('Tells you the total amount of points given in the guild'),
|
||||
)
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName('user')
|
||||
.setDescription("Lists a user's points")
|
||||
.addUserOption((option) =>
|
||||
option.setName('user').setDescription('The user whose points to list').setRequired(true),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Get the final raw data that can be sent to Discord
|
||||
const rawData = pointsCommand.toJSON();
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@discordjs/builders",
|
||||
"version": "0.12.0",
|
||||
"version": "0.14.0-dev",
|
||||
"description": "A set of builders that you can use when creating your bot",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
@@ -16,7 +16,8 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "src",
|
||||
@@ -51,33 +52,33 @@
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.3.0",
|
||||
"discord-api-types": "^0.26.1",
|
||||
"ts-mixer": "^6.0.0",
|
||||
"tslib": "^2.3.1",
|
||||
"zod": "^3.11.6"
|
||||
"@sapphire/shapeshift": "^2.0.0",
|
||||
"@sindresorhus/is": "^4.6.0",
|
||||
"discord-api-types": "^0.31.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.1",
|
||||
"tslib": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.12",
|
||||
"@babel/plugin-proposal-decorators": "^7.16.5",
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/plugin-proposal-decorators": "^7.17.9",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-typescript": "^7.16.5",
|
||||
"@discordjs/ts-docgen": "^0.3.4",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.0",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@discordjs/ts-docgen": "^0.4.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^16.11.27",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"babel-plugin-transform-typescript-metadata": "^0.3.2",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-marine": "^9.3.2",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.4.7",
|
||||
"prettier": "^2.5.1",
|
||||
"standard-version": "^9.3.2",
|
||||
"tsup": "^5.11.11",
|
||||
"typedoc": "^0.22.11",
|
||||
"typescript": "^4.5.5"
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-marine": "^9.4.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"tsup": "^5.12.5",
|
||||
"typedoc": "^0.22.15",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { APIActionRowComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import type { ButtonComponent, SelectMenuComponent } from '..';
|
||||
import type { Component } from './Component';
|
||||
import { createComponent } from './Components';
|
||||
import {
|
||||
type APIActionRowComponent,
|
||||
ComponentType,
|
||||
APIMessageActionRowComponent,
|
||||
APIModalActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from './Component';
|
||||
import { createComponentBuilder } from './Components';
|
||||
import type { ButtonBuilder, SelectMenuBuilder, TextInputBuilder } from '..';
|
||||
|
||||
export type ActionRowComponent = ButtonComponent | SelectMenuComponent;
|
||||
|
||||
// TODO: Add valid form component types
|
||||
export type MessageComponentBuilder =
|
||||
| MessageActionRowComponentBuilder
|
||||
| ActionRowBuilder<MessageActionRowComponentBuilder>;
|
||||
export type ModalComponentBuilder = ModalActionRowComponentBuilder | ActionRowBuilder<ModalActionRowComponentBuilder>;
|
||||
export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder;
|
||||
export type ModalActionRowComponentBuilder = TextInputBuilder;
|
||||
export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* Represents an action row component
|
||||
*/
|
||||
export class ActionRow<T extends ActionRowComponent> implements Component {
|
||||
public readonly components: T[] = [];
|
||||
public readonly type = ComponentType.ActionRow;
|
||||
export class ActionRowBuilder<T extends AnyComponentBuilder> extends ComponentBuilder<
|
||||
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
||||
> {
|
||||
/**
|
||||
* The components within this action row
|
||||
*/
|
||||
public readonly components: T[];
|
||||
|
||||
public constructor(data?: APIActionRowComponent) {
|
||||
this.components = (data?.components.map(createComponent) ?? []) as T[];
|
||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
||||
super({ type: ComponentType.ActionRow, ...data });
|
||||
this.components = (components?.map((c) => createComponentBuilder(c)) ?? []) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,15 +47,16 @@ export class ActionRow<T extends ActionRowComponent> implements Component {
|
||||
* Sets the components in this action row
|
||||
* @param components The components to set this row to
|
||||
*/
|
||||
public setComponents(components: T[]) {
|
||||
Reflect.set(this, 'components', [...components]);
|
||||
public setComponents(...components: T[]) {
|
||||
this.components.splice(0, this.components.length, ...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIActionRowComponent {
|
||||
public toJSON(): APIActionRowComponent<ReturnType<T['toJSON']>> {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this,
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
};
|
||||
} as APIActionRowComponent<ReturnType<T['toJSON']>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,58 @@
|
||||
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import type { SelectMenuOption } from './selectMenu/SelectMenuOption';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v10';
|
||||
import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './selectMenu/UnsafeSelectMenuOption';
|
||||
|
||||
export const customIdValidator = z.string().min(1).max(100);
|
||||
export const customIdValidator = s.string.lengthGe(1).lengthLe(100);
|
||||
|
||||
export const emojiValidator = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
animated: z.boolean(),
|
||||
})
|
||||
.partial()
|
||||
.strict();
|
||||
export const emojiValidator = s.object({
|
||||
id: s.string,
|
||||
name: s.string,
|
||||
animated: s.boolean,
|
||||
}).partial.strict;
|
||||
|
||||
export const disabledValidator = z.boolean();
|
||||
export const disabledValidator = s.boolean;
|
||||
|
||||
export const buttonLabelValidator = z.string().nonempty().max(80);
|
||||
export const buttonLabelValidator = s.string.lengthGe(1).lengthLe(80);
|
||||
|
||||
export const buttonStyleValidator = z.number().int().min(ButtonStyle.Primary).max(ButtonStyle.Link);
|
||||
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
|
||||
|
||||
export const placeholderValidator = z.string().max(100);
|
||||
export const minMaxValidator = z.number().int().min(0).max(25);
|
||||
export const placeholderValidator = s.string.lengthLe(150);
|
||||
export const minMaxValidator = s.number.int.ge(0).le(25);
|
||||
|
||||
export const optionsValidator = z.object({}).array().nonempty();
|
||||
export const labelValueDescriptionValidator = s.string.lengthGe(1).lengthLe(100);
|
||||
export const optionValidator = s.union(
|
||||
s.object({
|
||||
label: labelValueDescriptionValidator,
|
||||
value: labelValueDescriptionValidator,
|
||||
description: labelValueDescriptionValidator.optional,
|
||||
emoji: emojiValidator.optional,
|
||||
default: s.boolean.optional,
|
||||
}),
|
||||
s.instance(UnsafeSelectMenuOptionBuilder),
|
||||
);
|
||||
export const optionsValidator = optionValidator.array.lengthGe(0);
|
||||
export const optionsLengthValidator = s.number.int.ge(0).le(25);
|
||||
|
||||
export function validateRequiredSelectMenuParameters(options: SelectMenuOption[], customId?: string) {
|
||||
export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
optionsValidator.parse(options);
|
||||
}
|
||||
|
||||
export const labelValueValidator = z.string().min(1).max(100);
|
||||
export const defaultValidator = z.boolean();
|
||||
export const labelValueValidator = s.string.lengthGe(1).lengthLe(100);
|
||||
export const defaultValidator = s.boolean;
|
||||
|
||||
export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {
|
||||
labelValueValidator.parse(label);
|
||||
labelValueValidator.parse(value);
|
||||
}
|
||||
|
||||
export const urlValidator = z.string().url();
|
||||
export const urlValidator = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:', 'discord:'],
|
||||
});
|
||||
|
||||
export function validateRequiredButtonParameters(
|
||||
style: ButtonStyle,
|
||||
style?: ButtonStyle,
|
||||
label?: string,
|
||||
emoji?: APIMessageComponentEmoji,
|
||||
customId?: string,
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { APIButtonComponent, APIMessageComponentEmoji, ButtonStyle, ComponentType } from 'discord-api-types/v9';
|
||||
import {
|
||||
buttonLabelValidator,
|
||||
buttonStyleValidator,
|
||||
customIdValidator,
|
||||
disabledValidator,
|
||||
emojiValidator,
|
||||
urlValidator,
|
||||
validateRequiredButtonParameters,
|
||||
} from './Assertions';
|
||||
import type { Component } from './Component';
|
||||
|
||||
export class ButtonComponent implements Component {
|
||||
public readonly type = ComponentType.Button as const;
|
||||
public readonly style!: ButtonStyle;
|
||||
public readonly label?: string;
|
||||
public readonly emoji?: APIMessageComponentEmoji;
|
||||
public readonly disabled?: boolean;
|
||||
public readonly custom_id!: string;
|
||||
public readonly url!: string;
|
||||
|
||||
public constructor(data?: APIButtonComponent) {
|
||||
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.style = data?.style as ButtonStyle;
|
||||
this.label = data?.label;
|
||||
this.emoji = data?.emoji;
|
||||
this.disabled = data?.disabled;
|
||||
|
||||
// This if/else makes typescript happy
|
||||
if (data?.style === ButtonStyle.Link) {
|
||||
this.url = data.url;
|
||||
} else {
|
||||
this.custom_id = data?.custom_id as string;
|
||||
}
|
||||
|
||||
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style of this button
|
||||
* @param style The style of the button
|
||||
*/
|
||||
public setStyle(style: ButtonStyle) {
|
||||
buttonStyleValidator.parse(style);
|
||||
Reflect.set(this, 'style', style);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL for this button
|
||||
* @param url The URL to open when this button is clicked
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
urlValidator.parse(url);
|
||||
Reflect.set(this, 'url', url);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom Id for this button
|
||||
* @param customId The custom ID to use for this button
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
customIdValidator.parse(customId);
|
||||
Reflect.set(this, 'custom_id', customId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this button
|
||||
* @param emoji The emoji to display on this button
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
emojiValidator.parse(emoji);
|
||||
Reflect.set(this, 'emoji', emoji);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this button is disable or not
|
||||
* @param disabled Whether or not to disable this button or not
|
||||
*/
|
||||
public setDisabled(disabled: boolean) {
|
||||
disabledValidator.parse(disabled);
|
||||
Reflect.set(this, 'disabled', disabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this button
|
||||
* @param label The label to display on this button
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
buttonLabelValidator.parse(label);
|
||||
Reflect.set(this, 'label', label);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIButtonComponent {
|
||||
validateRequiredButtonParameters(this.style, this.label, this.emoji, this.custom_id, this.url);
|
||||
return {
|
||||
...this,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,28 @@
|
||||
import type { APIMessageComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
APIBaseComponent,
|
||||
ComponentType,
|
||||
} from 'discord-api-types/v10';
|
||||
import type { JSONEncodable } from '../util/jsonEncodable';
|
||||
|
||||
export type AnyAPIActionRowComponent = APIActionRowComponentTypes | APIActionRowComponent<APIActionRowComponentTypes>;
|
||||
|
||||
/**
|
||||
* Represents a discord component
|
||||
*/
|
||||
export interface Component {
|
||||
export abstract class ComponentBuilder<
|
||||
DataType extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>,
|
||||
> implements JSONEncodable<AnyAPIActionRowComponent>
|
||||
{
|
||||
/**
|
||||
* The type of this component
|
||||
* The API data associated with this component
|
||||
*/
|
||||
readonly type: ComponentType;
|
||||
/**
|
||||
* Converts this component to an API-compatible JSON object
|
||||
*/
|
||||
toJSON: () => APIMessageComponent;
|
||||
public readonly data: Partial<DataType>;
|
||||
|
||||
public abstract toJSON(): AnyAPIActionRowComponent;
|
||||
|
||||
public constructor(data: Partial<DataType>) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
import { APIMessageComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import { ActionRow, ButtonComponent, Component, SelectMenuComponent } from '../index';
|
||||
import type { ActionRowComponent } from './ActionRow';
|
||||
import { APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v10';
|
||||
import type { AnyComponentBuilder, MessageComponentBuilder, ModalComponentBuilder } from './ActionRow';
|
||||
import { ActionRowBuilder, ButtonBuilder, ComponentBuilder, SelectMenuBuilder, TextInputBuilder } from '../index';
|
||||
|
||||
export interface MappedComponentTypes {
|
||||
[ComponentType.ActionRow]: ActionRow<ActionRowComponent>;
|
||||
[ComponentType.Button]: ButtonComponent;
|
||||
[ComponentType.SelectMenu]: SelectMenuComponent;
|
||||
[ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>;
|
||||
[ComponentType.Button]: ButtonBuilder;
|
||||
[ComponentType.SelectMenu]: SelectMenuBuilder;
|
||||
[ComponentType.TextInput]: TextInputBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating components from API data
|
||||
* @param data The api data to transform to a component class
|
||||
*/
|
||||
export function createComponent<T extends keyof MappedComponentTypes>(
|
||||
data: APIMessageComponent & { type: T },
|
||||
export function createComponentBuilder<T extends keyof MappedComponentTypes>(
|
||||
data: (APIMessageComponent | APIModalComponent) & { type: T },
|
||||
): MappedComponentTypes[T];
|
||||
export function createComponent(data: APIMessageComponent): Component {
|
||||
export function createComponentBuilder<C extends MessageComponentBuilder | ModalComponentBuilder>(data: C): C;
|
||||
export function createComponentBuilder(
|
||||
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
|
||||
): ComponentBuilder {
|
||||
if (data instanceof ComponentBuilder) {
|
||||
return data;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case ComponentType.ActionRow:
|
||||
return new ActionRow(data);
|
||||
return new ActionRowBuilder(data);
|
||||
case ComponentType.Button:
|
||||
return new ButtonComponent(data);
|
||||
return new ButtonBuilder(data);
|
||||
case ComponentType.SelectMenu:
|
||||
return new SelectMenuComponent(data);
|
||||
return new SelectMenuBuilder(data);
|
||||
case ComponentType.TextInput:
|
||||
return new TextInputBuilder(data);
|
||||
default:
|
||||
// @ts-expect-error
|
||||
throw new Error(`Cannot serialize component type: ${data.type as number}`);
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Cannot properly serialize component type: ${data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
57
packages/builders/src/components/button/Button.ts
Normal file
57
packages/builders/src/components/button/Button.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type {
|
||||
ButtonStyle,
|
||||
APIMessageComponentEmoji,
|
||||
APIButtonComponent,
|
||||
APIButtonComponentWithCustomId,
|
||||
APIButtonComponentWithURL,
|
||||
} from 'discord-api-types/v10';
|
||||
import { UnsafeButtonBuilder } from './UnsafeButton';
|
||||
import {
|
||||
buttonLabelValidator,
|
||||
buttonStyleValidator,
|
||||
customIdValidator,
|
||||
disabledValidator,
|
||||
emojiValidator,
|
||||
urlValidator,
|
||||
validateRequiredButtonParameters,
|
||||
} from '../Assertions';
|
||||
|
||||
/**
|
||||
* Represents a validated button component
|
||||
*/
|
||||
export class ButtonBuilder extends UnsafeButtonBuilder {
|
||||
public override setStyle(style: ButtonStyle) {
|
||||
return super.setStyle(buttonStyleValidator.parse(style));
|
||||
}
|
||||
|
||||
public override setURL(url: string) {
|
||||
return super.setURL(urlValidator.parse(url));
|
||||
}
|
||||
|
||||
public override setCustomId(customId: string) {
|
||||
return super.setCustomId(customIdValidator.parse(customId));
|
||||
}
|
||||
|
||||
public override setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
return super.setEmoji(emojiValidator.parse(emoji));
|
||||
}
|
||||
|
||||
public override setDisabled(disabled = true) {
|
||||
return super.setDisabled(disabledValidator.parse(disabled));
|
||||
}
|
||||
|
||||
public override setLabel(label: string) {
|
||||
return super.setLabel(buttonLabelValidator.parse(label));
|
||||
}
|
||||
|
||||
public override toJSON(): APIButtonComponent {
|
||||
validateRequiredButtonParameters(
|
||||
this.data.style,
|
||||
this.data.label,
|
||||
this.data.emoji,
|
||||
(this.data as APIButtonComponentWithCustomId).custom_id,
|
||||
(this.data as APIButtonComponentWithURL).url,
|
||||
);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
79
packages/builders/src/components/button/UnsafeButton.ts
Normal file
79
packages/builders/src/components/button/UnsafeButton.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ComponentType,
|
||||
ButtonStyle,
|
||||
type APIMessageComponentEmoji,
|
||||
type APIButtonComponent,
|
||||
type APIButtonComponentWithURL,
|
||||
type APIButtonComponentWithCustomId,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
|
||||
/**
|
||||
* Represents a non-validated button component
|
||||
*/
|
||||
export class UnsafeButtonBuilder extends ComponentBuilder<APIButtonComponent> {
|
||||
public constructor(data?: Partial<APIButtonComponent>) {
|
||||
super({ type: ComponentType.Button, ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style of this button
|
||||
* @param style The style of the button
|
||||
*/
|
||||
public setStyle(style: ButtonStyle) {
|
||||
this.data.style = style;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL for this button
|
||||
* @param url The URL to open when this button is clicked
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
(this.data as APIButtonComponentWithURL).url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom Id for this button
|
||||
* @param customId The custom id to use for this button
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
(this.data as APIButtonComponentWithCustomId).custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this button
|
||||
* @param emoji The emoji to display on this button
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
this.data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this button is disable or not
|
||||
* @param disabled Whether or not to disable this button or not
|
||||
*/
|
||||
public setDisabled(disabled = true) {
|
||||
this.data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this button
|
||||
* @param label The label to display on this button
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIButtonComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
} as APIButtonComponent;
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,68 @@
|
||||
import { APISelectMenuComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import type { APISelectMenuComponent, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
import {
|
||||
customIdValidator,
|
||||
disabledValidator,
|
||||
minMaxValidator,
|
||||
optionsLengthValidator,
|
||||
optionValidator,
|
||||
placeholderValidator,
|
||||
validateRequiredSelectMenuParameters,
|
||||
} from '../Assertions';
|
||||
import type { Component } from '../Component';
|
||||
import { SelectMenuOption } from './SelectMenuOption';
|
||||
|
||||
/**
|
||||
* Represents a select menu component
|
||||
* Represents a validated select menu component
|
||||
*/
|
||||
export class SelectMenuComponent implements Component {
|
||||
public readonly type = ComponentType.SelectMenu as const;
|
||||
public readonly options: SelectMenuOption[];
|
||||
public readonly placeholder?: string;
|
||||
public readonly min_values?: number;
|
||||
public readonly max_values?: number;
|
||||
public readonly custom_id!: string;
|
||||
public readonly disabled?: boolean;
|
||||
|
||||
public constructor(data?: APISelectMenuComponent) {
|
||||
this.options = data?.options.map((option) => new SelectMenuOption(option)) ?? [];
|
||||
this.placeholder = data?.placeholder;
|
||||
this.min_values = data?.min_values;
|
||||
this.max_values = data?.max_values;
|
||||
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.custom_id = data?.custom_id as string;
|
||||
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.disabled = data?.disabled;
|
||||
export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
|
||||
public override setPlaceholder(placeholder: string) {
|
||||
return super.setPlaceholder(placeholderValidator.parse(placeholder));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the placeholder for this select menu
|
||||
* @param placeholder The placeholder to use for this select menu
|
||||
*/
|
||||
public setPlaceholder(placeholder: string) {
|
||||
placeholderValidator.parse(placeholder);
|
||||
Reflect.set(this, 'placeholder', placeholder);
|
||||
public override setMinValues(minValues: number) {
|
||||
return super.setMinValues(minMaxValidator.parse(minValues));
|
||||
}
|
||||
|
||||
public override setMaxValues(maxValues: number) {
|
||||
return super.setMaxValues(minMaxValidator.parse(maxValues));
|
||||
}
|
||||
|
||||
public override setCustomId(customId: string) {
|
||||
return super.setCustomId(customIdValidator.parse(customId));
|
||||
}
|
||||
|
||||
public override setDisabled(disabled = true) {
|
||||
return super.setDisabled(disabledValidator.parse(disabled));
|
||||
}
|
||||
|
||||
public override addOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
optionsLengthValidator.parse(this.options.length + options.length);
|
||||
this.options.push(
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOptionBuilder
|
||||
? option
|
||||
: new UnsafeSelectMenuOptionBuilder(optionValidator.parse(option) as APISelectMenuOption),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets thes minimum values that must be selected in the select menu
|
||||
* @param minValues The minimum values that must be selected
|
||||
*/
|
||||
public setMinValues(minValues: number) {
|
||||
minMaxValidator.parse(minValues);
|
||||
Reflect.set(this, 'min_values', minValues);
|
||||
public override setOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
optionsLengthValidator.parse(options.length);
|
||||
this.options.splice(
|
||||
0,
|
||||
this.options.length,
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOptionBuilder
|
||||
? option
|
||||
: new UnsafeSelectMenuOptionBuilder(optionValidator.parse(option) as APISelectMenuOption),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets thes maximum values that must be selected in the select menu
|
||||
* @param minValues The maximum values that must be selected
|
||||
*/
|
||||
public setMaxValues(maxValues: number) {
|
||||
minMaxValidator.parse(maxValues);
|
||||
Reflect.set(this, 'max_values', maxValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom Id for this select menu
|
||||
* @param customId The custom ID to use for this select menu
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
customIdValidator.parse(customId);
|
||||
Reflect.set(this, 'custom_id', customId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not this select menu is disabled
|
||||
* @param disabled Whether or not this select menu is disabled
|
||||
*/
|
||||
public setDisabled(disabled: boolean) {
|
||||
disabledValidator.parse(disabled);
|
||||
Reflect.set(this, 'disabled', disabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds options to this select menu
|
||||
* @param options The options to add to this select menu
|
||||
* @returns
|
||||
*/
|
||||
public addOptions(...options: SelectMenuOption[]) {
|
||||
this.options.push(...options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the options on this select menu
|
||||
* @param options The options to set on this select menu
|
||||
*/
|
||||
public setOptions(options: SelectMenuOption[]) {
|
||||
Reflect.set(this, 'options', [...options]);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APISelectMenuComponent {
|
||||
validateRequiredSelectMenuParameters(this.options, this.custom_id);
|
||||
return {
|
||||
...this,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
};
|
||||
public override toJSON(): APISelectMenuComponent {
|
||||
validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v9';
|
||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
import {
|
||||
defaultValidator,
|
||||
emojiValidator,
|
||||
@@ -7,77 +8,23 @@ import {
|
||||
} from '../Assertions';
|
||||
|
||||
/**
|
||||
* Represents an option within a select menu component
|
||||
* Represents a validated option within a select menu component
|
||||
*/
|
||||
export class SelectMenuOption {
|
||||
public readonly label!: string;
|
||||
public readonly value!: string;
|
||||
public readonly description?: string;
|
||||
public readonly emoji?: APIMessageComponentEmoji;
|
||||
public readonly default?: boolean;
|
||||
|
||||
public constructor(data?: APISelectMenuOption) {
|
||||
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.label = data?.label as string;
|
||||
this.value = data?.value as string;
|
||||
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.description = data?.description;
|
||||
this.emoji = data?.emoji;
|
||||
this.default = data?.default;
|
||||
export class SelectMenuOptionBuilder extends UnsafeSelectMenuOptionBuilder {
|
||||
public override setDescription(description: string) {
|
||||
return super.setDescription(labelValueValidator.parse(description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label of this option
|
||||
* @param label The label to show on this option
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
Reflect.set(this, 'label', label);
|
||||
return this;
|
||||
public override setDefault(isDefault = true) {
|
||||
return super.setDefault(defaultValidator.parse(isDefault));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of this option
|
||||
* @param value The value of this option
|
||||
*/
|
||||
public setValue(value: string) {
|
||||
Reflect.set(this, 'value', value);
|
||||
return this;
|
||||
public override setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
return super.setEmoji(emojiValidator.parse(emoji));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this option.
|
||||
* @param description The description of this option
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
labelValueValidator.parse(description);
|
||||
Reflect.set(this, 'description', description);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this option is selected by default
|
||||
* @param isDefault Whether or not this option is selected by default
|
||||
*/
|
||||
public setDefault(isDefault: boolean) {
|
||||
defaultValidator.parse(isDefault);
|
||||
Reflect.set(this, 'default', isDefault);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this button
|
||||
* @param emoji The emoji to display on this button
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
emojiValidator.parse(emoji);
|
||||
Reflect.set(this, 'emoji', emoji);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APISelectMenuOption {
|
||||
validateRequiredSelectMenuOptionParameters(this.label, this.value);
|
||||
return {
|
||||
...this,
|
||||
};
|
||||
public override toJSON(): APISelectMenuOption {
|
||||
validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
101
packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts
Normal file
101
packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v10';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
|
||||
/**
|
||||
* Represents a non-validated select menu component
|
||||
*/
|
||||
export class UnsafeSelectMenuBuilder extends ComponentBuilder<APISelectMenuComponent> {
|
||||
/**
|
||||
* The options within this select menu
|
||||
*/
|
||||
public readonly options: UnsafeSelectMenuOptionBuilder[];
|
||||
|
||||
public constructor(data?: Partial<APISelectMenuComponent>) {
|
||||
const { options, ...initData } = data ?? {};
|
||||
super({ type: ComponentType.SelectMenu, ...initData });
|
||||
this.options = options?.map((o) => new UnsafeSelectMenuOptionBuilder(o)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the placeholder for this select menu
|
||||
* @param placeholder The placeholder to use for this select menu
|
||||
*/
|
||||
public setPlaceholder(placeholder: string) {
|
||||
this.data.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum values that must be selected in the select menu
|
||||
* @param minValues The minimum values that must be selected
|
||||
*/
|
||||
public setMinValues(minValues: number) {
|
||||
this.data.min_values = minValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum values that must be selected in the select menu
|
||||
* @param minValues The maximum values that must be selected
|
||||
*/
|
||||
public setMaxValues(maxValues: number) {
|
||||
this.data.max_values = maxValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom Id for this select menu
|
||||
* @param customId The custom id to use for this select menu
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not this select menu is disabled
|
||||
* @param disabled Whether or not this select menu is disabled
|
||||
*/
|
||||
public setDisabled(disabled = true) {
|
||||
this.data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds options to this select menu
|
||||
* @param options The options to add to this select menu
|
||||
* @returns
|
||||
*/
|
||||
public addOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
this.options.push(
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the options on this select menu
|
||||
* @param options The options to set on this select menu
|
||||
*/
|
||||
public setOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
this.options.splice(
|
||||
0,
|
||||
this.options.length,
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APISelectMenuComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
options: this.options.map((o) => o.toJSON()),
|
||||
} as APISelectMenuComponent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
|
||||
/**
|
||||
* Represents a non-validated option within a select menu component
|
||||
*/
|
||||
export class UnsafeSelectMenuOptionBuilder {
|
||||
public constructor(public data: Partial<APISelectMenuOption> = {}) {}
|
||||
|
||||
/**
|
||||
* Sets the label of this option
|
||||
* @param label The label to show on this option
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of this option
|
||||
* @param value The value of this option
|
||||
*/
|
||||
public setValue(value: string) {
|
||||
this.data.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this option.
|
||||
* @param description The description of this option
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this option is selected by default
|
||||
* @param isDefault Whether this option is selected by default
|
||||
*/
|
||||
public setDefault(isDefault = true) {
|
||||
this.data.default = isDefault;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this option
|
||||
* @param emoji The emoji to display on this option
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
this.data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APISelectMenuOption {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
} as APISelectMenuOption;
|
||||
}
|
||||
}
|
||||
17
packages/builders/src/components/textInput/Assertions.ts
Normal file
17
packages/builders/src/components/textInput/Assertions.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { TextInputStyle } from 'discord-api-types/v10';
|
||||
import { customIdValidator } from '../Assertions';
|
||||
|
||||
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
|
||||
export const minLengthValidator = s.number.int.ge(0).le(4000);
|
||||
export const maxLengthValidator = s.number.int.ge(1).le(4000);
|
||||
export const requiredValidator = s.boolean;
|
||||
export const valueValidator = s.string.lengthLe(4000);
|
||||
export const placeholderValidator = s.string.lengthLe(100);
|
||||
export const labelValidator = s.string.lengthGe(1).lengthLe(45);
|
||||
|
||||
export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
textInputStyleValidator.parse(style);
|
||||
labelValidator.parse(label);
|
||||
}
|
||||
37
packages/builders/src/components/textInput/TextInput.ts
Normal file
37
packages/builders/src/components/textInput/TextInput.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { APITextInputComponent } from 'discord-api-types/v10';
|
||||
import {
|
||||
maxLengthValidator,
|
||||
minLengthValidator,
|
||||
placeholderValidator,
|
||||
requiredValidator,
|
||||
valueValidator,
|
||||
validateRequiredParameters,
|
||||
} from './Assertions';
|
||||
import { UnsafeTextInputBuilder } from './UnsafeTextInput';
|
||||
|
||||
export class TextInputBuilder extends UnsafeTextInputBuilder {
|
||||
public override setMinLength(minLength: number) {
|
||||
return super.setMinLength(minLengthValidator.parse(minLength));
|
||||
}
|
||||
|
||||
public override setMaxLength(maxLength: number) {
|
||||
return super.setMaxLength(maxLengthValidator.parse(maxLength));
|
||||
}
|
||||
|
||||
public override setRequired(required = true) {
|
||||
return super.setRequired(requiredValidator.parse(required));
|
||||
}
|
||||
|
||||
public override setValue(value: string) {
|
||||
return super.setValue(valueValidator.parse(value));
|
||||
}
|
||||
|
||||
public override setPlaceholder(placeholder: string) {
|
||||
return super.setPlaceholder(placeholderValidator.parse(placeholder));
|
||||
}
|
||||
|
||||
public override toJSON(): APITextInputComponent {
|
||||
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { ComponentBuilder } from '../../index';
|
||||
|
||||
export class UnsafeTextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
||||
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
|
||||
super({ type: ComponentType.TextInput, ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id for this text input
|
||||
* @param customId The custom id of this text inputå
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this text input
|
||||
* @param label The label for this text input
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style for this text input
|
||||
* @param style The style for this text input
|
||||
*/
|
||||
public setStyle(style: TextInputStyle) {
|
||||
this.data.style = style;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum length of text for this text input
|
||||
* @param minLength The minimum length of text for this text input
|
||||
*/
|
||||
public setMinLength(minLength: number) {
|
||||
this.data.min_length = minLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum length of text for this text input
|
||||
* @param maxLength The maximum length of text for this text input
|
||||
*/
|
||||
public setMaxLength(maxLength: number) {
|
||||
this.data.max_length = maxLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the placeholder of this text input
|
||||
* @param placeholder The placeholder of this text input
|
||||
*/
|
||||
public setPlaceholder(placeholder: string) {
|
||||
this.data.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of this text input
|
||||
* @param value The value for this text input
|
||||
*/
|
||||
public setValue(value: string) {
|
||||
this.data.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this text input is required or not
|
||||
* @param required Whether this text input is required or not
|
||||
*/
|
||||
public setRequired(required = true) {
|
||||
this.data.required = required;
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APITextInputComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
} as APITextInputComponent;
|
||||
}
|
||||
|
||||
public equals(other: UnsafeTextInputBuilder | APITextInputComponent): boolean {
|
||||
if (other instanceof UnsafeTextInputBuilder) {
|
||||
return isEqual(other.data, this.data);
|
||||
}
|
||||
|
||||
return isEqual(other, this.data);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
export * as EmbedAssertions from './messages/embed/Assertions';
|
||||
export * from './messages/embed/Embed';
|
||||
export * from './messages/formatters';
|
||||
export * from './messages/embed/UnsafeEmbed';
|
||||
|
||||
export * as ComponentAssertions from './components/Assertions';
|
||||
export * from './components/ActionRow';
|
||||
export * from './components/Button';
|
||||
export * from './components/button/Button';
|
||||
export * from './components/Component';
|
||||
export * from './components/Components';
|
||||
export * from './components/textInput/TextInput';
|
||||
export * as TextInputAssertions from './components/textInput/Assertions';
|
||||
export * from './components/textInput/UnsafeTextInput';
|
||||
export * from './interactions/modals/UnsafeModal';
|
||||
export * from './interactions/modals/Modal';
|
||||
export * as ModalAssertions from './interactions/modals/Assertions';
|
||||
export * from './components/selectMenu/SelectMenu';
|
||||
export * from './components/selectMenu/SelectMenuOption';
|
||||
export * from './components/button/UnsafeButton';
|
||||
export * from './components/selectMenu/UnsafeSelectMenu';
|
||||
export * from './components/selectMenu/UnsafeSelectMenuOption';
|
||||
|
||||
export * as SlashCommandAssertions from './interactions/slashCommands/Assertions';
|
||||
export * from './interactions/slashCommands/SlashCommandBuilder';
|
||||
@@ -19,8 +29,13 @@ export * from './interactions/slashCommands/options/integer';
|
||||
export * from './interactions/slashCommands/options/mentionable';
|
||||
export * from './interactions/slashCommands/options/number';
|
||||
export * from './interactions/slashCommands/options/role';
|
||||
export * from './interactions/slashCommands/options/attachment';
|
||||
export * from './interactions/slashCommands/options/string';
|
||||
export * from './interactions/slashCommands/options/user';
|
||||
|
||||
export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions';
|
||||
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder';
|
||||
|
||||
export * from './util/jsonEncodable';
|
||||
export * from './util/equatable';
|
||||
export * from './util/componentUtil';
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
import { ApplicationCommandType } from 'discord-api-types/v9';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ApplicationCommandType } from 'discord-api-types/v10';
|
||||
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder';
|
||||
|
||||
const namePredicate = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.regex(/^( *[\p{L}\p{N}_-]+ *)+$/u);
|
||||
const namePredicate = s.string
|
||||
.lengthGe(1)
|
||||
.lengthLe(32)
|
||||
.regex(/^( *[\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+ *)+$/u);
|
||||
|
||||
const typePredicate = z.union([z.literal(ApplicationCommandType.User), z.literal(ApplicationCommandType.Message)]);
|
||||
const typePredicate = s.union(s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message));
|
||||
|
||||
const booleanPredicate = z.boolean();
|
||||
const booleanPredicate = s.boolean;
|
||||
|
||||
export function validateDefaultPermission(value: unknown): asserts value is boolean {
|
||||
booleanPredicate.parse(value);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ApplicationCommandType, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10';
|
||||
import { validateRequiredParameters, validateName, validateType, validateDefaultPermission } from './Assertions';
|
||||
import type { ApplicationCommandType, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v9';
|
||||
|
||||
export class ContextMenuCommandBuilder {
|
||||
/**
|
||||
|
||||
16
packages/builders/src/interactions/modals/Assertions.ts
Normal file
16
packages/builders/src/interactions/modals/Assertions.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../..';
|
||||
import { customIdValidator } from '../../components/Assertions';
|
||||
|
||||
export const titleValidator = s.string.lengthGe(1).lengthLe(45);
|
||||
export const componentsValidator = s.instance(ActionRowBuilder).array.lengthGe(1);
|
||||
|
||||
export function validateRequiredParameters(
|
||||
customId?: string,
|
||||
title?: string,
|
||||
components?: ActionRowBuilder<ModalActionRowComponentBuilder>[],
|
||||
) {
|
||||
customIdValidator.parse(customId);
|
||||
titleValidator.parse(title);
|
||||
componentsValidator.parse(components);
|
||||
}
|
||||
19
packages/builders/src/interactions/modals/Modal.ts
Normal file
19
packages/builders/src/interactions/modals/Modal.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
|
||||
import { titleValidator, validateRequiredParameters } from './Assertions';
|
||||
import { UnsafeModalBuilder } from './UnsafeModal';
|
||||
import { customIdValidator } from '../../components/Assertions';
|
||||
|
||||
export class ModalBuilder extends UnsafeModalBuilder {
|
||||
public override setCustomId(customId: string): this {
|
||||
return super.setCustomId(customIdValidator.parse(customId));
|
||||
}
|
||||
|
||||
public override setTitle(title: string) {
|
||||
return super.setTitle(titleValidator.parse(title));
|
||||
}
|
||||
|
||||
public override toJSON(): APIModalInteractionResponseCallbackData {
|
||||
validateRequiredParameters(this.data.custom_id, this.data.title, this.components);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
72
packages/builders/src/interactions/modals/UnsafeModal.ts
Normal file
72
packages/builders/src/interactions/modals/UnsafeModal.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIModalActionRowComponent,
|
||||
APIModalInteractionResponseCallbackData,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ActionRowBuilder, createComponentBuilder, JSONEncodable, ModalActionRowComponentBuilder } from '../../index';
|
||||
|
||||
export class UnsafeModalBuilder implements JSONEncodable<APIModalInteractionResponseCallbackData> {
|
||||
public readonly data: Partial<APIModalInteractionResponseCallbackData>;
|
||||
public readonly components: ActionRowBuilder<ModalActionRowComponentBuilder>[] = [];
|
||||
|
||||
public constructor({ components, ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
|
||||
this.data = { ...data };
|
||||
this.components = (components?.map((c) => createComponentBuilder(c)) ??
|
||||
[]) as ActionRowBuilder<ModalActionRowComponentBuilder>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of the modal
|
||||
* @param title The title of the modal
|
||||
*/
|
||||
public setTitle(title: string) {
|
||||
this.data.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id of the modal
|
||||
* @param customId The custom id of this modal
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds components to this modal
|
||||
* @param components The components to add to this modal
|
||||
*/
|
||||
public addComponents(
|
||||
...components: (
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIModalActionRowComponent>
|
||||
)[]
|
||||
) {
|
||||
this.components.push(
|
||||
...components.map((component) =>
|
||||
component instanceof ActionRowBuilder
|
||||
? component
|
||||
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIModalInteractionResponseCallbackData {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
} as APIModalInteractionResponseCallbackData;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,30 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import is from '@sindresorhus/is';
|
||||
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
|
||||
import { type APIApplicationCommandOptionChoice, Locale } from 'discord-api-types/v10';
|
||||
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
|
||||
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
|
||||
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
|
||||
|
||||
const namePredicate = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.regex(/^[\P{Lu}\p{N}_-]+$/u);
|
||||
const namePredicate = s.string
|
||||
.lengthGe(1)
|
||||
.lengthLe(32)
|
||||
.regex(/^[\P{Lu}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u);
|
||||
|
||||
export function validateName(name: unknown): asserts name is string {
|
||||
namePredicate.parse(name);
|
||||
}
|
||||
|
||||
const descriptionPredicate = z.string().min(1).max(100);
|
||||
const descriptionPredicate = s.string.lengthGe(1).lengthLe(100);
|
||||
const localePredicate = s.nativeEnum(Locale);
|
||||
|
||||
export function validateDescription(description: unknown): asserts description is string {
|
||||
descriptionPredicate.parse(description);
|
||||
}
|
||||
|
||||
const maxArrayLengthPredicate = z.unknown().array().max(25);
|
||||
const maxArrayLengthPredicate = s.unknown.array.lengthLe(25);
|
||||
export function validateLocale(locale: unknown) {
|
||||
return localePredicate.parse(locale);
|
||||
}
|
||||
|
||||
export function validateMaxOptionsLength(options: unknown): asserts options is ToAPIApplicationCommandOptions[] {
|
||||
maxArrayLengthPredicate.parse(options);
|
||||
@@ -42,7 +45,7 @@ export function validateRequiredParameters(
|
||||
validateMaxOptionsLength(options);
|
||||
}
|
||||
|
||||
const booleanPredicate = z.boolean();
|
||||
const booleanPredicate = s.boolean;
|
||||
|
||||
export function validateDefaultPermission(value: unknown): asserts value is boolean {
|
||||
booleanPredicate.parse(value);
|
||||
@@ -52,8 +55,10 @@ export function validateRequired(required: unknown): asserts required is boolean
|
||||
booleanPredicate.parse(required);
|
||||
}
|
||||
|
||||
export function validateMaxChoicesLength(choices: APIApplicationCommandOptionChoice[]) {
|
||||
maxArrayLengthPredicate.parse(choices);
|
||||
const choicesLengthPredicate = s.number.le(25);
|
||||
|
||||
export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void {
|
||||
choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding);
|
||||
}
|
||||
|
||||
export function assertReturnOfBuilder<
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { APIApplicationCommandOption, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v9';
|
||||
import type {
|
||||
APIApplicationCommandOption,
|
||||
LocalizationMap,
|
||||
RESTPostAPIApplicationCommandsJSONBody,
|
||||
} from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import {
|
||||
assertReturnOfBuilder,
|
||||
@@ -6,9 +10,9 @@ import {
|
||||
validateMaxOptionsLength,
|
||||
validateRequiredParameters,
|
||||
} from './Assertions';
|
||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
|
||||
import { SharedNameAndDescription } from './mixins/NameAndDescription';
|
||||
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
|
||||
import { SharedNameAndDescription } from './mixins/NameAndDescription';
|
||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
|
||||
|
||||
@mix(SharedSlashCommandOptions, SharedNameAndDescription)
|
||||
export class SlashCommandBuilder {
|
||||
@@ -17,11 +21,21 @@ export class SlashCommandBuilder {
|
||||
*/
|
||||
public readonly name: string = undefined!;
|
||||
|
||||
/**
|
||||
* The localized names for this command
|
||||
*/
|
||||
public readonly name_localizations?: LocalizationMap;
|
||||
|
||||
/**
|
||||
* The description of this slash command
|
||||
*/
|
||||
public readonly description: string = undefined!;
|
||||
|
||||
/**
|
||||
* The localized descriptions for this command
|
||||
*/
|
||||
public readonly description_localizations?: LocalizationMap;
|
||||
|
||||
/**
|
||||
* The options of this slash command
|
||||
*/
|
||||
@@ -44,7 +58,9 @@ export class SlashCommandBuilder {
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
name_localizations: this.name_localizations,
|
||||
description: this.description,
|
||||
description_localizations: this.description_localizations,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
default_permission: this.defaultPermission,
|
||||
};
|
||||
|
||||
@@ -2,13 +2,13 @@ import {
|
||||
APIApplicationCommandSubcommandGroupOption,
|
||||
APIApplicationCommandSubcommandOption,
|
||||
ApplicationCommandOptionType,
|
||||
} from 'discord-api-types/v9';
|
||||
} from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions';
|
||||
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
|
||||
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
|
||||
import { SharedNameAndDescription } from './mixins/NameAndDescription';
|
||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
|
||||
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
|
||||
|
||||
/**
|
||||
* Represents a folder for subcommands
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { validateRequiredParameters, validateRequired } from '../Assertions';
|
||||
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { SharedNameAndDescription } from './NameAndDescription';
|
||||
import { validateRequiredParameters, validateRequired } from '../Assertions';
|
||||
|
||||
export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription {
|
||||
public abstract readonly type: ApplicationCommandOptionType;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChannelType } from 'discord-api-types/v9';
|
||||
import { z, ZodLiteral } from 'zod';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ChannelType } from 'discord-api-types/v10';
|
||||
|
||||
// Only allow valid channel types to be used. (This can't be dynamic because const enums are erased at runtime)
|
||||
const allowedChannelTypes = [
|
||||
@@ -7,7 +7,6 @@ const allowedChannelTypes = [
|
||||
ChannelType.GuildVoice,
|
||||
ChannelType.GuildCategory,
|
||||
ChannelType.GuildNews,
|
||||
ChannelType.GuildStore,
|
||||
ChannelType.GuildNewsThread,
|
||||
ChannelType.GuildPublicThread,
|
||||
ChannelType.GuildPrivateThread,
|
||||
@@ -16,40 +15,23 @@ const allowedChannelTypes = [
|
||||
|
||||
export type ApplicationCommandOptionAllowedChannelTypes = typeof allowedChannelTypes[number];
|
||||
|
||||
const channelTypePredicate = z.union(
|
||||
allowedChannelTypes.map((type) => z.literal(type)) as [
|
||||
ZodLiteral<ChannelType>,
|
||||
ZodLiteral<ChannelType>,
|
||||
...ZodLiteral<ChannelType>[]
|
||||
],
|
||||
);
|
||||
const channelTypesPredicate = s.array(s.union(...allowedChannelTypes.map((type) => s.literal(type))));
|
||||
|
||||
export class ApplicationCommandOptionChannelTypesMixin {
|
||||
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
|
||||
|
||||
/**
|
||||
* Adds a channel type to this option
|
||||
*
|
||||
* @param channelType The type of channel to allow
|
||||
*/
|
||||
public addChannelType(channelType: ApplicationCommandOptionAllowedChannelTypes) {
|
||||
if (this.channel_types === undefined) {
|
||||
Reflect.set(this, 'channel_types', []);
|
||||
}
|
||||
|
||||
channelTypePredicate.parse(channelType);
|
||||
this.channel_types!.push(channelType);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds channel types to this option
|
||||
*
|
||||
* @param channelTypes The channel types to add
|
||||
*/
|
||||
public addChannelTypes(channelTypes: ApplicationCommandOptionAllowedChannelTypes[]) {
|
||||
channelTypes.forEach((channelType) => this.addChannelType(channelType));
|
||||
public addChannelTypes(...channelTypes: ApplicationCommandOptionAllowedChannelTypes[]) {
|
||||
if (this.channel_types === undefined) {
|
||||
Reflect.set(this, 'channel_types', []);
|
||||
}
|
||||
|
||||
this.channel_types!.push(...channelTypesPredicate.parse(channelTypes));
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import { validateMaxChoicesLength } from '../Assertions';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { validateChoicesLength } from '../Assertions';
|
||||
|
||||
const stringPredicate = z.string().min(1).max(100);
|
||||
const numberPredicate = z.number().gt(-Infinity).lt(Infinity);
|
||||
const choicesPredicate = z.tuple([stringPredicate, z.union([stringPredicate, numberPredicate])]).array();
|
||||
const booleanPredicate = z.boolean();
|
||||
const stringPredicate = s.string.lengthGe(1).lengthLe(100);
|
||||
const numberPredicate = s.number.gt(-Infinity).lt(Infinity);
|
||||
const choicesPredicate = s.object({ name: stringPredicate, value: s.union(stringPredicate, numberPredicate) }).array;
|
||||
const booleanPredicate = s.boolean;
|
||||
|
||||
export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends string | number> {
|
||||
public readonly choices?: APIApplicationCommandOptionChoice<T>[];
|
||||
@@ -14,59 +14,39 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
|
||||
// Since this is present and this is a mixin, this is needed
|
||||
public readonly type!: ApplicationCommandOptionType;
|
||||
|
||||
/**
|
||||
* Adds a choice for this option
|
||||
*
|
||||
* @param name The name of the choice
|
||||
* @param value The value of the choice
|
||||
*/
|
||||
public addChoice(name: string, value: T): Omit<this, 'setAutocomplete'> {
|
||||
if (this.autocomplete) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
if (this.choices === undefined) {
|
||||
Reflect.set(this, 'choices', []);
|
||||
}
|
||||
|
||||
validateMaxChoicesLength(this.choices!);
|
||||
|
||||
// Validate name
|
||||
stringPredicate.parse(name);
|
||||
|
||||
// Validate the value
|
||||
if (this.type === ApplicationCommandOptionType.String) {
|
||||
stringPredicate.parse(value);
|
||||
} else {
|
||||
numberPredicate.parse(value);
|
||||
}
|
||||
|
||||
this.choices!.push({ name, value });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple choices for this option
|
||||
*
|
||||
* @param choices The choices to add
|
||||
*/
|
||||
public addChoices(choices: [name: string, value: T][]): Omit<this, 'setAutocomplete'> {
|
||||
if (this.autocomplete) {
|
||||
public addChoices(...choices: APIApplicationCommandOptionChoice<T>[]): this {
|
||||
if (choices.length > 0 && this.autocomplete) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
choicesPredicate.parse(choices);
|
||||
|
||||
for (const [label, value] of choices) this.addChoice(label, value);
|
||||
if (this.choices === undefined) {
|
||||
Reflect.set(this, 'choices', []);
|
||||
}
|
||||
|
||||
validateChoicesLength(choices.length, this.choices);
|
||||
|
||||
for (const { name, value } of choices) {
|
||||
// Validate the value
|
||||
if (this.type === ApplicationCommandOptionType.String) {
|
||||
stringPredicate.parse(value);
|
||||
} else {
|
||||
numberPredicate.parse(value);
|
||||
}
|
||||
|
||||
this.choices!.push({ name, value });
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public setChoices<Input extends [name: string, value: T][]>(
|
||||
choices: Input,
|
||||
): Input extends []
|
||||
? this & Pick<ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T>, 'setAutocomplete'>
|
||||
: Omit<this, 'setAutocomplete'> {
|
||||
public setChoices<Input extends APIApplicationCommandOptionChoice<T>[]>(...choices: Input): this {
|
||||
if (choices.length > 0 && this.autocomplete) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
@@ -74,7 +54,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
|
||||
choicesPredicate.parse(choices);
|
||||
|
||||
Reflect.set(this, 'choices', []);
|
||||
for (const [label, value] of choices) this.addChoice(label, value);
|
||||
this.addChoices(...choices);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -83,11 +63,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
|
||||
* Marks the option as autocompletable
|
||||
* @param autocomplete If this option should be autocompletable
|
||||
*/
|
||||
public setAutocomplete<U extends boolean>(
|
||||
autocomplete: U,
|
||||
): U extends true
|
||||
? Omit<this, 'addChoice' | 'addChoices'>
|
||||
: this & Pick<ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T>, 'addChoice' | 'addChoices'> {
|
||||
public setAutocomplete(autocomplete: boolean): this {
|
||||
// Assert that you actually passed a boolean
|
||||
booleanPredicate.parse(autocomplete);
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { validateDescription, validateName } from '../Assertions';
|
||||
import type { LocaleString, LocalizationMap } from 'discord-api-types/v10';
|
||||
import { validateDescription, validateLocale, validateName } from '../Assertions';
|
||||
|
||||
export class SharedNameAndDescription {
|
||||
public readonly name!: string;
|
||||
public readonly name_localizations?: LocalizationMap;
|
||||
public readonly description!: string;
|
||||
public readonly description_localizations?: LocalizationMap;
|
||||
|
||||
/**
|
||||
* Sets the name
|
||||
@@ -31,4 +34,85 @@ export class SharedNameAndDescription {
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a name localization
|
||||
*
|
||||
* @param locale The locale to set a description for
|
||||
* @param localizedName The localized description for the given locale
|
||||
*/
|
||||
public setNameLocalization(locale: LocaleString, localizedName: string | null) {
|
||||
if (!this.name_localizations) {
|
||||
Reflect.set(this, 'name_localizations', {});
|
||||
}
|
||||
|
||||
if (localizedName === null) {
|
||||
this.name_localizations![locale] = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
validateName(localizedName);
|
||||
|
||||
this.name_localizations![validateLocale(locale)] = localizedName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name localizations
|
||||
*
|
||||
* @param localizedNames The dictionary of localized descriptions to set
|
||||
*/
|
||||
public setNameLocalizations(localizedNames: LocalizationMap | null) {
|
||||
if (localizedNames === null) {
|
||||
Reflect.set(this, 'name_localizations', null);
|
||||
return this;
|
||||
}
|
||||
|
||||
Reflect.set(this, 'name_localizations', {});
|
||||
|
||||
Object.entries(localizedNames).forEach((args) =>
|
||||
this.setNameLocalization(...(args as [LocaleString, string | null])),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a description localization
|
||||
*
|
||||
* @param locale The locale to set a description for
|
||||
* @param localizedDescription The localized description for the given locale
|
||||
*/
|
||||
public setDescriptionLocalization(locale: LocaleString, localizedDescription: string | null) {
|
||||
if (!this.description_localizations) {
|
||||
Reflect.set(this, 'description_localizations', {});
|
||||
}
|
||||
|
||||
if (localizedDescription === null) {
|
||||
this.description_localizations![locale] = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
validateDescription(localizedDescription);
|
||||
|
||||
this.description_localizations![validateLocale(locale)] = localizedDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description localizations
|
||||
*
|
||||
* @param localizedDescriptions The dictionary of localized descriptions to set
|
||||
*/
|
||||
public setDescriptionLocalizations(localizedDescriptions: LocalizationMap | null) {
|
||||
if (localizedDescriptions === null) {
|
||||
Reflect.set(this, 'description_localizations', null);
|
||||
return this;
|
||||
}
|
||||
|
||||
Reflect.set(this, 'description_localizations', {});
|
||||
Object.entries(localizedDescriptions).forEach((args) =>
|
||||
this.setDescriptionLocalization(...(args as [LocaleString, string | null])),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions';
|
||||
import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase';
|
||||
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions';
|
||||
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder';
|
||||
import { SlashCommandAttachmentOption } from '../options/attachment';
|
||||
import { SlashCommandBooleanOption } from '../options/boolean';
|
||||
import { SlashCommandChannelOption } from '../options/channel';
|
||||
import { SlashCommandIntegerOption } from '../options/integer';
|
||||
@@ -8,7 +10,6 @@ import { SlashCommandNumberOption } from '../options/number';
|
||||
import { SlashCommandRoleOption } from '../options/role';
|
||||
import { SlashCommandStringOption } from '../options/string';
|
||||
import { SlashCommandUserOption } from '../options/user';
|
||||
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder';
|
||||
|
||||
export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
public readonly options!: ToAPIApplicationCommandOptions[];
|
||||
@@ -53,6 +54,17 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandRoleOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an attachment option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addAttachmentOption(
|
||||
input: SlashCommandAttachmentOption | ((builder: SlashCommandAttachmentOption) => SlashCommandAttachmentOption),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandAttachmentOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mentionable option
|
||||
*
|
||||
@@ -73,13 +85,13 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
input:
|
||||
| SlashCommandStringOption
|
||||
| Omit<SlashCommandStringOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandStringOption, 'addChoice' | 'addChoices'>
|
||||
| Omit<SlashCommandStringOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandStringOption,
|
||||
) =>
|
||||
| SlashCommandStringOption
|
||||
| Omit<SlashCommandStringOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandStringOption, 'addChoice' | 'addChoices'>),
|
||||
| Omit<SlashCommandStringOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandStringOption);
|
||||
}
|
||||
@@ -93,13 +105,13 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
input:
|
||||
| SlashCommandIntegerOption
|
||||
| Omit<SlashCommandIntegerOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandIntegerOption, 'addChoice' | 'addChoices'>
|
||||
| Omit<SlashCommandIntegerOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandIntegerOption,
|
||||
) =>
|
||||
| SlashCommandIntegerOption
|
||||
| Omit<SlashCommandIntegerOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandIntegerOption, 'addChoice' | 'addChoices'>),
|
||||
| Omit<SlashCommandIntegerOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandIntegerOption);
|
||||
}
|
||||
@@ -113,13 +125,13 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
input:
|
||||
| SlashCommandNumberOption
|
||||
| Omit<SlashCommandNumberOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandNumberOption, 'addChoice' | 'addChoices'>
|
||||
| Omit<SlashCommandNumberOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandNumberOption,
|
||||
) =>
|
||||
| SlashCommandNumberOption
|
||||
| Omit<SlashCommandNumberOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandNumberOption, 'addChoice' | 'addChoices'>),
|
||||
| Omit<SlashCommandNumberOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandNumberOption);
|
||||
}
|
||||
@@ -128,8 +140,8 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
input:
|
||||
| T
|
||||
| Omit<T, 'setAutocomplete'>
|
||||
| Omit<T, 'addChoice' | 'addChoices'>
|
||||
| ((builder: T) => T | Omit<T, 'setAutocomplete'> | Omit<T, 'addChoice' | 'addChoices'>),
|
||||
| Omit<T, 'addChoices'>
|
||||
| ((builder: T) => T | Omit<T, 'setAutocomplete'> | Omit<T, 'addChoices'>),
|
||||
Instance: new () => T,
|
||||
): ShouldOmitSubcommandFunctions extends true ? Omit<this, 'addSubcommand' | 'addSubcommandGroup'> : this {
|
||||
const { options } = this;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { APIApplicationCommandAttachmentOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandAttachmentOption extends ApplicationCommandOptionBase {
|
||||
public override readonly type = ApplicationCommandOptionType.Attachment as const;
|
||||
|
||||
public toJSON(): APIApplicationCommandAttachmentOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandBooleanOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandBooleanOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandBooleanOption extends ApplicationCommandOptionBase {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandChannelOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandChannelOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { z } from 'zod';
|
||||
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
const numberValidator = z.number().int().nonnegative();
|
||||
const numberValidator = s.number.int;
|
||||
|
||||
@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
|
||||
export class SlashCommandIntegerOption
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandMentionableOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandMentionableOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandMentionableOption extends ApplicationCommandOptionBase {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { z } from 'zod';
|
||||
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
const numberValidator = z.number().nonnegative();
|
||||
const numberValidator = s.number;
|
||||
|
||||
@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
|
||||
export class SlashCommandNumberOption
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandRoleOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandRoleOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandRoleOption extends ApplicationCommandOptionBase {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandStringOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandStringOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandUserOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandUserOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandUserOption extends ApplicationCommandOptionBase {
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
import type { APIEmbedField } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||
|
||||
export const fieldNamePredicate = z.string().min(1).max(256);
|
||||
export const fieldNamePredicate = s.string.lengthGe(1).lengthLe(256);
|
||||
|
||||
export const fieldValuePredicate = z.string().min(1).max(1024);
|
||||
export const fieldValuePredicate = s.string.lengthGe(1).lengthLe(1024);
|
||||
|
||||
export const fieldInlinePredicate = z.boolean().optional();
|
||||
export const fieldInlinePredicate = s.boolean.optional;
|
||||
|
||||
export const embedFieldPredicate = z.object({
|
||||
export const embedFieldPredicate = s.object({
|
||||
name: fieldNamePredicate,
|
||||
value: fieldValuePredicate,
|
||||
inline: fieldInlinePredicate,
|
||||
});
|
||||
|
||||
export const embedFieldsArrayPredicate = embedFieldPredicate.array();
|
||||
export const embedFieldsArrayPredicate = embedFieldPredicate.array;
|
||||
|
||||
export const fieldLengthPredicate = z.number().lte(25);
|
||||
export const fieldLengthPredicate = s.number.le(25);
|
||||
|
||||
export function validateFieldLength(fields: APIEmbedField[], amountAdding: number): void {
|
||||
fieldLengthPredicate.parse(fields.length + amountAdding);
|
||||
export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void {
|
||||
fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding);
|
||||
}
|
||||
|
||||
export const authorNamePredicate = fieldNamePredicate.nullable();
|
||||
export const authorNamePredicate = fieldNamePredicate.nullable;
|
||||
|
||||
export const urlPredicate = z.string().url().nullish();
|
||||
export const imageURLPredicate = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:', 'attachment:'],
|
||||
}).nullish;
|
||||
|
||||
export const colorPredicate = z.number().gte(0).lte(0xffffff).nullable();
|
||||
export const urlPredicate = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:'],
|
||||
}).nullish;
|
||||
|
||||
export const descriptionPredicate = z.string().min(1).max(4096).nullable();
|
||||
export const RGBPredicate = s.number.int.ge(0).le(255);
|
||||
export const colorPredicate = s.number.int
|
||||
.ge(0)
|
||||
.le(0xffffff)
|
||||
.or(s.tuple([RGBPredicate, RGBPredicate, RGBPredicate])).nullable;
|
||||
|
||||
export const footerTextPredicate = z.string().min(1).max(2048).nullable();
|
||||
export const descriptionPredicate = s.string.lengthGe(1).lengthLe(4096).nullable;
|
||||
|
||||
export const timestampPredicate = z.union([z.number(), z.date()]).nullable();
|
||||
export const footerTextPredicate = s.string.lengthGe(1).lengthLe(2048).nullable;
|
||||
|
||||
export const titlePredicate = fieldNamePredicate.nullable();
|
||||
export const timestampPredicate = s.union(s.number, s.date).nullable;
|
||||
|
||||
export const titlePredicate = fieldNamePredicate.nullable;
|
||||
|
||||
@@ -1,326 +1,95 @@
|
||||
import type {
|
||||
APIEmbed,
|
||||
APIEmbedAuthor,
|
||||
APIEmbedField,
|
||||
APIEmbedFooter,
|
||||
APIEmbedImage,
|
||||
APIEmbedProvider,
|
||||
APIEmbedThumbnail,
|
||||
APIEmbedVideo,
|
||||
} from 'discord-api-types/v9';
|
||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||
import {
|
||||
authorNamePredicate,
|
||||
colorPredicate,
|
||||
descriptionPredicate,
|
||||
embedFieldsArrayPredicate,
|
||||
fieldInlinePredicate,
|
||||
fieldNamePredicate,
|
||||
fieldValuePredicate,
|
||||
footerTextPredicate,
|
||||
imageURLPredicate,
|
||||
timestampPredicate,
|
||||
titlePredicate,
|
||||
urlPredicate,
|
||||
validateFieldLength,
|
||||
} from './Assertions';
|
||||
|
||||
export interface AuthorOptions {
|
||||
name: string;
|
||||
url?: string;
|
||||
iconURL?: string;
|
||||
}
|
||||
|
||||
export interface FooterOptions {
|
||||
text: string;
|
||||
iconURL?: string;
|
||||
}
|
||||
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbedBuilder } from './UnsafeEmbed';
|
||||
|
||||
/**
|
||||
* Represents an embed in a message (image/video preview, rich embed, etc.)
|
||||
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
|
||||
*/
|
||||
export class Embed implements APIEmbed {
|
||||
/**
|
||||
* An array of fields of this embed
|
||||
*/
|
||||
public fields: APIEmbedField[];
|
||||
|
||||
/**
|
||||
* The embed title
|
||||
*/
|
||||
public title?: string;
|
||||
|
||||
/**
|
||||
* The embed description
|
||||
*/
|
||||
public description?: string;
|
||||
|
||||
/**
|
||||
* The embed url
|
||||
*/
|
||||
public url?: string;
|
||||
|
||||
/**
|
||||
* The embed color
|
||||
*/
|
||||
public color?: number;
|
||||
|
||||
/**
|
||||
* The timestamp of the embed in the ISO format
|
||||
*/
|
||||
public timestamp?: string;
|
||||
|
||||
/**
|
||||
* The embed thumbnail data
|
||||
*/
|
||||
public thumbnail?: APIEmbedThumbnail;
|
||||
|
||||
/**
|
||||
* The embed image data
|
||||
*/
|
||||
public image?: APIEmbedImage;
|
||||
|
||||
/**
|
||||
* Received video data
|
||||
*/
|
||||
public video?: APIEmbedVideo;
|
||||
|
||||
/**
|
||||
* The embed author data
|
||||
*/
|
||||
public author?: APIEmbedAuthor;
|
||||
|
||||
/**
|
||||
* Received data about the embed provider
|
||||
*/
|
||||
public provider?: APIEmbedProvider;
|
||||
|
||||
/**
|
||||
* The embed footer data
|
||||
*/
|
||||
public footer?: APIEmbedFooter;
|
||||
|
||||
public constructor(data: APIEmbed = {}) {
|
||||
this.title = data.title;
|
||||
this.description = data.description;
|
||||
this.url = data.url;
|
||||
this.color = data.color;
|
||||
this.thumbnail = data.thumbnail;
|
||||
this.image = data.image;
|
||||
this.video = data.video;
|
||||
this.author = data.author;
|
||||
this.provider = data.provider;
|
||||
this.footer = data.footer;
|
||||
this.fields = data.fields ?? [];
|
||||
|
||||
if (data.timestamp) this.timestamp = new Date(data.timestamp).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The accumulated length for the embed title, description, fields, footer text, and author name
|
||||
*/
|
||||
public get length(): number {
|
||||
return (
|
||||
(this.title?.length ?? 0) +
|
||||
(this.description?.length ?? 0) +
|
||||
this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) +
|
||||
(this.footer?.text.length ?? 0) +
|
||||
(this.author?.name.length ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a field to the embed (max 25)
|
||||
*
|
||||
* @param field The field to add.
|
||||
*/
|
||||
public addField(field: APIEmbedField): this {
|
||||
return this.addFields(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields to the embed (max 25)
|
||||
*
|
||||
* @param fields The fields to add
|
||||
*/
|
||||
public addFields(...fields: APIEmbedField[]): this {
|
||||
// Data assertions
|
||||
embedFieldsArrayPredicate.parse(fields);
|
||||
|
||||
export class EmbedBuilder extends UnsafeEmbedBuilder {
|
||||
public override addFields(...fields: APIEmbedField[]): this {
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(this.fields, fields.length);
|
||||
validateFieldLength(fields.length, this.data.fields);
|
||||
|
||||
this.fields.push(...Embed.normalizeFields(...fields));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts fields in the embed (max 25)
|
||||
*
|
||||
* @param index The index to start at
|
||||
* @param deleteCount The number of fields to remove
|
||||
* @param fields The replacing field objects
|
||||
*/
|
||||
public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
|
||||
// Data assertions
|
||||
embedFieldsArrayPredicate.parse(fields);
|
||||
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(this.fields, fields.length - deleteCount);
|
||||
|
||||
this.fields.splice(index, deleteCount, ...Embed.normalizeFields(...fields));
|
||||
return this;
|
||||
return super.addFields(...embedFieldsArrayPredicate.parse(fields));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the author of this embed
|
||||
*
|
||||
* @param options The options for the author
|
||||
*/
|
||||
public setAuthor(options: AuthorOptions | null): this {
|
||||
public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(fields.length - deleteCount, this.data.fields);
|
||||
|
||||
// Data assertions
|
||||
return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields));
|
||||
}
|
||||
|
||||
public override setAuthor(options: EmbedAuthorOptions | null): this {
|
||||
if (options === null) {
|
||||
this.author = undefined;
|
||||
return this;
|
||||
return super.setAuthor(null);
|
||||
}
|
||||
|
||||
const { name, iconURL, url } = options;
|
||||
// Data assertions
|
||||
authorNamePredicate.parse(name);
|
||||
urlPredicate.parse(iconURL);
|
||||
urlPredicate.parse(url);
|
||||
authorNamePredicate.parse(options.name);
|
||||
urlPredicate.parse(options.iconURL);
|
||||
urlPredicate.parse(options.url);
|
||||
|
||||
this.author = { name, url, icon_url: iconURL };
|
||||
return this;
|
||||
return super.setAuthor(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of this embed
|
||||
*
|
||||
* @param color The color of the embed
|
||||
*/
|
||||
public setColor(color: number | null): this {
|
||||
public override setColor(color: number | RGBTuple | null): this {
|
||||
// Data assertions
|
||||
colorPredicate.parse(color);
|
||||
|
||||
this.color = color ?? undefined;
|
||||
return this;
|
||||
return super.setColor(colorPredicate.parse(color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this embed
|
||||
*
|
||||
* @param description The description
|
||||
*/
|
||||
public setDescription(description: string | null): this {
|
||||
public override setDescription(description: string | null): this {
|
||||
// Data assertions
|
||||
descriptionPredicate.parse(description);
|
||||
|
||||
this.description = description ?? undefined;
|
||||
return this;
|
||||
return super.setDescription(descriptionPredicate.parse(description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the footer of this embed
|
||||
*
|
||||
* @param options The options for the footer
|
||||
*/
|
||||
public setFooter(options: FooterOptions | null): this {
|
||||
public override setFooter(options: EmbedFooterOptions | null): this {
|
||||
if (options === null) {
|
||||
this.footer = undefined;
|
||||
return this;
|
||||
return super.setFooter(null);
|
||||
}
|
||||
|
||||
const { text, iconURL } = options;
|
||||
// Data assertions
|
||||
footerTextPredicate.parse(text);
|
||||
urlPredicate.parse(iconURL);
|
||||
footerTextPredicate.parse(options.text);
|
||||
urlPredicate.parse(options.iconURL);
|
||||
|
||||
this.footer = { text, icon_url: iconURL };
|
||||
return this;
|
||||
return super.setFooter(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image of this embed
|
||||
*
|
||||
* @param url The URL of the image
|
||||
*/
|
||||
public setImage(url: string | null): this {
|
||||
public override setImage(url: string | null): this {
|
||||
// Data assertions
|
||||
urlPredicate.parse(url);
|
||||
|
||||
this.image = url ? { url } : undefined;
|
||||
return this;
|
||||
return super.setImage(imageURLPredicate.parse(url)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the thumbnail of this embed
|
||||
*
|
||||
* @param url The URL of the thumbnail
|
||||
*/
|
||||
public setThumbnail(url: string | null): this {
|
||||
public override setThumbnail(url: string | null): this {
|
||||
// Data assertions
|
||||
urlPredicate.parse(url);
|
||||
|
||||
this.thumbnail = url ? { url } : undefined;
|
||||
return this;
|
||||
return super.setThumbnail(imageURLPredicate.parse(url)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timestamp of this embed
|
||||
*
|
||||
* @param timestamp The timestamp or date
|
||||
*/
|
||||
public setTimestamp(timestamp: number | Date | null = Date.now()): this {
|
||||
public override setTimestamp(timestamp: number | Date | null = Date.now()): this {
|
||||
// Data assertions
|
||||
timestampPredicate.parse(timestamp);
|
||||
|
||||
this.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined;
|
||||
return this;
|
||||
return super.setTimestamp(timestampPredicate.parse(timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of this embed
|
||||
*
|
||||
* @param title The title
|
||||
*/
|
||||
public setTitle(title: string | null): this {
|
||||
public override setTitle(title: string | null): this {
|
||||
// Data assertions
|
||||
titlePredicate.parse(title);
|
||||
|
||||
this.title = title ?? undefined;
|
||||
return this;
|
||||
return super.setTitle(titlePredicate.parse(title));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL of this embed
|
||||
*
|
||||
* @param url The URL
|
||||
*/
|
||||
public setURL(url: string | null): this {
|
||||
public override setURL(url: string | null): this {
|
||||
// Data assertions
|
||||
urlPredicate.parse(url);
|
||||
|
||||
this.url = url ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the embed to a plain object
|
||||
*/
|
||||
public toJSON(): APIEmbed {
|
||||
return { ...this };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes field input and resolves strings
|
||||
*
|
||||
* @param fields Fields to normalize
|
||||
*/
|
||||
public static normalizeFields(...fields: APIEmbedField[]): APIEmbedField[] {
|
||||
return fields.flat(Infinity).map((field) => {
|
||||
fieldNamePredicate.parse(field.name);
|
||||
fieldValuePredicate.parse(field.value);
|
||||
fieldInlinePredicate.parse(field.inline);
|
||||
|
||||
return { name: field.name, value: field.value, inline: field.inline ?? undefined };
|
||||
});
|
||||
return super.setURL(urlPredicate.parse(url)!);
|
||||
}
|
||||
}
|
||||
|
||||
186
packages/builders/src/messages/embed/UnsafeEmbed.ts
Normal file
186
packages/builders/src/messages/embed/UnsafeEmbed.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v10';
|
||||
|
||||
export type RGBTuple = [red: number, green: number, blue: number];
|
||||
|
||||
export interface IconData {
|
||||
/**
|
||||
* The URL of the icon
|
||||
*/
|
||||
iconURL?: string;
|
||||
/**
|
||||
* The proxy URL of the icon
|
||||
*/
|
||||
proxyIconURL?: string;
|
||||
}
|
||||
|
||||
export type EmbedAuthorData = Omit<APIEmbedAuthor, 'icon_url' | 'proxy_icon_url'> & IconData;
|
||||
|
||||
export type EmbedAuthorOptions = Omit<EmbedAuthorData, 'proxyIconURL'>;
|
||||
|
||||
export type EmbedFooterData = Omit<APIEmbedFooter, 'icon_url' | 'proxy_icon_url'> & IconData;
|
||||
|
||||
export type EmbedFooterOptions = Omit<EmbedFooterData, 'proxyIconURL'>;
|
||||
|
||||
export interface EmbedImageData extends Omit<APIEmbedImage, 'proxy_url'> {
|
||||
/**
|
||||
* The proxy URL for the image
|
||||
*/
|
||||
proxyURL?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a non-validated embed in a message (image/video preview, rich embed, etc.)
|
||||
*/
|
||||
export class UnsafeEmbedBuilder {
|
||||
public readonly data: APIEmbed;
|
||||
|
||||
public constructor(data: APIEmbed = {}) {
|
||||
this.data = { ...data };
|
||||
if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields to the embed (max 25)
|
||||
*
|
||||
* @param fields The fields to add
|
||||
*/
|
||||
public addFields(...fields: APIEmbedField[]): this {
|
||||
if (this.data.fields) this.data.fields.push(...fields);
|
||||
else this.data.fields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts fields in the embed (max 25)
|
||||
*
|
||||
* @param index The index to start at
|
||||
* @param deleteCount The number of fields to remove
|
||||
* @param fields The replacing field objects
|
||||
*/
|
||||
public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
|
||||
if (this.data.fields) this.data.fields.splice(index, deleteCount, ...fields);
|
||||
else this.data.fields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the author of this embed
|
||||
*
|
||||
* @param options The options for the author
|
||||
*/
|
||||
public setAuthor(options: EmbedAuthorOptions | null): this {
|
||||
if (options === null) {
|
||||
this.data.author = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.author = { name: options.name, url: options.url, icon_url: options.iconURL };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of this embed
|
||||
*
|
||||
* @param color The color of the embed
|
||||
*/
|
||||
public setColor(color: number | RGBTuple | null): this {
|
||||
if (Array.isArray(color)) {
|
||||
const [red, green, blue] = color;
|
||||
this.data.color = (red << 16) + (green << 8) + blue;
|
||||
return this;
|
||||
}
|
||||
this.data.color = color ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this embed
|
||||
*
|
||||
* @param description The description
|
||||
*/
|
||||
public setDescription(description: string | null): this {
|
||||
this.data.description = description ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the footer of this embed
|
||||
*
|
||||
* @param options The options for the footer
|
||||
*/
|
||||
public setFooter(options: EmbedFooterOptions | null): this {
|
||||
if (options === null) {
|
||||
this.data.footer = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.footer = { text: options.text, icon_url: options.iconURL };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image of this embed
|
||||
*
|
||||
* @param url The URL of the image
|
||||
*/
|
||||
public setImage(url: string | null): this {
|
||||
this.data.image = url ? { url } : undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the thumbnail of this embed
|
||||
*
|
||||
* @param url The URL of the thumbnail
|
||||
*/
|
||||
public setThumbnail(url: string | null): this {
|
||||
this.data.thumbnail = url ? { url } : undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timestamp of this embed
|
||||
*
|
||||
* @param timestamp The timestamp or date
|
||||
*/
|
||||
public setTimestamp(timestamp: number | Date | null = Date.now()): this {
|
||||
this.data.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of this embed
|
||||
*
|
||||
* @param title The title
|
||||
*/
|
||||
public setTitle(title: string | null): this {
|
||||
this.data.title = title ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL of this embed
|
||||
*
|
||||
* @param url The URL
|
||||
*/
|
||||
public setURL(url: string | null): this {
|
||||
this.data.url = url ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the embed to a plain object
|
||||
*/
|
||||
public toJSON(): APIEmbed {
|
||||
return { ...this.data };
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Snowflake } from 'discord-api-types/globals';
|
||||
import type { URL } from 'url';
|
||||
import type { Snowflake } from 'discord-api-types/globals';
|
||||
|
||||
/**
|
||||
* Wraps the content inside a codeblock with no language
|
||||
@@ -164,15 +164,6 @@ export function userMention<C extends Snowflake>(userId: C): `<@${C}>` {
|
||||
return `<@${userId}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a user ID into a member-nickname mention
|
||||
*
|
||||
* @param memberId The user ID to format
|
||||
*/
|
||||
export function memberNicknameMention<C extends Snowflake>(memberId: C): `<@!${C}>` {
|
||||
return `<@!${memberId}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a channel ID into a channel mention
|
||||
*
|
||||
|
||||
11
packages/builders/src/util/componentUtil.ts
Normal file
11
packages/builders/src/util/componentUtil.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { APIEmbed } from 'discord-api-types/v10';
|
||||
|
||||
export function embedLength(data: APIEmbed) {
|
||||
return (
|
||||
(data.title?.length ?? 0) +
|
||||
(data.description?.length ?? 0) +
|
||||
(data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) +
|
||||
(data.footer?.text.length ?? 0) +
|
||||
(data.author?.name.length ?? 0)
|
||||
);
|
||||
}
|
||||
14
packages/builders/src/util/equatable.ts
Normal file
14
packages/builders/src/util/equatable.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface Equatable<T> {
|
||||
/**
|
||||
* Whether or not this is equal to another structure
|
||||
*/
|
||||
equals: (other: T) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if an object is equatable or not.
|
||||
* @param maybeEquatable The object to check against
|
||||
*/
|
||||
export function isEquatable(maybeEquatable: unknown): maybeEquatable is Equatable<unknown> {
|
||||
return maybeEquatable !== null && typeof maybeEquatable === 'object' && 'equals' in maybeEquatable;
|
||||
}
|
||||
14
packages/builders/src/util/jsonEncodable.ts
Normal file
14
packages/builders/src/util/jsonEncodable.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface JSONEncodable<T> {
|
||||
/**
|
||||
* Transforms this object to its JSON format
|
||||
*/
|
||||
toJSON: () => T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if an object is encodable or not.
|
||||
* @param maybeEncodable The object to check against
|
||||
*/
|
||||
export function isJSONEncodable(maybeEncodable: unknown): maybeEncodable is JSONEncodable<unknown> {
|
||||
return maybeEncodable !== null && typeof maybeEncodable === 'object' && 'toJSON' in maybeEncodable;
|
||||
}
|
||||
@@ -5,8 +5,16 @@ export const tsup: Options = {
|
||||
dts: true,
|
||||
entryPoints: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
minify: true,
|
||||
minify: false,
|
||||
keepNames: true,
|
||||
skipNodeModulesBundle: true,
|
||||
sourcemap: true,
|
||||
target: 'es2021',
|
||||
esbuildOptions: (options, context) => {
|
||||
if (context.format === 'cjs') {
|
||||
options.banner = {
|
||||
js: '"use strict";',
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
# [0.6.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@0.5.0...@discordjs/collection@0.6.0) (2022-04-17)
|
||||
|
||||
## Features
|
||||
|
||||
- Add support for module: NodeNext in TS and ESM (#7598) ([8f1986a](https://github.com/discordjs/discord.js/commit/8f1986a6aa98365e09b00e84ad5f9f354ab61f3d))
|
||||
- **builders:** Add attachment command option type (#7203) ([ae0f35f](https://github.com/discordjs/discord.js/commit/ae0f35f51d68dfa5a7dc43d161ef9365171debdb))
|
||||
- **Collection:** Add merging functions (#7299) ([e4bd07b](https://github.com/discordjs/discord.js/commit/e4bd07b2394f227ea06b72eb6999de9ab3127b25))
|
||||
|
||||
## Refactor
|
||||
|
||||
- Make `intersect` perform a true intersection (#7211) ([d8efba2](https://github.com/discordjs/discord.js/commit/d8efba24e09aa2a8dbf028fc57a561a56e7833fd))
|
||||
|
||||
## Typings
|
||||
|
||||
- Add `ReadonlyCollection` (#7245) ([db25f52](https://github.com/discordjs/discord.js/commit/db25f529b26d7c819c1c42ad3e26c2263ea2da0e))
|
||||
- **Collection:** Union types on `intersect` and `difference` (#7196) ([1f9b922](https://github.com/discordjs/discord.js/commit/1f9b9225f2066e9cc66c3355417139fd25cc403c))
|
||||
|
||||
# [0.5.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@0.4.0...@discordjs/collection@0.5.0) (2021-12-08)
|
||||
|
||||
## Refactor
|
||||
|
||||
@@ -460,3 +460,95 @@ describe('ensure() tests', () => {
|
||||
expect(coll.size).toStrictEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge() tests', () => {
|
||||
const cL = new Collection([
|
||||
['L', 1],
|
||||
['LR', 2],
|
||||
]);
|
||||
const cR = new Collection([
|
||||
['R', 3],
|
||||
['LR', 4],
|
||||
]);
|
||||
|
||||
test('merges two collection, with all keys together', () => {
|
||||
const c = cL.merge(
|
||||
cR,
|
||||
(x) => ({ keep: true, value: `L${x}` }),
|
||||
(y) => ({ keep: true, value: `R${y}` }),
|
||||
(x, y) => ({ keep: true, value: `LR${x},${y}` }),
|
||||
);
|
||||
expect(c.get('L')).toStrictEqual('L1');
|
||||
expect(c.get('R')).toStrictEqual('R3');
|
||||
expect(c.get('LR')).toStrictEqual('LR2,4');
|
||||
expect(c.size).toStrictEqual(3);
|
||||
});
|
||||
|
||||
test('merges two collection, removing left entries', () => {
|
||||
const c = cL.merge(
|
||||
cR,
|
||||
() => ({ keep: false }),
|
||||
(y) => ({ keep: true, value: `R${y}` }),
|
||||
(x, y) => ({ keep: true, value: `LR${x},${y}` }),
|
||||
);
|
||||
expect(c.get('R')).toStrictEqual('R3');
|
||||
expect(c.get('LR')).toStrictEqual('LR2,4');
|
||||
expect(c.size).toStrictEqual(2);
|
||||
});
|
||||
|
||||
test('merges two collection, removing right entries', () => {
|
||||
const c = cL.merge(
|
||||
cR,
|
||||
(x) => ({ keep: true, value: `L${x}` }),
|
||||
() => ({ keep: false }),
|
||||
(x, y) => ({ keep: true, value: `LR${x},${y}` }),
|
||||
);
|
||||
expect(c.get('L')).toStrictEqual('L1');
|
||||
expect(c.get('LR')).toStrictEqual('LR2,4');
|
||||
expect(c.size).toStrictEqual(2);
|
||||
});
|
||||
|
||||
test('merges two collection, removing in-both entries', () => {
|
||||
const c = cL.merge(
|
||||
cR,
|
||||
(x) => ({ keep: true, value: `L${x}` }),
|
||||
(y) => ({ keep: true, value: `R${y}` }),
|
||||
() => ({ keep: false }),
|
||||
);
|
||||
expect(c.get('L')).toStrictEqual('L1');
|
||||
expect(c.get('R')).toStrictEqual('R3');
|
||||
expect(c.size).toStrictEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combineEntries() tests', () => {
|
||||
test('it adds entries together', () => {
|
||||
const c = Collection.combineEntries(
|
||||
[
|
||||
['a', 1],
|
||||
['b', 2],
|
||||
['a', 2],
|
||||
],
|
||||
(x, y) => x + y,
|
||||
);
|
||||
expect([...c]).toStrictEqual([
|
||||
['a', 3],
|
||||
['b', 2],
|
||||
]);
|
||||
});
|
||||
|
||||
test('it really goes through all the entries', () => {
|
||||
const c = Collection.combineEntries(
|
||||
[
|
||||
['a', [1]],
|
||||
['b', [2]],
|
||||
['a', [2]],
|
||||
],
|
||||
(x, y) => x.concat(y),
|
||||
);
|
||||
expect([...c]).toStrictEqual([
|
||||
['a', [1, 2]],
|
||||
['b', [2]],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@discordjs/collection",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0-dev",
|
||||
"description": "Utility data structure used in discord.js",
|
||||
"scripts": {
|
||||
"test": "jest --pass-with-no-tests",
|
||||
@@ -16,7 +16,8 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "src",
|
||||
@@ -47,24 +48,23 @@
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.12",
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-typescript": "^7.16.5",
|
||||
"@discordjs/ts-docgen": "^0.3.4",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||
"@typescript-eslint/parser": "^5.10.0",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-marine": "^9.3.2",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.4.7",
|
||||
"prettier": "^2.5.1",
|
||||
"standard-version": "^9.3.2",
|
||||
"tsup": "^5.11.11",
|
||||
"typedoc": "^0.22.11",
|
||||
"typescript": "^4.5.5"
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@discordjs/ts-docgen": "^0.4.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^16.11.27",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-marine": "^9.4.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"tsup": "^5.12.5",
|
||||
"typedoc": "^0.22.15",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
|
||||
@@ -666,6 +666,57 @@ export class Collection<K, V> extends Map<K, V> {
|
||||
return coll;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two Collections together into a new Collection.
|
||||
* @param other The other Collection to merge with
|
||||
* @param whenInSelf Function getting the result if the entry only exists in this Collection
|
||||
* @param whenInOther Function getting the result if the entry only exists in the other Collection
|
||||
* @param whenInBoth Function getting the result if the entry exists in both Collections
|
||||
*
|
||||
* @example
|
||||
* // Sums up the entries in two collections.
|
||||
* coll.merge(
|
||||
* other,
|
||||
* x => ({ keep: true, value: x }),
|
||||
* y => ({ keep: true, value: y }),
|
||||
* (x, y) => ({ keep: true, value: x + y }),
|
||||
* );
|
||||
*
|
||||
* @example
|
||||
* // Intersects two collections in a left-biased manner.
|
||||
* coll.merge(
|
||||
* other,
|
||||
* x => ({ keep: false }),
|
||||
* y => ({ keep: false }),
|
||||
* (x, _) => ({ keep: true, value: x }),
|
||||
* );
|
||||
*/
|
||||
public merge<T, R>(
|
||||
other: ReadonlyCollection<K, T>,
|
||||
whenInSelf: (value: V, key: K) => Keep<R>,
|
||||
whenInOther: (valueOther: T, key: K) => Keep<R>,
|
||||
whenInBoth: (value: V, valueOther: T, key: K) => Keep<R>,
|
||||
): Collection<K, R> {
|
||||
const coll = new this.constructor[Symbol.species]<K, R>();
|
||||
const keys = new Set([...this.keys(), ...other.keys()]);
|
||||
for (const k of keys) {
|
||||
const hasInSelf = this.has(k);
|
||||
const hasInOther = other.has(k);
|
||||
|
||||
if (hasInSelf && hasInOther) {
|
||||
const r = whenInBoth(this.get(k)!, other.get(k)!, k);
|
||||
if (r.keep) coll.set(k, r.value);
|
||||
} else if (hasInSelf) {
|
||||
const r = whenInSelf(this.get(k)!, k);
|
||||
if (r.keep) coll.set(k, r.value);
|
||||
} else if (hasInOther) {
|
||||
const r = whenInOther(other.get(k)!, k);
|
||||
if (r.keep) coll.set(k, r.value);
|
||||
}
|
||||
}
|
||||
return coll;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sorted method sorts the items of a collection and returns it.
|
||||
* The sort is not necessarily stable in Node 10 or older.
|
||||
@@ -690,8 +741,37 @@ export class Collection<K, V> extends Map<K, V> {
|
||||
private static defaultSort<V>(firstValue: V, secondValue: V): number {
|
||||
return Number(firstValue > secondValue) || Number(firstValue === secondValue) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Collection from a list of entries.
|
||||
* @param entries The list of entries
|
||||
* @param combine Function to combine an existing entry with a new one
|
||||
*
|
||||
* @example
|
||||
* Collection.combineEntries([["a", 1], ["b", 2], ["a", 2]], (x, y) => x + y);
|
||||
* // returns Collection { "a" => 3, "b" => 2 }
|
||||
*/
|
||||
public static combineEntries<K, V>(
|
||||
entries: Iterable<[K, V]>,
|
||||
combine: (firstValue: V, secondValue: V, key: K) => V,
|
||||
): Collection<K, V> {
|
||||
const coll = new Collection<K, V>();
|
||||
for (const [k, v] of entries) {
|
||||
if (coll.has(k)) {
|
||||
coll.set(k, combine(coll.get(k)!, v, k));
|
||||
} else {
|
||||
coll.set(k, v);
|
||||
}
|
||||
}
|
||||
return coll;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type Keep<V> = { keep: true; value: V } | { keep: false };
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
||||
@@ -5,10 +5,17 @@ export const tsup: Options = {
|
||||
dts: true,
|
||||
entryPoints: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
minify: true,
|
||||
minify: false,
|
||||
// if false: causes Collection.constructor to be a minified value like: 'o'
|
||||
keepNames: true,
|
||||
skipNodeModulesBundle: true,
|
||||
sourcemap: true,
|
||||
target: 'es2021',
|
||||
esbuildOptions: (options, context) => {
|
||||
if (context.format === 'cjs') {
|
||||
options.banner = {
|
||||
js: '"use strict";',
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
||||
"plugins": ["import"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2021
|
||||
"ecmaVersion": 2022
|
||||
},
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"es2022": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
|
||||
@@ -54,7 +54,7 @@ Register a slash command against the Discord API:
|
||||
|
||||
```js
|
||||
const { REST } = require('@discordjs/rest');
|
||||
const { Routes } = require('discord-api-types/v9');
|
||||
const { Routes } = require('discord-api-types/v10');
|
||||
|
||||
const commands = [
|
||||
{
|
||||
@@ -63,7 +63,7 @@ const commands = [
|
||||
},
|
||||
];
|
||||
|
||||
const rest = new REST({ version: '9' }).setToken('token');
|
||||
const rest = new REST({ version: '10' }).setToken('token');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@@ -81,8 +81,8 @@ const rest = new REST({ version: '9' }).setToken('token');
|
||||
Afterwards we can create a quite simple example bot:
|
||||
|
||||
```js
|
||||
const { Client, Intents } = require('discord.js');
|
||||
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
|
||||
const { Client, GatewayIntentBits } = require('discord.js');
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"test:typescript": "tsc --noEmit && tsd",
|
||||
"lint": "prettier --check . && eslint src && tslint typings/index.d.ts",
|
||||
"format": "prettier --write . && eslint src --fix",
|
||||
"docs": "docgen --source ./src --custom ./docs/index.yml --output ./docs/docs.json",
|
||||
"docs:test": "docgen --source ./src --custom ./docs/index.yml",
|
||||
"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/*'"
|
||||
},
|
||||
@@ -47,32 +47,33 @@
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^0.11.0",
|
||||
"@discordjs/collection": "^0.4.0",
|
||||
"@sapphire/async-queue": "^1.1.9",
|
||||
"@sapphire/snowflake": "^3.0.1",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/ws": "^8.2.2",
|
||||
"discord-api-types": "^0.26.1",
|
||||
"form-data": "^4.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ws": "^8.4.2"
|
||||
"@discordjs/builders": "workspace:^",
|
||||
"@discordjs/collection": "workspace:^",
|
||||
"@discordjs/rest": "workspace:^",
|
||||
"@sapphire/snowflake": "^3.2.1",
|
||||
"@types/ws": "^8.5.3",
|
||||
"discord-api-types": "^0.31.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"tslib": "^2.3.1",
|
||||
"undici": "^4.16.0",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/docgen": "^0.11.0",
|
||||
"@types/node": "^16.11.12",
|
||||
"@discordjs/docgen": "^0.11.1",
|
||||
"@types/node": "^16.11.27",
|
||||
"dtslint": "^4.2.1",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^27.4.7",
|
||||
"prettier": "^2.5.1",
|
||||
"tsd": "^0.19.0",
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"tsd": "^0.20.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.5.5"
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { readdir, writeFile } from 'node:fs/promises';
|
||||
import { Constants } from '../src/index.js';
|
||||
import { GatewayDispatchEvents } from '../src/index.js';
|
||||
|
||||
async function writeWebsocketHandlerImports() {
|
||||
const lines = ["'use strict';\n", 'const handlers = Object.fromEntries(['];
|
||||
|
||||
for (const name of Object.keys(Constants.WSEvents)) {
|
||||
for (const name of Object.values(GatewayDispatchEvents)) {
|
||||
lines.push(` ['${name}', require('./${name}')],`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('node:events');
|
||||
const { clearInterval } = require('node:timers');
|
||||
const { REST } = require('@discordjs/rest');
|
||||
const { TypeError } = require('../errors');
|
||||
const RESTManager = require('../rest/RESTManager');
|
||||
const Options = require('../util/Options');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
@@ -27,20 +26,9 @@ class BaseClient extends EventEmitter {
|
||||
|
||||
/**
|
||||
* The REST manager of the client
|
||||
* @type {RESTManager}
|
||||
* @private
|
||||
* @type {REST}
|
||||
*/
|
||||
this.rest = new RESTManager(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* API shortcut
|
||||
* @type {Object}
|
||||
* @readonly
|
||||
* @private
|
||||
*/
|
||||
get api() {
|
||||
return this.rest.api;
|
||||
this.rest = new REST(this.options.rest);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,7 +36,8 @@ class BaseClient extends EventEmitter {
|
||||
* @returns {void}
|
||||
*/
|
||||
destroy() {
|
||||
if (this.rest.sweepInterval) clearInterval(this.rest.sweepInterval);
|
||||
this.rest.requestManager.clearHashSweeper();
|
||||
this.rest.requestManager.clearHandlerSweeper();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +70,6 @@ class BaseClient extends EventEmitter {
|
||||
module.exports = BaseClient;
|
||||
|
||||
/**
|
||||
* Emitted for general debugging information.
|
||||
* @event BaseClient#debug
|
||||
* @param {string} info The debug information
|
||||
* @external REST
|
||||
* @see {@link https://discord.js.org/#/docs/rest/main/class/REST}
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
const process = require('node:process');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { OAuth2Scopes, Routes } = require('discord-api-types/v10');
|
||||
const BaseClient = require('./BaseClient');
|
||||
const ActionsManager = require('./actions/ActionsManager');
|
||||
const ClientVoiceManager = require('./voice/ClientVoiceManager');
|
||||
@@ -21,11 +23,12 @@ const StickerPack = require('../structures/StickerPack');
|
||||
const VoiceRegion = require('../structures/VoiceRegion');
|
||||
const Webhook = require('../structures/Webhook');
|
||||
const Widget = require('../structures/Widget');
|
||||
const { Events, InviteScopes, Status } = require('../util/Constants');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const Intents = require('../util/Intents');
|
||||
const Events = require('../util/Events');
|
||||
const IntentsBitField = require('../util/IntentsBitField');
|
||||
const Options = require('../util/Options');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const PermissionsBitField = require('../util/PermissionsBitField');
|
||||
const Status = require('../util/Status');
|
||||
const Sweepers = require('../util/Sweepers');
|
||||
|
||||
/**
|
||||
@@ -210,8 +213,9 @@ class Client extends BaseClient {
|
||||
async login(token = this.token) {
|
||||
if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
|
||||
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
|
||||
this.rest.setToken(token);
|
||||
this.emit(
|
||||
Events.DEBUG,
|
||||
Events.Debug,
|
||||
`Provided token: ${token
|
||||
.split('.')
|
||||
.map((val, i) => (i > 1 ? val.replace(/./g, '*') : val))
|
||||
@@ -222,7 +226,7 @@ class Client extends BaseClient {
|
||||
this.options.ws.presence = this.presence._parse(this.options.presence);
|
||||
}
|
||||
|
||||
this.emit(Events.DEBUG, 'Preparing to connect to the gateway...');
|
||||
this.emit(Events.Debug, 'Preparing to connect to the gateway...');
|
||||
|
||||
try {
|
||||
await this.ws.connect();
|
||||
@@ -239,7 +243,7 @@ class Client extends BaseClient {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReady() {
|
||||
return this.ws.status === Status.READY;
|
||||
return this.ws.status === Status.Ready;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,6 +256,7 @@ class Client extends BaseClient {
|
||||
this.sweepers.destroy();
|
||||
this.ws.destroy();
|
||||
this.token = null;
|
||||
this.rest.setToken(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,9 +278,12 @@ class Client extends BaseClient {
|
||||
*/
|
||||
async fetchInvite(invite, options) {
|
||||
const code = DataResolver.resolveInviteCode(invite);
|
||||
const data = await this.api.invites(code).get({
|
||||
query: { with_counts: true, with_expiration: true, guild_scheduled_event_id: options?.guildScheduledEventId },
|
||||
const query = makeURLSearchParams({
|
||||
with_counts: true,
|
||||
with_expiration: true,
|
||||
guild_scheduled_event_id: options?.guildScheduledEventId,
|
||||
});
|
||||
const data = await this.rest.get(Routes.invite(code), { query });
|
||||
return new Invite(this, data);
|
||||
}
|
||||
|
||||
@@ -290,7 +298,7 @@ class Client extends BaseClient {
|
||||
*/
|
||||
async fetchGuildTemplate(template) {
|
||||
const code = DataResolver.resolveGuildTemplateCode(template);
|
||||
const data = await this.api.guilds.templates(code).get();
|
||||
const data = await this.rest.get(Routes.template(code));
|
||||
return new GuildTemplate(this, data);
|
||||
}
|
||||
|
||||
@@ -305,7 +313,7 @@ class Client extends BaseClient {
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async fetchWebhook(id, token) {
|
||||
const data = await this.api.webhooks(id, token).get();
|
||||
const data = await this.rest.get(Routes.webhook(id, token));
|
||||
return new Webhook(this, { token, ...data });
|
||||
}
|
||||
|
||||
@@ -318,7 +326,7 @@ class Client extends BaseClient {
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async fetchVoiceRegions() {
|
||||
const apiRegions = await this.api.voice.regions.get();
|
||||
const apiRegions = await this.rest.get(Routes.voiceRegions());
|
||||
const regions = new Collection();
|
||||
for (const region of apiRegions) regions.set(region.id, new VoiceRegion(region));
|
||||
return regions;
|
||||
@@ -334,7 +342,7 @@ class Client extends BaseClient {
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async fetchSticker(id) {
|
||||
const data = await this.api.stickers(id).get();
|
||||
const data = await this.rest.get(Routes.sticker(id));
|
||||
return new Sticker(this, data);
|
||||
}
|
||||
|
||||
@@ -347,7 +355,7 @@ class Client extends BaseClient {
|
||||
* .catch(console.error);
|
||||
*/
|
||||
async fetchPremiumStickerPacks() {
|
||||
const data = await this.api('sticker-packs').get();
|
||||
const data = await this.rest.get(Routes.nitroStickerPacks());
|
||||
return new Collection(data.sticker_packs.map(p => [p.id, new StickerPack(this, p)]));
|
||||
}
|
||||
|
||||
@@ -359,7 +367,7 @@ class Client extends BaseClient {
|
||||
async fetchGuildPreview(guild) {
|
||||
const id = this.guilds.resolveId(guild);
|
||||
if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
|
||||
const data = await this.api.guilds(id).preview.get();
|
||||
const data = await this.rest.get(Routes.guildPreview(id));
|
||||
return new GuildPreview(this, data);
|
||||
}
|
||||
|
||||
@@ -371,14 +379,14 @@ class Client extends BaseClient {
|
||||
async fetchGuildWidget(guild) {
|
||||
const id = this.guilds.resolveId(guild);
|
||||
if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
|
||||
const data = await this.api.guilds(id, 'widget.json').get();
|
||||
const data = await this.rest.get(Routes.guildWidgetJSON(id));
|
||||
return new Widget(this, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link Client#generateInvite}.
|
||||
* @typedef {Object} InviteGenerationOptions
|
||||
* @property {InviteScope[]} scopes Scopes that should be requested
|
||||
* @property {OAuth2Scopes[]} scopes Scopes that should be requested
|
||||
* @property {PermissionResolvable} [permissions] Permissions to request
|
||||
* @property {GuildResolvable} [guild] Guild to preselect
|
||||
* @property {boolean} [disableGuildSelect] Whether to disable the guild selection
|
||||
@@ -390,17 +398,17 @@ class Client extends BaseClient {
|
||||
* @returns {string}
|
||||
* @example
|
||||
* const link = client.generateInvite({
|
||||
* scopes: ['applications.commands'],
|
||||
* scopes: [OAuth2Scopes.ApplicationsCommands],
|
||||
* });
|
||||
* console.log(`Generated application invite link: ${link}`);
|
||||
* @example
|
||||
* const link = client.generateInvite({
|
||||
* permissions: [
|
||||
* Permissions.FLAGS.SEND_MESSAGES,
|
||||
* Permissions.FLAGS.MANAGE_GUILD,
|
||||
* Permissions.FLAGS.MENTION_EVERYONE,
|
||||
* PermissionFlagsBits.SendMessages,
|
||||
* PermissionFlagsBits.ManageGuild,
|
||||
* PermissionFlagsBits.MentionEveryone,
|
||||
* ],
|
||||
* scopes: ['bot'],
|
||||
* scopes: [OAuth2Scopes.Bot],
|
||||
* });
|
||||
* console.log(`Generated bot invite link: ${link}`);
|
||||
*/
|
||||
@@ -408,10 +416,6 @@ class Client extends BaseClient {
|
||||
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
|
||||
if (!this.application) throw new Error('CLIENT_NOT_READY', 'generate an invite link');
|
||||
|
||||
const query = new URLSearchParams({
|
||||
client_id: this.application.id,
|
||||
});
|
||||
|
||||
const { scopes } = options;
|
||||
if (typeof scopes === 'undefined') {
|
||||
throw new TypeError('INVITE_MISSING_SCOPES');
|
||||
@@ -419,22 +423,24 @@ class Client extends BaseClient {
|
||||
if (!Array.isArray(scopes)) {
|
||||
throw new TypeError('INVALID_TYPE', 'scopes', 'Array of Invite Scopes', true);
|
||||
}
|
||||
if (!scopes.some(scope => ['bot', 'applications.commands'].includes(scope))) {
|
||||
if (!scopes.some(scope => [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands].includes(scope))) {
|
||||
throw new TypeError('INVITE_MISSING_SCOPES');
|
||||
}
|
||||
const invalidScope = scopes.find(scope => !InviteScopes.includes(scope));
|
||||
const validScopes = Object.values(OAuth2Scopes);
|
||||
const invalidScope = scopes.find(scope => !validScopes.includes(scope));
|
||||
if (invalidScope) {
|
||||
throw new TypeError('INVALID_ELEMENT', 'Array', 'scopes', invalidScope);
|
||||
}
|
||||
query.set('scope', scopes.join(' '));
|
||||
|
||||
const query = makeURLSearchParams({
|
||||
client_id: this.application.id,
|
||||
scope: scopes.join(' '),
|
||||
disable_guild_select: options.disableGuildSelect,
|
||||
});
|
||||
|
||||
if (options.permissions) {
|
||||
const permissions = Permissions.resolve(options.permissions);
|
||||
if (permissions) query.set('permissions', permissions);
|
||||
}
|
||||
|
||||
if (options.disableGuildSelect) {
|
||||
query.set('disable_guild_select', true);
|
||||
const permissions = PermissionsBitField.resolve(options.permissions);
|
||||
if (permissions) query.set('permissions', permissions.toString());
|
||||
}
|
||||
|
||||
if (options.guild) {
|
||||
@@ -443,7 +449,7 @@ class Client extends BaseClient {
|
||||
query.set('guild_id', guildId);
|
||||
}
|
||||
|
||||
return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`;
|
||||
return `${this.options.rest.api}${Routes.oauth2Authorization()}?${query}`;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@@ -472,7 +478,7 @@ class Client extends BaseClient {
|
||||
if (typeof options.intents === 'undefined') {
|
||||
throw new TypeError('CLIENT_MISSING_INTENTS');
|
||||
} else {
|
||||
options.intents = Intents.resolve(options.intents);
|
||||
options.intents = IntentsBitField.resolve(options.intents);
|
||||
}
|
||||
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1');
|
||||
@@ -487,39 +493,15 @@ class Client extends BaseClient {
|
||||
if (typeof options.sweepers !== 'object' || options.sweepers === null) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'sweepers', 'an object');
|
||||
}
|
||||
if (typeof options.invalidRequestWarningInterval !== 'number' || isNaN(options.invalidRequestWarningInterval)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'invalidRequestWarningInterval', 'a number');
|
||||
}
|
||||
if (!Array.isArray(options.partials)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array');
|
||||
}
|
||||
if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'waitGuildTimeout', 'a number');
|
||||
}
|
||||
if (typeof options.restRequestTimeout !== 'number' || isNaN(options.restRequestTimeout)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'restRequestTimeout', 'a number');
|
||||
}
|
||||
if (typeof options.restGlobalRateLimit !== 'number' || isNaN(options.restGlobalRateLimit)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'restGlobalRateLimit', 'a number');
|
||||
}
|
||||
if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number');
|
||||
}
|
||||
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number');
|
||||
}
|
||||
if (typeof options.failIfNotExists !== 'boolean') {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'failIfNotExists', 'a boolean');
|
||||
}
|
||||
if (!Array.isArray(options.userAgentSuffix)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'userAgentSuffix', 'an array of strings');
|
||||
}
|
||||
if (
|
||||
typeof options.rejectOnRateLimit !== 'undefined' &&
|
||||
!(typeof options.rejectOnRateLimit === 'function' || Array.isArray(options.rejectOnRateLimit))
|
||||
) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'rejectOnRateLimit', 'an array or a function');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,6 +520,12 @@ module.exports = Client;
|
||||
* @typedef {string} Snowflake
|
||||
*/
|
||||
|
||||
/**
|
||||
* Emitted for general debugging information.
|
||||
* @event Client#debug
|
||||
* @param {string} info The debug information
|
||||
*/
|
||||
|
||||
/**
|
||||
* Emitted for general warnings.
|
||||
* @event Client#warn
|
||||
@@ -548,3 +536,13 @@ module.exports = Client;
|
||||
* @external Collection
|
||||
* @see {@link https://discord.js.org/#/docs/collection/main/class/Collection}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external ImageURLOptions
|
||||
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/ImageURLOptions}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external BaseImageURLOptions
|
||||
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/BaseImageURLOptions}
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { PartialTypes } = require('../../util/Constants');
|
||||
const Partials = require('../../util/Partials');
|
||||
|
||||
/*
|
||||
|
||||
@@ -43,7 +43,7 @@ class GenericAction {
|
||||
},
|
||||
this.client.channels,
|
||||
id,
|
||||
PartialTypes.CHANNEL,
|
||||
Partials.Channel,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class GenericAction {
|
||||
},
|
||||
channel.messages,
|
||||
id,
|
||||
PartialTypes.MESSAGE,
|
||||
Partials.Message,
|
||||
cache,
|
||||
)
|
||||
);
|
||||
@@ -76,17 +76,17 @@ class GenericAction {
|
||||
},
|
||||
message.reactions,
|
||||
id,
|
||||
PartialTypes.REACTION,
|
||||
Partials.Reaction,
|
||||
);
|
||||
}
|
||||
|
||||
getMember(data, guild) {
|
||||
return this.getPayload(data, guild.members, data.user.id, PartialTypes.GUILD_MEMBER);
|
||||
return this.getPayload(data, guild.members, data.user.id, Partials.GuildMember);
|
||||
}
|
||||
|
||||
getUser(data) {
|
||||
const id = data.user_id;
|
||||
return data.user ?? this.getPayload({ id }, this.client.users, id, PartialTypes.USER);
|
||||
return data.user ?? this.getPayload({ id }, this.client.users, id, Partials.User);
|
||||
}
|
||||
|
||||
getUserFromMember(data) {
|
||||
@@ -107,9 +107,13 @@ class GenericAction {
|
||||
{ id, guild_id: data.guild_id ?? guild.id },
|
||||
guild.scheduledEvents,
|
||||
id,
|
||||
PartialTypes.GUILD_SCHEDULED_EVENT,
|
||||
Partials.GuildScheduledEvent,
|
||||
);
|
||||
}
|
||||
|
||||
getThreadMember(id, manager) {
|
||||
return this.getPayload({ user_id: id }, manager, id, Partials.ThreadMember, false);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GenericAction;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class ChannelCreateAction extends Action {
|
||||
handle(data) {
|
||||
@@ -14,7 +14,7 @@ class ChannelCreateAction extends Action {
|
||||
* @event Client#channelCreate
|
||||
* @param {GuildChannel} channel The channel that was created
|
||||
*/
|
||||
client.emit(Events.CHANNEL_CREATE, channel);
|
||||
client.emit(Events.ChannelCreate, channel);
|
||||
}
|
||||
return { channel };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class ChannelDeleteAction extends Action {
|
||||
handle(data) {
|
||||
@@ -15,7 +15,7 @@ class ChannelDeleteAction extends Action {
|
||||
* @event Client#channelDelete
|
||||
* @param {DMChannel|GuildChannel} channel The channel that was deleted
|
||||
*/
|
||||
client.emit(Events.CHANNEL_DELETE, channel);
|
||||
client.emit(Events.ChannelDelete, channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildBanAdd extends Action {
|
||||
handle(data) {
|
||||
@@ -13,7 +13,7 @@ class GuildBanAdd extends Action {
|
||||
* @event Client#guildBanAdd
|
||||
* @param {GuildBan} ban The ban that occurred
|
||||
*/
|
||||
if (guild) client.emit(Events.GUILD_BAN_ADD, guild.bans._add(data));
|
||||
if (guild) client.emit(Events.GuildBanAdd, guild.bans._add(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const Action = require('./Action');
|
||||
const GuildBan = require('../../structures/GuildBan');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildBanRemove extends Action {
|
||||
handle(data) {
|
||||
@@ -17,7 +17,7 @@ class GuildBanRemove extends Action {
|
||||
if (guild) {
|
||||
const ban = guild.bans.cache.get(data.user.id) ?? new GuildBan(client, data, guild);
|
||||
guild.bans.cache.delete(ban.user.id);
|
||||
client.emit(Events.GUILD_BAN_REMOVE, ban);
|
||||
client.emit(Events.GuildBanRemove, ban);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildDeleteAction extends Action {
|
||||
handle(data) {
|
||||
@@ -18,7 +18,7 @@ class GuildDeleteAction extends Action {
|
||||
* @event Client#guildUnavailable
|
||||
* @param {Guild} guild The guild that has become unavailable
|
||||
*/
|
||||
client.emit(Events.GUILD_UNAVAILABLE, guild);
|
||||
client.emit(Events.GuildUnavailable, guild);
|
||||
|
||||
// Stops the GuildDelete packet thinking a guild was actually deleted,
|
||||
// handles emitting of event itself
|
||||
@@ -36,7 +36,7 @@ class GuildDeleteAction extends Action {
|
||||
* @event Client#guildDelete
|
||||
* @param {Guild} guild The guild that was deleted
|
||||
*/
|
||||
client.emit(Events.GUILD_DELETE, guild);
|
||||
client.emit(Events.GuildDelete, guild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildEmojiCreateAction extends Action {
|
||||
handle(guild, createdEmoji) {
|
||||
@@ -12,7 +12,7 @@ class GuildEmojiCreateAction extends Action {
|
||||
* @event Client#emojiCreate
|
||||
* @param {GuildEmoji} emoji The emoji that was created
|
||||
*/
|
||||
if (!already) this.client.emit(Events.GUILD_EMOJI_CREATE, emoji);
|
||||
if (!already) this.client.emit(Events.GuildEmojiCreate, emoji);
|
||||
return { emoji };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildEmojiDeleteAction extends Action {
|
||||
handle(emoji) {
|
||||
@@ -11,7 +11,7 @@ class GuildEmojiDeleteAction extends Action {
|
||||
* @event Client#emojiDelete
|
||||
* @param {GuildEmoji} emoji The emoji that was deleted
|
||||
*/
|
||||
this.client.emit(Events.GUILD_EMOJI_DELETE, emoji);
|
||||
this.client.emit(Events.GuildEmojiDelete, emoji);
|
||||
return { emoji };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildEmojiUpdateAction extends Action {
|
||||
handle(current, data) {
|
||||
@@ -12,7 +12,7 @@ class GuildEmojiUpdateAction extends Action {
|
||||
* @param {GuildEmoji} oldEmoji The old emoji
|
||||
* @param {GuildEmoji} newEmoji The new emoji
|
||||
*/
|
||||
this.client.emit(Events.GUILD_EMOJI_UPDATE, old, current);
|
||||
this.client.emit(Events.GuildEmojiUpdate, old, current);
|
||||
return { emoji: current };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildIntegrationsUpdate extends Action {
|
||||
handle(data) {
|
||||
@@ -12,7 +12,7 @@ class GuildIntegrationsUpdate extends Action {
|
||||
* @event Client#guildIntegrationsUpdate
|
||||
* @param {Guild} guild The guild whose integrations were updated
|
||||
*/
|
||||
if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild);
|
||||
if (guild) client.emit(Events.GuildIntegrationsUpdate, guild);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events, Status } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
const Status = require('../../util/Status');
|
||||
|
||||
class GuildMemberRemoveAction extends Action {
|
||||
handle(data, shard) {
|
||||
@@ -18,7 +19,7 @@ class GuildMemberRemoveAction extends Action {
|
||||
* @event Client#guildMemberRemove
|
||||
* @param {GuildMember} member The member that has left/been kicked from the guild
|
||||
*/
|
||||
if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member);
|
||||
if (shard.status === Status.Ready) client.emit(Events.GuildMemberRemove, member);
|
||||
}
|
||||
guild.voiceStates.cache.delete(data.user.id);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Status, Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
const Status = require('../../util/Status');
|
||||
|
||||
class GuildMemberUpdateAction extends Action {
|
||||
handle(data, shard) {
|
||||
@@ -26,7 +27,7 @@ class GuildMemberUpdateAction extends Action {
|
||||
* @param {GuildMember} oldMember The member before the update
|
||||
* @param {GuildMember} newMember The member after the update
|
||||
*/
|
||||
if (shard.status === Status.READY && !member.equals(old)) client.emit(Events.GUILD_MEMBER_UPDATE, old, member);
|
||||
if (shard.status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member);
|
||||
} else {
|
||||
const newMember = guild.members._add(data);
|
||||
/**
|
||||
@@ -34,7 +35,7 @@ class GuildMemberUpdateAction extends Action {
|
||||
* @event Client#guildMemberAvailable
|
||||
* @param {GuildMember} member The member that became available
|
||||
*/
|
||||
this.client.emit(Events.GUILD_MEMBER_AVAILABLE, newMember);
|
||||
this.client.emit(Events.GuildMemberAvailable, newMember);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildRoleCreate extends Action {
|
||||
handle(data) {
|
||||
@@ -16,7 +16,7 @@ class GuildRoleCreate extends Action {
|
||||
* @event Client#roleCreate
|
||||
* @param {Role} role The role that was created
|
||||
*/
|
||||
if (!already) client.emit(Events.GUILD_ROLE_CREATE, role);
|
||||
if (!already) client.emit(Events.GuildRoleCreate, role);
|
||||
}
|
||||
return { role };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildRoleDeleteAction extends Action {
|
||||
handle(data) {
|
||||
@@ -18,7 +18,7 @@ class GuildRoleDeleteAction extends Action {
|
||||
* @event Client#roleDelete
|
||||
* @param {Role} role The role that was deleted
|
||||
*/
|
||||
client.emit(Events.GUILD_ROLE_DELETE, role);
|
||||
client.emit(Events.GuildRoleDelete, role);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Action = require('./Action');
|
||||
const { Events } = require('../../util/Constants');
|
||||
const Events = require('../../util/Events');
|
||||
|
||||
class GuildRoleUpdateAction extends Action {
|
||||
handle(data) {
|
||||
@@ -20,7 +20,7 @@ class GuildRoleUpdateAction extends Action {
|
||||
* @param {Role} oldRole The role before the update
|
||||
* @param {Role} newRole The role after the update
|
||||
*/
|
||||
client.emit(Events.GUILD_ROLE_UPDATE, old, role);
|
||||
client.emit(Events.GuildRoleUpdate, old, role);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user