mirror of
https://github.com/discordjs/discord.js.git
synced 2026-05-23 20:10:08 +00:00
Compare commits
362 Commits
stable
...
@discordjs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ef7ffdc6d | ||
|
|
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 | ||
|
|
a319607af6 | ||
|
|
487a5a2669 | ||
|
|
c10afeadc7 | ||
|
|
d0fa5aaa26 | ||
|
|
d8ea572fb8 | ||
|
|
1c6c9449ad | ||
|
|
b4ed8fd3ed | ||
|
|
857bba4480 | ||
|
|
bd33ebb507 | ||
|
|
cf25de9373 | ||
|
|
5b4dbc3359 | ||
|
|
213acd7997 | ||
|
|
164589c551 | ||
|
|
347ff80bbc | ||
|
|
307389a335 | ||
|
|
9181a31e0b | ||
|
|
86d9d06743 | ||
|
|
da05a8856b | ||
|
|
6112767128 | ||
|
|
26a9dc3206 | ||
|
|
18b0ed4cbe | ||
|
|
c90e47f904 | ||
|
|
3bb4829800 | ||
|
|
e65da44d9c | ||
|
|
0951309b32 | ||
|
|
101d7c5ffa | ||
|
|
599f96740e | ||
|
|
9054f2f7ad | ||
|
|
ecf2430f6e | ||
|
|
7c07976018 | ||
|
|
8ddd44ed85 | ||
|
|
0b23b7f039 | ||
|
|
06f5210f58 | ||
|
|
f284a4641f | ||
|
|
37ec0bda6d | ||
|
|
0a5d5f38c0 | ||
|
|
0dd56afe1c | ||
|
|
7b53978a73 | ||
|
|
2bb40fd767 | ||
|
|
37a22e04c2 | ||
|
|
f753882592 | ||
|
|
aa6d1c74de | ||
|
|
1479e40bce | ||
|
|
1745973024 | ||
|
|
62c74b8333 | ||
|
|
db25f529b2 | ||
|
|
16938da355 | ||
|
|
ff3a8b8323 | ||
|
|
86ab526d49 | ||
|
|
d8efba24e0 | ||
|
|
9052e321d1 | ||
|
|
22fdbb0a29 | ||
|
|
ed0cfd91ed | ||
|
|
b2dfb2ec70 | ||
|
|
56b69a749a | ||
|
|
fae39efc7f | ||
|
|
7ddd337c4a | ||
|
|
a8509c91ca | ||
|
|
a0a4a2258a | ||
|
|
4770c814b2 | ||
|
|
4917eb3539 | ||
|
|
c0824ae419 | ||
|
|
174782e132 | ||
|
|
538d6683bc | ||
|
|
0236308581 | ||
|
|
822dc678da | ||
|
|
f1d4bd68ec | ||
|
|
e38d79988a | ||
|
|
bd7a6f2652 | ||
|
|
37cad54dbd | ||
|
|
4fd127e79e | ||
|
|
ab3ff5a262 | ||
|
|
b92a7d7233 | ||
|
|
caaef53dd9 | ||
|
|
68b9564f18 | ||
|
|
962f4bf882 | ||
|
|
e07c374337 | ||
|
|
7832dbc07a | ||
|
|
4a2dbd6238 | ||
|
|
b593bd32a9 | ||
|
|
1d532d9468 | ||
|
|
3f36746561 | ||
|
|
e082dfb158 | ||
|
|
ae876d9624 | ||
|
|
171e917fb9 | ||
|
|
63034b44c9 | ||
|
|
13eb78256d | ||
|
|
1c615d1bb2 | ||
|
|
1db6c2d37e | ||
|
|
cee7fd181c | ||
|
|
55e21f5366 | ||
|
|
d06d70ccf2 | ||
|
|
95f8375d42 | ||
|
|
137ea249df | ||
|
|
49dada35f9 | ||
|
|
53971f0de9 | ||
|
|
9760fd1b77 | ||
|
|
fbef454894 | ||
|
|
fbbac27978 | ||
|
|
dbf5ce3e6c | ||
|
|
ef6f035b5d | ||
|
|
9d17904eff | ||
|
|
033151cf92 | ||
|
|
3269ec1984 | ||
|
|
c969cbf652 | ||
|
|
3872acfeb8 | ||
|
|
12ffa069aa | ||
|
|
1f9b9225f2 | ||
|
|
b7856e7809 | ||
|
|
5022b14da0 | ||
|
|
fbe40031cf | ||
|
|
a0d5f13dd9 | ||
|
|
6085b4f727 | ||
|
|
0c24cc8ec0 | ||
|
|
75616a305f | ||
|
|
2b480cb14e | ||
|
|
b454740ae8 | ||
|
|
c978dbb623 | ||
|
|
d4e6e03708 | ||
|
|
db669b8971 | ||
|
|
f79ea67d3a | ||
|
|
ae2f013653 | ||
|
|
37587e2bcb | ||
|
|
43e5e3c339 | ||
|
|
b246fc4101 | ||
|
|
4293b74253 | ||
|
|
c8bfb24589 | ||
|
|
b08c41d425 | ||
|
|
4a80710e7a | ||
|
|
1060dcdd4e | ||
|
|
967e9ace21 | ||
|
|
3c9a9c2c18 | ||
|
|
16390efe6e |
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-angular"],
|
||||
"rules": {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
["chore", "build", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "types", "typings"]
|
||||
]
|
||||
}
|
||||
"extends": ["@commitlint/config-angular"],
|
||||
"rules": {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
["chore", "build", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "types", "typings"]
|
||||
],
|
||||
"scope-case": [0]
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/COMMIT_CONVENTION.md
vendored
2
.github/COMMIT_CONVENTION.md
vendored
@@ -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))
|
||||
|
||||
99
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
99
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,12 +1,23 @@
|
||||
name: Bug report
|
||||
description: Report incorrect or unexpected behavior of discord.js
|
||||
description: Report incorrect or unexpected behavior of a package
|
||||
labels: [bug, need repro]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Use Discord for questions: https://discord.gg/djs
|
||||
If you are reporting a voice issue, please post your issue at https://github.com/discordjs/voice/issues
|
||||
- type: dropdown
|
||||
id: package
|
||||
attributes:
|
||||
label: Which package is this bug report for?
|
||||
options:
|
||||
- discord.js
|
||||
- builders
|
||||
- collection
|
||||
- rest
|
||||
- voice
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
@@ -30,28 +41,16 @@ body:
|
||||
description: Include a reproducible, minimal code sample. This will be automatically formatted into code, so no need for backticks.
|
||||
render: typescript
|
||||
placeholder: |
|
||||
const { Client, Intents } = require('discord.js');
|
||||
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
});
|
||||
|
||||
client.on('interactionCreate', async interaction => {
|
||||
if (!interaction.isCommand()) return;
|
||||
|
||||
if (interaction.commandName === 'ping') {
|
||||
await interaction.reply('Pong!');
|
||||
}
|
||||
});
|
||||
|
||||
client.login('token');
|
||||
Your code sample should be...
|
||||
... Minimal - Use as little code as possible that still produces the same problem (and is understandable)
|
||||
... Complete - Provide all parts someone else needs to reproduce your problem
|
||||
... Reproducible - Test the code you're about to provide to make sure it reproduces the problem
|
||||
- type: input
|
||||
id: djs-version
|
||||
attributes:
|
||||
label: discord.js version
|
||||
description: Which version of discord.js are you using? Run `npm list discord.js` in your project directory and paste the output.
|
||||
placeholder: 13.x.x (we no longer support version 12 or earlier)
|
||||
label: Package version
|
||||
description: Which version of are you using? Run `npm list <package>` in your project directory and paste the output.
|
||||
placeholder: We no longer support version 12 or earlier of discord.js
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -61,7 +60,7 @@ body:
|
||||
description: |
|
||||
Which version of Node.js are you using? Run `node --version` in your project directory and paste the output.
|
||||
If you are using TypeScript, please include its version (`npm list typescript`) as well.
|
||||
placeholder: Node.js version 16.6+ is required for version 13.0.0+
|
||||
placeholder: Node.js version 16.9+ is required for version 14.0.0+
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -89,13 +88,15 @@ body:
|
||||
|
||||
Tip: you can select multiple items
|
||||
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
|
||||
@@ -104,26 +105,28 @@ body:
|
||||
attributes:
|
||||
label: Which gateway intents are you subscribing to?
|
||||
description: |
|
||||
Check your Client constructor for the `intents` key.
|
||||
Check your Client constructor options for the `intents` key.
|
||||
|
||||
Tip: you can select multiple items
|
||||
options:
|
||||
- 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
|
||||
- Not applicable (subpackage bug)
|
||||
- No Intents
|
||||
- Guilds
|
||||
- GuildMembers
|
||||
- GuildBans
|
||||
- GuildEmojisAndStickers
|
||||
- GuildIntegrations
|
||||
- GuildWebhooks
|
||||
- GuildInvites
|
||||
- GuildVoiceStates
|
||||
- GuildPresences
|
||||
- GuildMessages
|
||||
- GuildMessageReactions
|
||||
- GuildMessageTyping
|
||||
- DirectMessages
|
||||
- DirectMessageReactions
|
||||
- DirectMessageTyping
|
||||
- GuildScheduledEvents
|
||||
multiple: true
|
||||
validations:
|
||||
required: true
|
||||
@@ -131,8 +134,8 @@ body:
|
||||
id: dev-release
|
||||
attributes:
|
||||
label: I have tested this issue on a development release
|
||||
placeholder: d23280c
|
||||
placeholder: d23280c (commit hash)
|
||||
description: |
|
||||
The issue might already be fixed in a development release. This is not required, but helps us greatly.
|
||||
To install the latest development release run `npm i discord.js@dev` in your project directory.
|
||||
The issue might already be fixed in a development release or main. This is not required, but helps us greatly.
|
||||
[discord.js only] To install the latest development release run `npm i discord.js@dev` in your project directory.
|
||||
Run `npm list discord.js` and use the last part of the printed information (`d23280c` for `discord.js@xx.x.x-dev.1530234593.d23280c`)
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
14
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Feature request
|
||||
description: Request a new feature (documented features of the official Discord developer API only!)
|
||||
description: Request a new feature (discord.js accepts documented features of the official Discord developer API only!)
|
||||
labels: [feature request]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -8,6 +8,18 @@ body:
|
||||
We can only implement features that Discord publishes, documents and merges into the Discord API documentation.
|
||||
We do not implement unreleased features.
|
||||
Use Discord for questions: https://discord.gg/djs
|
||||
- type: dropdown
|
||||
id: package
|
||||
attributes:
|
||||
label: Which package is the feature request for?
|
||||
options:
|
||||
- discord.js
|
||||
- builders
|
||||
- collection
|
||||
- rest
|
||||
- voice
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,7 +1,5 @@
|
||||
**Please describe the changes this PR makes and why it should be merged:**
|
||||
|
||||
|
||||
|
||||
**Status and versioning classification:**
|
||||
|
||||
<!--
|
||||
|
||||
7
.github/auto_assign.yml
vendored
Normal file
7
.github/auto_assign.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
addReviewers: true
|
||||
reviewers:
|
||||
- iCrawl
|
||||
- SpaceEEC
|
||||
- kyranet
|
||||
- vladfrangu
|
||||
numberOfReviewers: 0
|
||||
27
.github/labeler.yml
vendored
Normal file
27
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
chore:
|
||||
- any: ['*']
|
||||
all: ['!packages/*', '!packages/**/*']
|
||||
|
||||
'packages:builders':
|
||||
- packages/builders/*
|
||||
- packages/builders/**/*
|
||||
|
||||
'packages:collection':
|
||||
- packages/collection/*
|
||||
- packages/collection/**/*
|
||||
|
||||
'packages:discord.js':
|
||||
- packages/discord.js/*
|
||||
- packages/discord.js/**/*
|
||||
|
||||
'packages:rest':
|
||||
- packages/rest/*
|
||||
- packages/rest/**/*
|
||||
|
||||
'packages:voice':
|
||||
- packages/voice/*
|
||||
- packages/voice/**/*
|
||||
|
||||
'packages:ws':
|
||||
- packages/ws/*
|
||||
- packages/ws/**/*
|
||||
12
.github/labels.yml
vendored
12
.github/labels.yml
vendored
@@ -44,6 +44,18 @@
|
||||
color: 'e4e669'
|
||||
- name: 'need repro'
|
||||
color: 'c66037'
|
||||
- name: 'packages:builders'
|
||||
color: 'fbca04'
|
||||
- name: 'packages:collection'
|
||||
color: 'fbca04'
|
||||
- name: 'packages:discord.js'
|
||||
color: 'fbca04'
|
||||
- name: 'packages:rest'
|
||||
color: 'fbca04'
|
||||
- name: 'packages:voice'
|
||||
color: 'fbca04'
|
||||
- name: 'packages:ws'
|
||||
color: 'fbca04'
|
||||
- name: 'performance'
|
||||
color: '80c042'
|
||||
- name: 'permissions'
|
||||
|
||||
32
.github/tsc.json
vendored
32
.github/tsc.json
vendored
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "tsc",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(?:\\s+\\d+\\>)?([^\\s].*)\\((\\d+),(\\d+)\\)\\s*:\\s+(error|warning|info)\\s+(\\w{1,2}\\d+)\\s*:\\s*(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"code": 5,
|
||||
"message": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "tsc",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(?:\\s+\\d+\\>)?([^\\s].*)\\((\\d+),(\\d+)\\)\\s*:\\s+(error|warning|info)\\s+(\\w{1,2}\\d+)\\s*:\\s*(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"code": 5,
|
||||
"message": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
10
.github/workflows/auto-deprecate.yml
vendored
10
.github/workflows/auto-deprecate.yml
vendored
@@ -7,20 +7,22 @@ jobs:
|
||||
auto-deprecate:
|
||||
name: npm auto deprecate
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'discordjs'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node v16
|
||||
- name: Install node.js v16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
cache: npm
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
run: yarn --immutable
|
||||
|
||||
- name: Deprecate versions
|
||||
run: 'npm exec --no 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 }}
|
||||
|
||||
29
.github/workflows/deploy.yml
vendored
29
.github/workflows/deploy.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Deployment
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- '!docs'
|
||||
tags:
|
||||
- '*'
|
||||
jobs:
|
||||
docs:
|
||||
name: Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node v16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build and deploy documentation
|
||||
uses: discordjs/action-docs@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
127
.github/workflows/documentation.yml
vendored
Normal file
127
.github/workflows/documentation.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'stable'
|
||||
- '!docs'
|
||||
tags:
|
||||
- '**'
|
||||
jobs:
|
||||
build:
|
||||
name: Build documentation
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'discordjs'
|
||||
outputs:
|
||||
BRANCH_NAME: ${{ steps.env.outputs.BRANCH_NAME }}
|
||||
BRANCH_OR_TAG: ${{ steps.env.outputs.BRANCH_OR_TAG }}
|
||||
SHA: ${{ steps.env.outputs.SHA }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install node.js v16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: yarn.lock
|
||||
|
||||
- name: Turbo cache
|
||||
id: turbo-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ github.job }}-${{ github.ref_name }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn --immutable
|
||||
|
||||
- name: Build docs
|
||||
run: yarn docs --cache-dir=".turbo"
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: docs
|
||||
path: packages/*/docs/docs.json
|
||||
|
||||
- name: Set outputs for upload job
|
||||
id: env
|
||||
run: |
|
||||
echo "::set-output name=BRANCH_NAME::${GITHUB_REF_NAME}"
|
||||
echo "::set-output name=BRANCH_OR_TAG::${GITHUB_REF_TYPE}"
|
||||
echo "::set-output name=SHA::${GITHUB_SHA}"
|
||||
|
||||
upload:
|
||||
name: Upload Documentation
|
||||
needs: build
|
||||
strategy:
|
||||
max-parallel: 1
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: ['builders', 'collection', 'discord.js', 'rest', 'voice']
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.build.outputs.BRANCH_NAME }}
|
||||
BRANCH_OR_TAG: ${{ needs.build.outputs.BRANCH_OR_TAG }}
|
||||
SHA: ${{ needs.build.outputs.SHA }}
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docs
|
||||
path: docs
|
||||
|
||||
- name: Checkout docs repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'discordjs/docs'
|
||||
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: |
|
||||
mkdir -p out/${PACKAGE}
|
||||
mv docs/${PACKAGE}/docs/docs.json out/${PACKAGE}/${BRANCH_NAME}.json
|
||||
|
||||
- name: Commit and push
|
||||
run: |
|
||||
cd out
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
git add .
|
||||
git commit -m "Docs build for ${BRANCH_OR_TAG} ${BRANCH_NAME}: ${SHA}" || true
|
||||
git push
|
||||
6
.github/workflows/labelsync.yml
vendored
6
.github/workflows/labelsync.yml
vendored
@@ -9,14 +9,14 @@ on:
|
||||
paths:
|
||||
- '.github/labels.yml'
|
||||
jobs:
|
||||
labeler:
|
||||
name: Labeler
|
||||
labelsync:
|
||||
name: Label sync
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run Label Sync
|
||||
- name: Label sync
|
||||
uses: crazy-max/ghaction-github-labeler@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
16
.github/workflows/pr-automation.yml
vendored
Normal file
16
.github/workflows/pr-automation.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: 'PR Automation'
|
||||
on:
|
||||
pull_request_target:
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Automatically label PR
|
||||
uses: actions/labeler@v3
|
||||
with:
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
sync-labels: true
|
||||
|
||||
- name: Automatically assign reviewers
|
||||
if: ${{ github.event.action == 'opened' }}
|
||||
uses: kentaro-m/auto-assign-action@v1.1.2
|
||||
45
.github/workflows/publish-dev.yml
vendored
45
.github/workflows/publish-dev.yml
vendored
@@ -6,39 +6,68 @@ 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:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node v16
|
||||
- name: Install node.js v16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: npm
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: yarn.lock
|
||||
|
||||
- name: Turbo cache
|
||||
id: turbo-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ github.job }}-${{ github.ref_name }}-
|
||||
|
||||
- name: Check previous released version
|
||||
id: pre-release
|
||||
run: |
|
||||
if [[ $(npm view discord.js@dev version | grep -e "$(jq --raw-output '.version' 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
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.pre-release.outputs.release == 'true'
|
||||
run: npm ci --ignore-scripts
|
||||
run: yarn --immutable
|
||||
|
||||
- name: Build dependencies
|
||||
if: steps.pre-release.outputs.release == 'true'
|
||||
run: yarn build --cache-dir=".turbo"
|
||||
|
||||
- name: Deprecate old versions
|
||||
if: steps.pre-release.outputs.release == 'true'
|
||||
run: npm deprecate discord.js@"~$(jq --raw-output '.version' 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: |
|
||||
npm version --git-tag-version=false $(jq --raw-output '.version' package.json).$(date +%s).$(git rev-parse --short HEAD)
|
||||
npm 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 }}
|
||||
|
||||
80
.github/workflows/test.yml
vendored
80
.github/workflows/test.yml
vendored
@@ -2,80 +2,36 @@ name: Tests
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
lint:
|
||||
name: ESLint
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node v16
|
||||
- name: Install node.js v16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
cache: npm
|
||||
cache: 'yarn'
|
||||
cache-dependency-path: yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
typings:
|
||||
name: TSLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node v16
|
||||
uses: actions/setup-node@v2
|
||||
- name: Turbo cache
|
||||
id: turbo-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
node-version: 16
|
||||
cache: npm
|
||||
path: .turbo
|
||||
key: turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ github.job }}-${{ github.ref_name }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: yarn --immutable
|
||||
|
||||
- name: Run TSLint
|
||||
run: npm run lint:typings
|
||||
- name: ESLint
|
||||
run: yarn lint --cache-dir=".turbo"
|
||||
|
||||
typescript:
|
||||
name: TypeScript
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Tests
|
||||
run: yarn test --cache-dir=".turbo"
|
||||
|
||||
- name: Install Node v16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Register Problem Matcher
|
||||
run: echo "##[add-matcher].github/tsc.json"
|
||||
|
||||
- name: Run Type Tests
|
||||
run: npm run test:typescript
|
||||
|
||||
docs:
|
||||
name: Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node v16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Test documentation
|
||||
run: npm run docs:test
|
||||
- name: Build
|
||||
run: yarn build --cache-dir=".turbo"
|
||||
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
@@ -13,18 +13,25 @@ pids
|
||||
|
||||
# Env
|
||||
.env
|
||||
test/auth.json
|
||||
test/auth.js
|
||||
docs/deploy/deploy_key
|
||||
docs/deploy/deploy_key.pub
|
||||
deploy/deploy_key
|
||||
deploy/deploy_key.pub
|
||||
|
||||
# Dist
|
||||
dist/
|
||||
docs/docs.json
|
||||
|
||||
# Miscellaneous
|
||||
.tmp/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
.idea/
|
||||
.DS_Store
|
||||
.turbo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# yarn
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit $1
|
||||
yarn commitlint --edit $1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install lint-staged
|
||||
yarn format
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"*.{mjs,js}": "eslint --fix --ext mjs,js,ts",
|
||||
"*.{ts,json,yml,yaml}": "prettier --write"
|
||||
}
|
||||
5
.npmrc
5
.npmrc
@@ -1,5 +0,0 @@
|
||||
audit=false
|
||||
fund=false
|
||||
legacy-peer-deps=true
|
||||
tag-version-prefix=""
|
||||
message="chore(Release): %s"
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf",
|
||||
"arrowParens": "avoid"
|
||||
"printWidth": 120,
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"ecmaVersion": 7,
|
||||
"libs": [],
|
||||
"loadEagerly": ["./src/*.js"],
|
||||
"dontLoad": ["node_modules/**"],
|
||||
"plugins": {
|
||||
"es_modules": {},
|
||||
"node": {},
|
||||
"doc_comment": {
|
||||
"fullDocs": true,
|
||||
"strong": true
|
||||
},
|
||||
}
|
||||
}
|
||||
12
.vscode/extensions.json
vendored
Normal file
12
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"tamasfe.even-better-toml",
|
||||
"github.vscode-pull-request-github",
|
||||
"codezombiech.gitignore",
|
||||
"eamodio.gitlens",
|
||||
"christian-kohler.npm-intellisense",
|
||||
"christian-kohler.path-intellisense"
|
||||
]
|
||||
}
|
||||
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"eslint.workingDirectories": [{ "pattern": "./packages/*" }],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true,
|
||||
"source.organizeImports": false
|
||||
}
|
||||
}
|
||||
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
|
||||
56
README.md
56
README.md
@@ -8,7 +8,7 @@
|
||||
<a href="https://discord.gg/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&logo=discord&logoColor=white" alt="Discord server" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="npm version" /></a>
|
||||
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="npm downloads" /></a>
|
||||
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/workflows/Testing/badge.svg" alt="Tests status" /></a>
|
||||
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/test.yml/badge.svg" alt="Tests status" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
|
||||
|
||||
## Installation
|
||||
|
||||
**Node.js 16.6.0 or newer is required.**
|
||||
**Node.js 16.9.0 or newer is required.**
|
||||
|
||||
```sh-session
|
||||
npm install discord.js
|
||||
@@ -38,11 +38,12 @@ pnpm add discord.js
|
||||
- [erlpack](https://github.com/discord/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discord/erlpack`)
|
||||
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`)
|
||||
- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`)
|
||||
- [@discordjs/voice](https://github.com/discordjs/voice) for interacting with the Discord Voice API (`npm install @discordjs/voice`)
|
||||
- [@discordjs/voice](https://www.npmjs.com/package/@discordjs/voice) for interacting with the Discord Voice API (`npm install @discordjs/voice`)
|
||||
|
||||
## Example usage
|
||||
|
||||
Install all required dependencies:
|
||||
|
||||
```sh-session
|
||||
npm install discord.js @discordjs/rest discord-api-types
|
||||
yarn add discord.js @discordjs/rest discord-api-types
|
||||
@@ -50,48 +51,49 @@ pnpm add discord.js @discordjs/rest discord-api-types
|
||||
```
|
||||
|
||||
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 = [{
|
||||
name: 'ping',
|
||||
description: 'Replies with Pong!'
|
||||
}];
|
||||
const commands = [
|
||||
{
|
||||
name: 'ping',
|
||||
description: 'Replies with Pong!',
|
||||
},
|
||||
];
|
||||
|
||||
const rest = new REST({ version: '9' }).setToken('token');
|
||||
const rest = new REST({ version: '10' }).setToken('token');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('Started refreshing application (/) commands.');
|
||||
try {
|
||||
console.log('Started refreshing application (/) commands.');
|
||||
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
|
||||
{ body: commands },
|
||||
);
|
||||
await rest.put(Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), { body: commands });
|
||||
|
||||
console.log('Successfully reloaded application (/) commands.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
console.log('Successfully reloaded application (/) commands.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
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}!`);
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
});
|
||||
|
||||
client.on('interactionCreate', async interaction => {
|
||||
if (!interaction.isCommand()) return;
|
||||
client.on('interactionCreate', async (interaction) => {
|
||||
if (!interaction.isCommand()) return;
|
||||
|
||||
if (interaction.commandName === 'ping') {
|
||||
await interaction.reply('Pong!');
|
||||
}
|
||||
if (interaction.commandName === 'ping') {
|
||||
await interaction.reply('Pong!');
|
||||
}
|
||||
});
|
||||
|
||||
client.login('token');
|
||||
|
||||
22508
package-lock.json
generated
22508
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
140
package.json
140
package.json
@@ -1,88 +1,56 @@
|
||||
{
|
||||
"name": "discord.js",
|
||||
"version": "14.0.0-dev",
|
||||
"description": "A powerful library for interacting with the Discord API",
|
||||
"scripts": {
|
||||
"test": "npm run lint && npm run docs:test && npm run lint:typings && npm run test:typescript",
|
||||
"test:typescript": "tsc --noEmit && tsd",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint:typings": "tslint typings/index.d.ts",
|
||||
"format": "prettier --write src/**/*.js typings/**/*.ts",
|
||||
"prepare": "is-ci || husky install",
|
||||
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
|
||||
"docs:test": "docgen --source src --custom docs/index.yml",
|
||||
"prepublishOnly": "npm run test",
|
||||
"changelog": "git cliff --prepend CHANGELOG.md -l"
|
||||
},
|
||||
"main": "./src/index.js",
|
||||
"types": "./typings/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"typings"
|
||||
],
|
||||
"directories": {
|
||||
"lib": "src",
|
||||
"test": "test"
|
||||
},
|
||||
"contributors": [
|
||||
"Crawl <icrawltogo@gmail.com>",
|
||||
"Amish Shah <amishshah.2k@gmail.com>",
|
||||
"Vlad Frangu <kingdgrizzle@gmail.com>",
|
||||
"SpaceEEC <spaceeec@yahoo.com>",
|
||||
"Antonio Roman <kyradiscord@gmail.com>"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"discord",
|
||||
"api",
|
||||
"bot",
|
||||
"client",
|
||||
"node",
|
||||
"discordapp"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/discordjs/discord.js.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/discordjs/discord.js/issues"
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^0.11.0",
|
||||
"@discordjs/collection": "^0.4.0",
|
||||
"@sapphire/async-queue": "^1.1.9",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/ws": "^8.2.2",
|
||||
"discord-api-types": "^0.26.0",
|
||||
"form-data": "^4.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ws": "^8.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.0.1",
|
||||
"@commitlint/config-angular": "^16.0.0",
|
||||
"@discordjs/docgen": "^0.11.0",
|
||||
"@favware/npm-deprecate": "^1.0.4",
|
||||
"@types/node": "^16.11.12",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"dtslint": "^4.2.1",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^27.4.5",
|
||||
"lint-staged": "^12.1.4",
|
||||
"prettier": "^2.5.1",
|
||||
"tsd": "^0.19.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.6.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
"name": "@discordjs/discord.js",
|
||||
"version": "0.0.0",
|
||||
"description": "A powerful library for interacting with the Discord API",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"test": "turbo run test",
|
||||
"lint": "turbo run lint",
|
||||
"format": "turbo run format",
|
||||
"fmt": "turbo run format",
|
||||
"postinstall": "is-ci || husky install",
|
||||
"docs": "turbo run docs",
|
||||
"changelog": "turbo run changelog",
|
||||
"update": "yarn upgrade-interactive"
|
||||
},
|
||||
"contributors": [
|
||||
"Crawl <icrawltogo@gmail.com>",
|
||||
"Amish Shah <amishshah.2k@gmail.com>",
|
||||
"Vlad Frangu <kingdgrizzle@gmail.com>",
|
||||
"SpaceEEC <spaceeec@yahoo.com>",
|
||||
"Antonio Roman <kyradiscord@gmail.com>"
|
||||
],
|
||||
"keywords": [
|
||||
"discord",
|
||||
"api",
|
||||
"bot",
|
||||
"client",
|
||||
"node",
|
||||
"discordapp"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/discordjs/discord.js.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/discordjs/discord.js/issues"
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"devDependencies": {
|
||||
"@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.6.2",
|
||||
"turbo": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"packageManager": "yarn@3.1.1"
|
||||
}
|
||||
|
||||
12
packages/builders/.eslintrc.json
Normal file
12
packages/builders/.eslintrc.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": "marine/prettier/node",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.eslint.json",
|
||||
"extraFileExtensions": [".mjs"]
|
||||
},
|
||||
"ignorePatterns": ["**/dist/*"],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
29
packages/builders/.gitignore
vendored
Normal file
29
packages/builders/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Packages
|
||||
node_modules/
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Env
|
||||
.env
|
||||
|
||||
# Dist
|
||||
dist/
|
||||
typings/
|
||||
|
||||
docs/**/*
|
||||
!docs/index.yml
|
||||
!docs/README.md
|
||||
!docs/examples/
|
||||
!docs/examples/*.md
|
||||
|
||||
# Miscellaneous
|
||||
.tmp/
|
||||
coverage/
|
||||
8
packages/builders/.prettierignore
Normal file
8
packages/builders/.prettierignore
Normal file
@@ -0,0 +1,8 @@
|
||||
# Autogenerated
|
||||
CHANGELOG.md
|
||||
.turbo
|
||||
dist/
|
||||
docs/**/*
|
||||
!docs/index.yml
|
||||
!docs/README.md
|
||||
coverage/
|
||||
8
packages/builders/.prettierrc.json
Normal file
8
packages/builders/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
3
packages/builders/.versionrc
Normal file
3
packages/builders/.versionrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"releaseCommitMessageFormat": "chore(Release): publish"
|
||||
}
|
||||
181
packages/builders/CHANGELOG.md
Normal file
181
packages/builders/CHANGELOG.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Changelog
|
||||
|
||||
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
|
||||
|
||||
- **builders:** Dont export `Button` component stuff twice (#7289) ([86d9d06](https://github.com/discordjs/discord.js/commit/86d9d0674347c08d056cd054cb4ce4253195bf94))
|
||||
|
||||
## Documentation
|
||||
|
||||
- **SlashCommandSubcommands:** Updating old links from Discord developer portal (#7224) ([bd7a6f2](https://github.com/discordjs/discord.js/commit/bd7a6f265212624199fb0b2ddc8ece39759c63de))
|
||||
|
||||
## Features
|
||||
|
||||
- Add components to /builders (#7195) ([2bb40fd](https://github.com/discordjs/discord.js/commit/2bb40fd767cf5918e3ba422ff73082734bfa05b0))
|
||||
|
||||
## Typings
|
||||
|
||||
- Make `required` a boolean (#7307) ([c10afea](https://github.com/discordjs/discord.js/commit/c10afeadc702ab98bec5e077b3b92494a9596f9c))
|
||||
|
||||
# [0.11.0](https://github.com/discordjs/builders/compare/v0.10.0...v0.11.0) (2021-12-29)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **ApplicationCommandOptions:** clean up code for builder options ([#68](https://github.com/discordjs/builders/issues/68)) ([b5d0b15](https://github.com/discordjs/builders/commit/b5d0b157b1262bd01fa011f8e0cf33adb82776e7))
|
||||
|
||||
# [0.10.0](https://github.com/discordjs/builders/compare/v0.9.0...v0.10.0) (2021-12-24)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- use zod instead of ow for max/min option validation ([#66](https://github.com/discordjs/builders/issues/66)) ([beb35fb](https://github.com/discordjs/builders/commit/beb35fb1f65bd6be2321e17cc792f67e8615fd48))
|
||||
|
||||
## Features
|
||||
|
||||
- add max/min option for int and number builder options ([#47](https://github.com/discordjs/builders/issues/47)) ([2e1e860](https://github.com/discordjs/builders/commit/2e1e860b46e3453398b20df63dabb6d4325e32d1))
|
||||
|
||||
# [0.9.0](https://github.com/discordjs/builders/compare/v0.8.2...v0.9.0) (2021-12-02)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- replace ow with zod ([#58](https://github.com/discordjs/builders/issues/58)) ([0b6fb81](https://github.com/discordjs/builders/commit/0b6fb8161b858e42781855fb73aaa873fec58160))
|
||||
|
||||
## Features
|
||||
|
||||
- **SlashCommandBuilder:** add autocomplete ([#53](https://github.com/discordjs/builders/issues/53)) ([05b07a7](https://github.com/discordjs/builders/commit/05b07a7e88848188c27d7380d9f948cba25ef778))
|
||||
|
||||
## [0.8.2](https://github.com/discordjs/builders/compare/v0.8.1...v0.8.2) (2021-10-30)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- downgrade ow because of esm issues ([#55](https://github.com/discordjs/builders/issues/55)) ([3722d2c](https://github.com/discordjs/builders/commit/3722d2c1109a7a5c0abad63c1a7eb944df6e46c8))
|
||||
|
||||
## [0.8.1](https://github.com/discordjs/builders/compare/v0.8.0...v0.8.1) (2021-10-29)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- documentation ([e33ec8d](https://github.com/discordjs/builders/commit/e33ec8dfd5785312f82e0afb017a3dac614fd71d))
|
||||
|
||||
# [0.7.0](https://github.com/discordjs/builders/compare/v0.6.0...v0.7.0) (2021-10-18)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- properly type `toJSON` methods ([#34](https://github.com/discordjs/builders/issues/34)) ([7723ad0](https://github.com/discordjs/builders/commit/7723ad0da169386e638188de220451a97513bc25))
|
||||
|
||||
## Features
|
||||
|
||||
- **ContextMenus:** add context menu command builder ([#29](https://github.com/discordjs/builders/issues/29)) ([f0641e5](https://github.com/discordjs/builders/commit/f0641e55733de8992600f3082bcf054e6f815cf7))
|
||||
- add support for channel types on channel options ([#41](https://github.com/discordjs/builders/issues/41)) ([f6c187e](https://github.com/discordjs/builders/commit/f6c187e0ad6ebe03e65186ece3e95cb1db5aeb50))
|
||||
|
||||
# [0.6.0](https://github.com/discordjs/builders/compare/v0.5.0...v0.6.0) (2021-08-24)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **SlashCommandBuilder:** allow subcommands and groups to coexist at the root level ([#26](https://github.com/discordjs/builders/issues/26)) ([0be4daf](https://github.com/discordjs/builders/commit/0be4dafdfc0b5747c880be0078c00ada913eb4fb))
|
||||
|
||||
## Features
|
||||
|
||||
- create `Embed` builder ([#11](https://github.com/discordjs/builders/issues/11)) ([eb942a4](https://github.com/discordjs/builders/commit/eb942a4d1f3bcec9a4e370b6af602a713ad8f9b7))
|
||||
- **SlashCommandBuilder:** create setDefaultPermission function ([#19](https://github.com/discordjs/builders/issues/19)) ([5d53759](https://github.com/discordjs/builders/commit/5d537593937a8da330153ce4711b7d093a80330e))
|
||||
- **SlashCommands:** add number option type ([#23](https://github.com/discordjs/builders/issues/23)) ([1563991](https://github.com/discordjs/builders/commit/1563991d421bb07bf7a412c87e7613692d770f04))
|
||||
|
||||
# [0.5.0](https://github.com/discordjs/builders/compare/v0.3.0...v0.5.0) (2021-08-10)
|
||||
|
||||
## Features
|
||||
|
||||
- **Formatters:** add `formatEmoji` ([#20](https://github.com/discordjs/builders/issues/20)) ([c3d8bb5](https://github.com/discordjs/builders/commit/c3d8bb5363a1d46b45c0def4277da6921e2ba209))
|
||||
|
||||
# [0.4.0](https://github.com/discordjs/builders/compare/v0.3.0...v0.4.0) (2021-08-05)
|
||||
|
||||
## Features
|
||||
|
||||
- `sub command` => `subcommand` ([#18](https://github.com/discordjs/builders/pull/18)) ([95599c5](https://github.com/discordjs/builders/commit/95599c5b5366ebd054c4c277c52f1a44cda1209d))
|
||||
|
||||
# [0.3.0](https://github.com/discordjs/builders/compare/v0.2.0...v0.3.0) (2021-08-01)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Shrug:** Update comment ([#14](https://github.com/discordjs/builders/issues/14)) ([6fa6c40](https://github.com/discordjs/builders/commit/6fa6c405f2ea733811677d3d1bfb1e2806d504d5))
|
||||
- shrug face rendering ([#13](https://github.com/discordjs/builders/issues/13)) ([6ad24ec](https://github.com/discordjs/builders/commit/6ad24ecd96c82b0f576e78e9e53fc7bf9c36ef5d))
|
||||
|
||||
## Features
|
||||
|
||||
- **formatters:** mentions ([#9](https://github.com/discordjs/builders/issues/9)) ([f83fe99](https://github.com/discordjs/builders/commit/f83fe99b83188ed999845751ffb005c687dbd60a))
|
||||
- **Formatters:** Add a spoiler function ([#16](https://github.com/discordjs/builders/issues/16)) ([c213a6a](https://github.com/discordjs/builders/commit/c213a6abb114f65653017a4edec4bdba2162d771))
|
||||
- **SlashCommands:** add slash command builders ([#3](https://github.com/discordjs/builders/issues/3)) ([6aa3af0](https://github.com/discordjs/builders/commit/6aa3af07b0ee342fff91f080914bb12b3ab773f8))
|
||||
- shrug, tableflip and unflip strings ([#5](https://github.com/discordjs/builders/issues/5)) ([de5fa82](https://github.com/discordjs/builders/commit/de5fa823cd6f1feba5b2d0a63b2cb1761dfd1814))
|
||||
|
||||
# [0.2.0](https://github.com/discordjs/builders/compare/v0.1.1...v0.2.0) (2021-07-03)
|
||||
|
||||
## Features
|
||||
|
||||
- **Formatters:** added `hyperlink` and `hideLinkEmbed` ([#4](https://github.com/discordjs/builders/issues/4)) ([c532daf](https://github.com/discordjs/builders/commit/c532daf2ba2feae75bf9668f63462e96a5314cff))
|
||||
|
||||
# [0.1.1](https://github.com/discordjs/builders/compare/v0.1.0...v0.1.1) (2021-06-30)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Deps:** added `tslib` as dependency ([#2](https://github.com/discordjs/builders/issues/2)) ([5576ff3](https://github.com/discordjs/builders/commit/5576ff3b67136b957bed0ab8a4c655d5de322813))
|
||||
|
||||
# 0.1.0 (2021-06-30)
|
||||
|
||||
## Features
|
||||
|
||||
- added message formatters ([#1](https://github.com/discordjs/builders/issues/1)) ([765e46d](https://github.com/discordjs/builders/commit/765e46dac96c4e49d350243e5fad34c2bc738a7c))
|
||||
191
packages/builders/LICENSE
Normal file
191
packages/builders/LICENSE
Normal file
@@ -0,0 +1,191 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2021 Noel Buechler
|
||||
Copyright 2021 Vlad Frangu
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
53
packages/builders/README.md
Normal file
53
packages/builders/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
<div align="center">
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.gg/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&logo=discord&logoColor=white" alt="Discord server" /></a>
|
||||
<a href="https://www.npmjs.com/package/@discordjs/builders"><img src="https://img.shields.io/npm/v/@discordjs/builders.svg?maxAge=3600" alt="npm version" /></a>
|
||||
<a href="https://www.npmjs.com/package/@discordjs/builders"><img src="https://img.shields.io/npm/dt/@discordjs/builders.svg?maxAge=3600" alt="npm downloads" /></a>
|
||||
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/test.yml/badge.svg" alt="Build status" /></a>
|
||||
<a href="https://codecov.io/gh/discordjs/builders"><img src="https://codecov.io/gh/discordjs/builders/branch/main/graph/badge.svg" alt="Code coverage" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## Installation
|
||||
|
||||
**Node.js 16.9.0 or newer is required.**
|
||||
|
||||
```sh-session
|
||||
npm install @discordjs/builders
|
||||
yarn add @discordjs/builders
|
||||
pnpm add @discordjs/builders
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Here are some examples for the builders and utilities you can find in this package:
|
||||
|
||||
- [Slash Command Builders](https://github.com/discordjs/discord.js/blob/main/packages/builders/docs/examples/Slash%20Command%20Builders.md)
|
||||
|
||||
## Links
|
||||
|
||||
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
|
||||
- [Documentation](https://discord.js.org/#/docs/builders)
|
||||
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
|
||||
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
|
||||
- [discord.js Discord server](https://discord.gg/djs)
|
||||
- [Discord API Discord server](https://discord.gg/discord-api)
|
||||
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/builders)
|
||||
- [npm](https://www.npmjs.com/package/@discordjs/builders)
|
||||
- [Related libraries](https://discord.com/developers/docs/topics/community-resources#libraries)
|
||||
|
||||
## Contributing
|
||||
|
||||
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||
[documentation](https://discord.js.org/#/docs/builders).
|
||||
See [the contribution guide](https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
|
||||
## Help
|
||||
|
||||
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||
nudge in the right direction, please don't hesitate to join our official [discord.js Server](https://discord.gg/djs).
|
||||
140
packages/builders/__tests__/components/actionRow.test.ts
Normal file
140
packages/builders/__tests__/components/actionRow.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
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 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<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
label: 'button',
|
||||
style: ButtonStyle.Primary,
|
||||
custom_id: 'test',
|
||||
},
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
label: 'link',
|
||||
style: ButtonStyle.Link,
|
||||
url: 'https://google.com',
|
||||
},
|
||||
{
|
||||
type: ComponentType.SelectMenu,
|
||||
placeholder: 'test',
|
||||
custom_id: 'test',
|
||||
options: [
|
||||
{
|
||||
label: 'option',
|
||||
value: 'option',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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<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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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 SelectMenuOptionBuilder().setLabel('one').setValue('one'),
|
||||
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
||||
);
|
||||
|
||||
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);
|
||||
expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
|
||||
});
|
||||
});
|
||||
});
|
||||
146
packages/builders/__tests__/components/button.test.ts
Normal file
146
packages/builders/__tests__/components/button.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
APIButtonComponentWithCustomId,
|
||||
APIButtonComponentWithURL,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
} from 'discord-api-types/v10';
|
||||
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions';
|
||||
import { ButtonBuilder } from '../../src/components/button/Button';
|
||||
|
||||
const buttonComponent = () => new ButtonBuilder();
|
||||
|
||||
const longStr =
|
||||
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
||||
|
||||
describe('Button Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid label THEN validator does not throw', () => {
|
||||
expect(() => buttonLabelValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid label THEN validator does throw', () => {
|
||||
expect(() => buttonLabelValidator.parse(null)).toThrowError();
|
||||
expect(() => buttonLabelValidator.parse('')).toThrowError();
|
||||
|
||||
expect(() => buttonLabelValidator.parse(longStr)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid style THEN validator does not throw', () => {
|
||||
expect(() => buttonStyleValidator.parse(3)).not.toThrowError();
|
||||
expect(() => buttonStyleValidator.parse(ButtonStyle.Secondary)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid style THEN validator does not throw', () => {
|
||||
expect(() => buttonStyleValidator.parse(7)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||
expect(() =>
|
||||
buttonComponent().setCustomId('custom').setStyle(ButtonStyle.Primary).setLabel('test'),
|
||||
).not.toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const button = buttonComponent()
|
||||
.setCustomId('custom')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(true)
|
||||
.setEmoji({ name: 'test' });
|
||||
|
||||
button.toJSON();
|
||||
}).not.toThrowError();
|
||||
|
||||
expect(() => buttonComponent().setURL('https://google.com')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid fields THEN build does throw', () => {
|
||||
expect(() => {
|
||||
buttonComponent().setCustomId(longStr);
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const button = buttonComponent()
|
||||
.setCustomId('custom')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(true)
|
||||
.setLabel('test')
|
||||
.setURL('https://google.com')
|
||||
.setEmoji({ name: 'test' });
|
||||
|
||||
button.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
// @ts-expect-error
|
||||
const button = buttonComponent().setEmoji('test');
|
||||
button.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const button = buttonComponent().setStyle(ButtonStyle.Primary);
|
||||
button.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const button = buttonComponent().setStyle(ButtonStyle.Primary).setCustomId('test');
|
||||
button.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const button = buttonComponent().setStyle(ButtonStyle.Link);
|
||||
button.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const button = buttonComponent().setStyle(ButtonStyle.Primary).setLabel('test').setURL('https://google.com');
|
||||
button.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const button = buttonComponent().setStyle(ButtonStyle.Link).setLabel('test');
|
||||
button.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => buttonComponent().setStyle(24)).toThrowError();
|
||||
expect(() => buttonComponent().setLabel(longStr)).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => buttonComponent().setDisabled(0)).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => buttonComponent().setEmoji('foo')).toThrowError();
|
||||
|
||||
expect(() => buttonComponent().setURL('foobar')).toThrowError();
|
||||
});
|
||||
|
||||
test('GiVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const interactionData: APIButtonComponentWithCustomId = {
|
||||
type: ComponentType.Button,
|
||||
custom_id: 'test',
|
||||
label: 'test',
|
||||
style: ButtonStyle.Primary,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
|
||||
|
||||
expect(
|
||||
buttonComponent()
|
||||
.setCustomId(interactionData.custom_id)
|
||||
.setLabel(interactionData.label)
|
||||
.setStyle(interactionData.style)
|
||||
.setDisabled(interactionData.disabled)
|
||||
.toJSON(),
|
||||
).toEqual(interactionData);
|
||||
|
||||
const linkData: APIButtonComponentWithURL = {
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
style: ButtonStyle.Link,
|
||||
disabled: true,
|
||||
url: 'https://google.com',
|
||||
};
|
||||
|
||||
expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData);
|
||||
|
||||
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
|
||||
});
|
||||
});
|
||||
});
|
||||
121
packages/builders/__tests__/components/selectMenu.test.ts
Normal file
121
packages/builders/__tests__/components/selectMenu.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v10';
|
||||
import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index';
|
||||
|
||||
const selectMenu = () => new SelectMenuBuilder();
|
||||
const selectMenuOption = () => new SelectMenuOptionBuilder();
|
||||
|
||||
const longStr = 'a'.repeat(256);
|
||||
|
||||
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')
|
||||
.setDefault(true)
|
||||
.setEmoji({ name: 'test' })
|
||||
.setDescription('description');
|
||||
expect(() => selectMenu().addOptions(option)).not.toThrowError();
|
||||
expect(() => selectMenu().setOptions(option)).not.toThrowError();
|
||||
expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError();
|
||||
expect(() =>
|
||||
selectMenu().addOptions({
|
||||
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', () => {
|
||||
expect(() => selectMenu().setCustomId(longStr)).toThrowError();
|
||||
expect(() => selectMenu().setMaxValues(30)).toThrowError();
|
||||
expect(() => selectMenu().setMinValues(-20)).toThrowError();
|
||||
// @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()
|
||||
.setLabel(longStr)
|
||||
.setValue(longStr)
|
||||
// @ts-expect-error
|
||||
.setDefault(-1)
|
||||
// @ts-expect-error
|
||||
.setEmoji({ name: 1 })
|
||||
.setDescription(longStr);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index';
|
||||
|
||||
const getBuilder = () => new ContextMenuCommandBuilder();
|
||||
|
||||
describe('Context Menu Commands', () => {
|
||||
describe('Assertions tests', () => {
|
||||
test('GIVEN valid name THEN does not throw error', () => {
|
||||
expect(() => ContextMenuCommandAssertions.validateName('ping')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid name THEN throw error', () => {
|
||||
expect(() => ContextMenuCommandAssertions.validateName(null)).toThrowError();
|
||||
|
||||
// Too short of a name
|
||||
expect(() => ContextMenuCommandAssertions.validateName('')).toThrowError();
|
||||
|
||||
// Invalid characters used
|
||||
expect(() => ContextMenuCommandAssertions.validateName('ABC123$%^&')).toThrowError();
|
||||
|
||||
// Too long of a name
|
||||
expect(() =>
|
||||
ContextMenuCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid type THEN does not throw error', () => {
|
||||
expect(() => ContextMenuCommandAssertions.validateType(3)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid type THEN throw error', () => {
|
||||
expect(() => ContextMenuCommandAssertions.validateType(null)).toThrowError();
|
||||
|
||||
// Out of range
|
||||
expect(() => ContextMenuCommandAssertions.validateType(1)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid required parameters THEN does not throw error', () => {
|
||||
expect(() => ContextMenuCommandAssertions.validateRequiredParameters('owo', 2)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid default_permission THEN does not throw error', () => {
|
||||
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid default_permission THEN throw error', () => {
|
||||
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(null)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ContextMenuCommandBuilder', () => {
|
||||
describe('Builder tests', () => {
|
||||
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
|
||||
expect(() => getBuilder().toJSON()).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setName('example').setType(3).toJSON()).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid name THEN throw error', () => {
|
||||
expect(() => getBuilder().setName('$$$')).toThrowError();
|
||||
|
||||
expect(() => getBuilder().setName(' ')).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid names THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().setName('A COMMAND')).not.toThrowError();
|
||||
|
||||
// Translation: a_command
|
||||
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
|
||||
|
||||
// Translation: thx (according to GTranslate)
|
||||
expect(() => getBuilder().setName('どうも')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid types THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setType(2)).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().setType(3)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setName('foo').setDefaultPermission(false)).not.toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
APIApplicationCommandBooleanOption,
|
||||
APIApplicationCommandChannelOption,
|
||||
APIApplicationCommandIntegerOption,
|
||||
APIApplicationCommandMentionableOption,
|
||||
APIApplicationCommandNumberOption,
|
||||
APIApplicationCommandRoleOption,
|
||||
APIApplicationCommandStringOption,
|
||||
APIApplicationCommandUserOption,
|
||||
ApplicationCommandOptionType,
|
||||
ChannelType,
|
||||
} from 'discord-api-types/v10';
|
||||
import {
|
||||
SlashCommandBooleanOption,
|
||||
SlashCommandChannelOption,
|
||||
SlashCommandIntegerOption,
|
||||
SlashCommandMentionableOption,
|
||||
SlashCommandNumberOption,
|
||||
SlashCommandRoleOption,
|
||||
SlashCommandStringOption,
|
||||
SlashCommandUserOption,
|
||||
} from '../../../src/index';
|
||||
|
||||
const getBooleanOption = () =>
|
||||
new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
|
||||
const getChannelOption = () =>
|
||||
new SlashCommandChannelOption()
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.addChannelTypes(ChannelType.GuildText);
|
||||
|
||||
const getStringOption = () =>
|
||||
new SlashCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
|
||||
const getIntegerOption = () =>
|
||||
new SlashCommandIntegerOption()
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.setMinValue(-1)
|
||||
.setMaxValue(10);
|
||||
|
||||
const getNumberOption = () =>
|
||||
new SlashCommandNumberOption()
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.setMinValue(-1.23)
|
||||
.setMaxValue(10);
|
||||
|
||||
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
|
||||
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
|
||||
const getMentionableOption = () =>
|
||||
new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
|
||||
describe('Application Command toJSON() results', () => {
|
||||
test('GIVEN a boolean option THEN calling toJSON should return a valid JSON', () => {
|
||||
expect(getBooleanOption().toJSON()).toEqual<APIApplicationCommandBooleanOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Boolean,
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a channel option THEN calling toJSON should return a valid JSON', () => {
|
||||
expect(getChannelOption().toJSON()).toEqual<APIApplicationCommandChannelOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Channel,
|
||||
required: true,
|
||||
channel_types: [ChannelType.GuildText],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a integer option THEN calling toJSON should return a valid JSON', () => {
|
||||
expect(getIntegerOption().toJSON()).toEqual<APIApplicationCommandIntegerOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
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().addChoices({ name: 'uwu', value: 1 }).toJSON(),
|
||||
).toEqual<APIApplicationCommandIntegerOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: -1,
|
||||
choices: [{ name: 'uwu', value: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a mentionable option THEN calling toJSON should return a valid JSON', () => {
|
||||
expect(getMentionableOption().toJSON()).toEqual<APIApplicationCommandMentionableOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Mentionable,
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a number option THEN calling toJSON should return a valid JSON', () => {
|
||||
expect(getNumberOption().toJSON()).toEqual<APIApplicationCommandNumberOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: -1.23,
|
||||
});
|
||||
|
||||
expect(getNumberOption().setAutocomplete(true).setChoices().toJSON()).toEqual<APIApplicationCommandNumberOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
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().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', () => {
|
||||
expect(getRoleOption().toJSON()).toEqual<APIApplicationCommandRoleOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Role,
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a string option THEN calling toJSON should return a valid JSON', () => {
|
||||
expect(getStringOption().toJSON()).toEqual<APIApplicationCommandStringOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
});
|
||||
|
||||
expect(getStringOption().setAutocomplete(true).setChoices().toJSON()).toEqual<APIApplicationCommandStringOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
autocomplete: true,
|
||||
// @ts-expect-error TODO: you *can* send an empty array with autocomplete: true, should correct that in types
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(
|
||||
getStringOption().addChoices({ name: 'uwu', value: '1' }).toJSON(),
|
||||
).toEqual<APIApplicationCommandStringOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
choices: [{ name: 'uwu', value: '1' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN a user option THEN calling toJSON should return a valid JSON', () => {
|
||||
expect(getUserOption().toJSON()).toEqual<APIApplicationCommandUserOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,480 @@
|
||||
import { APIApplicationCommandOptionChoice, ChannelType } from 'discord-api-types/v10';
|
||||
import {
|
||||
SlashCommandAssertions,
|
||||
SlashCommandBooleanOption,
|
||||
SlashCommandBuilder,
|
||||
SlashCommandChannelOption,
|
||||
SlashCommandIntegerOption,
|
||||
SlashCommandMentionableOption,
|
||||
SlashCommandNumberOption,
|
||||
SlashCommandRoleOption,
|
||||
SlashCommandAttachmentOption,
|
||||
SlashCommandStringOption,
|
||||
SlashCommandSubcommandBuilder,
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
SlashCommandUserOption,
|
||||
} from '../../../src/index';
|
||||
|
||||
const largeArray = Array.from({ length: 26 }, () => 1 as unknown as APIApplicationCommandOptionChoice);
|
||||
|
||||
const getBuilder = () => new SlashCommandBuilder();
|
||||
const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command');
|
||||
const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123');
|
||||
const getIntegerOption = () => new SlashCommandIntegerOption().setName('owo').setDescription('Testing 123');
|
||||
const getNumberOption = () => new SlashCommandNumberOption().setName('owo').setDescription('Testing 123');
|
||||
const getBooleanOption = () => new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123');
|
||||
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');
|
||||
|
||||
class Collection {
|
||||
public get [Symbol.toStringTag]() {
|
||||
return 'Map';
|
||||
}
|
||||
}
|
||||
|
||||
describe('Slash Commands', () => {
|
||||
describe('Assertions tests', () => {
|
||||
test('GIVEN valid name THEN does not throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateName('ping')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid name THEN throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateName(null)).toThrowError();
|
||||
|
||||
// Too short of a name
|
||||
expect(() => SlashCommandAssertions.validateName('')).toThrowError();
|
||||
|
||||
// Invalid characters used
|
||||
expect(() => SlashCommandAssertions.validateName('ABC123$%^&')).toThrowError();
|
||||
|
||||
// Too long of a name
|
||||
expect(() =>
|
||||
SlashCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid description THEN does not throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateDescription('This is an OwO moment fur sure!~')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid description THEN throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateDescription(null)).toThrowError();
|
||||
|
||||
// Too short of a description
|
||||
expect(() => SlashCommandAssertions.validateDescription('')).toThrowError();
|
||||
|
||||
// Too long of a description
|
||||
expect(() =>
|
||||
SlashCommandAssertions.validateDescription(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Magnam autem libero expedita vitae accusamus nostrum ipsam tempore repudiandae deserunt ipsum facilis, velit fugiat facere accusantium, explicabo corporis aliquam non quos.',
|
||||
),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid default_permission THEN does not throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid default_permission THEN throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateDefaultPermission(null)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid array of options or choices THEN does not throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength([])).not.toThrowError();
|
||||
|
||||
expect(() => SlashCommandAssertions.validateChoicesLength(25, [])).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid options or choices THEN throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength(null)).toThrowError();
|
||||
|
||||
// Given an array that's too big
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength(largeArray)).toThrowError();
|
||||
|
||||
expect(() => SlashCommandAssertions.validateChoicesLength(1, largeArray)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid required parameters THEN does not throw error', () => {
|
||||
expect(() =>
|
||||
SlashCommandAssertions.validateRequiredParameters(
|
||||
'owo',
|
||||
'My fancy command that totally exists, to test assertions',
|
||||
[],
|
||||
),
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SlashCommandBuilder', () => {
|
||||
describe('Builder with no options', () => {
|
||||
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
|
||||
expect(() => getBuilder().toJSON()).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Builder with simple options', () => {
|
||||
test('GIVEN valid builder with options THEN does not throw error', () => {
|
||||
expect(() =>
|
||||
getBuilder()
|
||||
.setName('example')
|
||||
.setDescription('Example command')
|
||||
.addBooleanOption((boolean) =>
|
||||
boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true),
|
||||
)
|
||||
.addChannelOption((channel) => channel.setName('iscool').setDescription('Are we cool or what?'))
|
||||
.addMentionableOption((mentionable) => mentionable.setName('iscool').setDescription('Are we cool or what?'))
|
||||
.addRoleOption((role) => role.setName('iscool').setDescription('Are we cool or what?'))
|
||||
.addUserOption((user) => user.setName('iscool').setDescription('Are we cool or what?'))
|
||||
.addIntegerOption((integer) =>
|
||||
integer
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.addChoices({ name: 'Very cool', value: 1_000 }),
|
||||
)
|
||||
.addNumberOption((number) =>
|
||||
number
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.addChoices({ name: 'Very cool', value: 1.5 }),
|
||||
)
|
||||
.addStringOption((string) =>
|
||||
string
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.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),
|
||||
)
|
||||
.addNumberOption((number) =>
|
||||
number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
||||
)
|
||||
.addStringOption((string) =>
|
||||
string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
||||
)
|
||||
.toJSON(),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => {
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addStringOption(getStringOption().setAutocomplete('not a boolean'))).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
getStringOption()
|
||||
.setAutocomplete(true)
|
||||
.addChoices(
|
||||
{ name: 'Fancy Pants', value: 'fp_1' },
|
||||
{ name: 'Fancy Shoes', value: 'fs_1' },
|
||||
{ name: 'The Whole shebang', value: 'all' },
|
||||
),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const option = getStringOption();
|
||||
Reflect.set(option, 'autocomplete', true);
|
||||
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
|
||||
return option.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const option = getNumberOption();
|
||||
Reflect.set(option, 'autocomplete', true);
|
||||
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
|
||||
return option.toJSON();
|
||||
}).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
const option = getIntegerOption();
|
||||
Reflect.set(option, 'autocomplete', true);
|
||||
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
|
||||
return option.toJSON();
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addChannelOption(getChannelOption().addChannelTypes(ChannelType.GuildText)),
|
||||
).not.toThrowError();
|
||||
|
||||
expect(() => {
|
||||
getBuilder().addChannelOption(
|
||||
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().addChannelTypes(100))).toThrowError();
|
||||
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100, 200))).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue('test'))).toThrowError();
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue('test'))).toThrowError();
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue('test'))).toThrowError();
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue('test'))).toThrowError();
|
||||
|
||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1.5))).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => {
|
||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1))).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue(1.5))).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue(1))).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue(1.5))).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an already built builder THEN does not throw an error', () => {
|
||||
expect(() => getBuilder().addStringOption(getStringOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addIntegerOption(getIntegerOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addNumberOption(getNumberOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addBooleanOption(getBooleanOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addUserOption(getUserOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addRoleOption(getRoleOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addAttachmentOption(getAttachmentOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addMentionableOption(getMentionableOption())).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN no valid return for an addOption method THEN throw error', () => {
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addBooleanOption()).toThrowError();
|
||||
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addBooleanOption(getRoleOption())).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid name THEN throw error', () => {
|
||||
expect(() => getBuilder().setName('TEST_COMMAND')).toThrowError();
|
||||
|
||||
expect(() => getBuilder().setName('ĂĂĂĂĂĂ')).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid names THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
|
||||
|
||||
// Translation: a_command
|
||||
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
|
||||
|
||||
// Translation: thx (according to GTranslate)
|
||||
expect(() => getBuilder().setName('どうも')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid returns for builder THEN throw error', () => {
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addBooleanOption(true)).toThrowError();
|
||||
|
||||
expect(() => getBuilder().addBooleanOption(null)).toThrowError();
|
||||
|
||||
expect(() => getBuilder().addBooleanOption(undefined)).toThrowError();
|
||||
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addBooleanOption(() => SlashCommandStringOption)).toThrowError();
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addBooleanOption(() => new Collection())).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError();
|
||||
});
|
||||
|
||||
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()),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
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({ name: 'owo', value: 'uwu' })),
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Builder with subcommand (group) options', () => {
|
||||
test('GIVEN builder with subcommand group THEN does not throw error', () => {
|
||||
expect(() =>
|
||||
getNamedBuilder().addSubcommandGroup((group) => group.setName('group').setDescription('Group us together!')),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN builder with subcommand THEN does not throw error', () => {
|
||||
expect(() =>
|
||||
getNamedBuilder().addSubcommand((subcommand) =>
|
||||
subcommand.setName('boop').setDescription('Boops a fellow nerd (you)'),
|
||||
),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN builder with already built subcommand group THEN does not throw error', () => {
|
||||
expect(() => getNamedBuilder().addSubcommandGroup(getSubcommandGroup())).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN builder with already built subcommand THEN does not throw error', () => {
|
||||
expect(() => getNamedBuilder().addSubcommand(getSubcommand())).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN builder with already built subcommand with options THEN does not throw error', () => {
|
||||
expect(() =>
|
||||
getNamedBuilder().addSubcommand(getSubcommand().addBooleanOption(getBooleanOption())),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN builder with a subcommand that tries to add an invalid result THEN throw error', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error Checking if check works JS-side too
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
||||
getNamedBuilder().addSubcommand(getSubcommand()).addInteger(getInteger()),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN no valid return for an addSubcommand(Group) method THEN throw error', () => {
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addSubcommandGroup()).toThrowError();
|
||||
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addSubcommand()).toThrowError();
|
||||
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getBuilder().addSubcommand(getSubcommandGroup())).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subcommand group builder', () => {
|
||||
test('GIVEN no valid subcommand THEN throw error', () => {
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getSubcommandGroup().addSubcommand()).toThrowError();
|
||||
|
||||
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
|
||||
expect(() => getSubcommandGroup().addSubcommand(getSubcommandGroup())).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a valid subcommand THEN does not throw an error', () => {
|
||||
expect(() =>
|
||||
getSubcommandGroup()
|
||||
.addSubcommand((sub) => sub.setName('sub').setDescription('Testing 123'))
|
||||
.toJSON(),
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Subcommand builder', () => {
|
||||
test('GIVEN a valid subcommand with options THEN does not throw error', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
409
packages/builders/__tests__/messages/embed.test.ts
Normal file
409
packages/builders/__tests__/messages/embed.test.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
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 EmbedBuilder({
|
||||
title: alpha,
|
||||
description: alpha,
|
||||
fields: [{ name: alpha, value: alpha }],
|
||||
author: { name: alpha },
|
||||
footer: { text: alpha },
|
||||
});
|
||||
|
||||
expect(embedLength(embed.data)).toBe(alpha.length * 6);
|
||||
});
|
||||
|
||||
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
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 EmbedBuilder({ title: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTitle('foo');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder({ title: 'foo' });
|
||||
embed.setTitle(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ title: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid title THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setTitle('a'.repeat(257))).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed description', () => {
|
||||
test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => {
|
||||
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 EmbedBuilder();
|
||||
embed.setDescription('foo');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder({ description: 'foo' });
|
||||
embed.setDescription(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ description: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid description THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setDescription('a'.repeat(4097))).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed URL', () => {
|
||||
test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder({ url: 'https://discord.js.org/' });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
url: 'https://discord.js.org/',
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setURL('https://discord.js.org/');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
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 EmbedBuilder({ url: 'https://discord.js.org' });
|
||||
embed.setURL(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ url: undefined });
|
||||
});
|
||||
|
||||
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
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 EmbedBuilder({ color: 0xff0000 });
|
||||
expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
|
||||
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 EmbedBuilder({ color: 0xff0000 });
|
||||
embed.setColor(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ color: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid color THEN throws error', () => {
|
||||
const embed = new 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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Timestamp', () => {
|
||||
const now = new Date();
|
||||
|
||||
test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => {
|
||||
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 EmbedBuilder();
|
||||
embed.setTimestamp(now);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTimestamp(now.getTime());
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTimestamp();
|
||||
|
||||
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 EmbedBuilder({ timestamp: now.toISOString() });
|
||||
embed.setTimestamp(null);
|
||||
|
||||
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 EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setThumbnail('https://discord.js.org/static/logo.svg');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
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 EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
embed.setThumbnail(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setThumbnail('owo')).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Image', () => {
|
||||
test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
image: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setImage('https://discord.js.org/static/logo.svg');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
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 EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } });
|
||||
embed.setImage(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ image: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid image THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setImage('owo')).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Author', () => {
|
||||
test('GIVEN an embed with a pre-defined author THEN returns valid toJSON data', () => {
|
||||
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({
|
||||
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 EmbedBuilder();
|
||||
embed.setAuthor({
|
||||
name: 'Wumpus',
|
||||
iconURL: 'https://discord.js.org/static/logo.svg',
|
||||
url: 'https://discord.js.org',
|
||||
});
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
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 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({ author: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid author name THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Footer', () => {
|
||||
test('GIVEN an embed with a pre-defined footer THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder({
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
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 EmbedBuilder();
|
||||
embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' });
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
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 EmbedBuilder({
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
embed.setFooter(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ footer: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with invalid footer text THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setFooter({ text: 'a'.repeat(2049) })).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Fields', () => {
|
||||
test('GIVEN an embed with a pre-defined field THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder({
|
||||
fields: [{ name: 'foo', value: 'bar' }],
|
||||
});
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
fields: [{ name: 'foo', value: 'bar' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields({ name: 'foo', value: 'bar' });
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
|
||||
|
||||
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
|
||||
fields: [{ name: 'foo', value: 'baz', inline: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
||||
|
||||
expect(() =>
|
||||
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
||||
|
||||
expect(() =>
|
||||
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
).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 EmbedBuilder();
|
||||
|
||||
expect(() =>
|
||||
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field name THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field name length THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field value length THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
199
packages/builders/__tests__/messages/formatters.test.ts
Normal file
199
packages/builders/__tests__/messages/formatters.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
blockQuote,
|
||||
bold,
|
||||
channelMention,
|
||||
codeBlock,
|
||||
Faces,
|
||||
formatEmoji,
|
||||
hideLinkEmbed,
|
||||
hyperlink,
|
||||
inlineCode,
|
||||
italic,
|
||||
quote,
|
||||
roleMention,
|
||||
spoiler,
|
||||
strikethrough,
|
||||
time,
|
||||
TimestampStyles,
|
||||
underscore,
|
||||
userMention,
|
||||
} from '../../src';
|
||||
|
||||
describe('Message formatters', () => {
|
||||
describe('codeBlock', () => {
|
||||
test('GIVEN "discord.js" with no language THEN returns "```\\ndiscord.js```"', () => {
|
||||
expect<'```\ndiscord.js```'>(codeBlock('discord.js')).toBe('```\ndiscord.js```');
|
||||
});
|
||||
|
||||
test('GIVEN "discord.js" with "js" as language THEN returns "```js\\ndiscord.js```"', () => {
|
||||
expect<'```js\ndiscord.js```'>(codeBlock('js', 'discord.js')).toBe('```js\ndiscord.js```');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inlineCode', () => {
|
||||
test('GIVEN "discord.js" THEN returns "`discord.js`"', () => {
|
||||
expect<'`discord.js`'>(inlineCode('discord.js')).toBe('`discord.js`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('italic', () => {
|
||||
test('GIVEN "discord.js" THEN returns "_discord.js_"', () => {
|
||||
expect<'_discord.js_'>(italic('discord.js')).toBe('_discord.js_');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bold', () => {
|
||||
test('GIVEN "discord.js" THEN returns "**discord.js**"', () => {
|
||||
expect<'**discord.js**'>(bold('discord.js')).toBe('**discord.js**');
|
||||
});
|
||||
});
|
||||
|
||||
describe('underscore', () => {
|
||||
test('GIVEN "discord.js" THEN returns "__discord.js__"', () => {
|
||||
expect<'__discord.js__'>(underscore('discord.js')).toBe('__discord.js__');
|
||||
});
|
||||
});
|
||||
|
||||
describe('strikethrough', () => {
|
||||
test('GIVEN "discord.js" THEN returns "~~discord.js~~"', () => {
|
||||
expect<'~~discord.js~~'>(strikethrough('discord.js')).toBe('~~discord.js~~');
|
||||
});
|
||||
});
|
||||
|
||||
describe('quote', () => {
|
||||
test('GIVEN "discord.js" THEN returns "> discord.js"', () => {
|
||||
expect<'> discord.js'>(quote('discord.js')).toBe('> discord.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('blockQuote', () => {
|
||||
test('GIVEN "discord.js" THEN returns ">>> discord.js"', () => {
|
||||
expect<'>>> discord.js'>(blockQuote('discord.js')).toBe('>>> discord.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hideLinkEmbed', () => {
|
||||
test('GIVEN "https://discord.js.org" THEN returns "<https://discord.js.org>"', () => {
|
||||
expect<'<https://discord.js.org>'>(hideLinkEmbed('https://discord.js.org')).toBe('<https://discord.js.org>');
|
||||
});
|
||||
|
||||
test('GIVEN new URL("https://discord.js.org") THEN returns "<https://discord.js.org>"', () => {
|
||||
expect<`<${string}>`>(hideLinkEmbed(new URL('https://discord.js.org/'))).toBe('<https://discord.js.org/>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hyperlink', () => {
|
||||
test('GIVEN content and string URL THEN returns "[content](url)"', () => {
|
||||
expect<'[discord.js](https://discord.js.org)'>(hyperlink('discord.js', 'https://discord.js.org')).toBe(
|
||||
'[discord.js](https://discord.js.org)',
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN content and URL THEN returns "[content](url)"', () => {
|
||||
expect<`[discord.js](${string})`>(hyperlink('discord.js', new URL('https://discord.js.org'))).toBe(
|
||||
'[discord.js](https://discord.js.org/)',
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN content, string URL, and title THEN returns "[content](url "title")"', () => {
|
||||
expect<'[discord.js](https://discord.js.org "Official Documentation")'>(
|
||||
hyperlink('discord.js', 'https://discord.js.org', 'Official Documentation'),
|
||||
).toBe('[discord.js](https://discord.js.org "Official Documentation")');
|
||||
});
|
||||
|
||||
test('GIVEN content, URL, and title THEN returns "[content](url "title")"', () => {
|
||||
expect<`[discord.js](${string} "Official Documentation")`>(
|
||||
hyperlink('discord.js', new URL('https://discord.js.org'), 'Official Documentation'),
|
||||
).toBe('[discord.js](https://discord.js.org/ "Official Documentation")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('spoiler', () => {
|
||||
test('GIVEN "discord.js" THEN returns "||discord.js||"', () => {
|
||||
expect<'||discord.js||'>(spoiler('discord.js')).toBe('||discord.js||');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mentions', () => {
|
||||
describe('userMention', () => {
|
||||
test('GIVEN userId THEN returns "<@[userId]>"', () => {
|
||||
expect(userMention('139836912335716352')).toBe('<@139836912335716352>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('channelMention', () => {
|
||||
test('GIVEN channelId THEN returns "<#[channelId]>"', () => {
|
||||
expect(channelMention('829924760309334087')).toBe('<#829924760309334087>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('roleMention', () => {
|
||||
test('GIVEN roleId THEN returns "<&[roleId]>"', () => {
|
||||
expect(roleMention('815434166602170409')).toBe('<@&815434166602170409>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatEmoji', () => {
|
||||
test('GIVEN static emojiId THEN returns "<:_:${emojiId}>"', () => {
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji('851461487498493952')).toBe('<:_:851461487498493952>');
|
||||
});
|
||||
|
||||
test('GIVEN static emojiId WITH animated explicitly false THEN returns "<:_:[emojiId]>"', () => {
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji('851461487498493952', false)).toBe('<:_:851461487498493952>');
|
||||
});
|
||||
|
||||
test('GIVEN animated emojiId THEN returns "<a:_:${emojiId}>"', () => {
|
||||
expect<`<a:_:827220205352255549>`>(formatEmoji('827220205352255549', true)).toBe('<a:_:827220205352255549>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('time', () => {
|
||||
test('GIVEN no arguments THEN returns "<t:${bigint}>"', () => {
|
||||
jest.useFakeTimers('modern');
|
||||
jest.setSystemTime(1566424897579);
|
||||
|
||||
expect<`<t:${bigint}>`>(time()).toBe('<t:1566424897>');
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('GIVEN a date THEN returns "<t:${bigint}>"', () => {
|
||||
expect<`<t:${bigint}>`>(time(new Date(1867424897579))).toBe('<t:1867424897>');
|
||||
});
|
||||
|
||||
test('GIVEN a date and a style from string THEN returns "<t:${bigint}:${style}>"', () => {
|
||||
expect<`<t:${bigint}:d>`>(time(new Date(1867424897579), 'd')).toBe('<t:1867424897:d>');
|
||||
});
|
||||
|
||||
test('GIVEN a date and a format from enum THEN returns "<t:${bigint}:${style}>"', () => {
|
||||
expect<`<t:${bigint}:R>`>(time(new Date(1867424897579), TimestampStyles.RelativeTime)).toBe('<t:1867424897:R>');
|
||||
});
|
||||
|
||||
test('GIVEN a date THEN returns "<t:${time}>"', () => {
|
||||
expect<'<t:1867424897>'>(time(1867424897)).toBe('<t:1867424897>');
|
||||
});
|
||||
|
||||
test('GIVEN a date and a style from string THEN returns "<t:${time}:${style}>"', () => {
|
||||
expect<'<t:1867424897:d>'>(time(1867424897, 'd')).toBe('<t:1867424897:d>');
|
||||
});
|
||||
|
||||
test('GIVEN a date and a format from enum THEN returns "<t:${time}:${style}>"', () => {
|
||||
expect<'<t:1867424897:R>'>(time(1867424897, TimestampStyles.RelativeTime)).toBe('<t:1867424897:R>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Faces', () => {
|
||||
test('GIVEN Faces.Shrug THEN returns "¯\\_(ツ)\\_/¯"', () => {
|
||||
expect<'¯\\_(ツ)\\_/¯'>(Faces.Shrug).toBe('¯\\_(ツ)\\_/¯');
|
||||
});
|
||||
|
||||
test('GIVEN Faces.Tableflip THEN returns "(╯°□°)╯︵ ┻━┻"', () => {
|
||||
expect<'(╯°□°)╯︵ ┻━┻'>(Faces.Tableflip).toBe('(╯°□°)╯︵ ┻━┻');
|
||||
});
|
||||
|
||||
test('GIVEN Faces.Unflip THEN returns "┬─┬ ノ( ゜-゜ノ)"', () => {
|
||||
expect<'┬─┬ ノ( ゜-゜ノ)'>(Faces.Unflip).toBe('┬─┬ ノ( ゜-゜ノ)');
|
||||
});
|
||||
});
|
||||
});
|
||||
18
packages/builders/babel.config.js
Normal file
18
packages/builders/babel.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @type {import('@babel/core').TransformOptions}
|
||||
*/
|
||||
module.exports = {
|
||||
parserOpts: { strictMode: true },
|
||||
sourceMaps: 'inline',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: { node: 'current' },
|
||||
modules: 'commonjs',
|
||||
},
|
||||
],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['babel-plugin-transform-typescript-metadata', ['@babel/plugin-proposal-decorators', { legacy: true }]],
|
||||
};
|
||||
62
packages/builders/cliff.toml
Normal file
62
packages/builders/cliff.toml
Normal file
@@ -0,0 +1,62 @@
|
||||
[changelog]
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
body = """
|
||||
{% if version %}\
|
||||
# [{{ version | trim_start_matches(pat="v") }}]\
|
||||
{% if previous %}\
|
||||
{% if previous.version %}\
|
||||
(https://github.com/discordjs/discord.js/compare/{{ previous.version }}...{{ version }})\
|
||||
{% else %}
|
||||
(https://github.com/discordjs/discord.js/tree/{{ version }})\
|
||||
{% endif %}\
|
||||
{% endif %} \
|
||||
- ({{ timestamp | date(format="%Y-%m-%d") }})
|
||||
{% else %}\
|
||||
# [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
## {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.breaking %}\
|
||||
[**breaking**] \
|
||||
{% endif %}\
|
||||
{% if commit.scope %}\
|
||||
**{{commit.scope}}:** \
|
||||
{% endif %}\
|
||||
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/discordjs/discord.js/commit/{{ commit.id }}))\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
trim = true
|
||||
footer = ""
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features"},
|
||||
{ message = "^fix", group = "Bug Fixes"},
|
||||
{ message = "^docs", group = "Documentation"},
|
||||
{ message = "^perf", group = "Performance"},
|
||||
{ message = "^refactor", group = "Refactor"},
|
||||
{ message = "^typings", group = "Typings"},
|
||||
{ message = "^types", group = "Typings"},
|
||||
{ message = ".*deprecated", body = ".*deprecated", group = "Deprecation"},
|
||||
{ message = "^revert", skip = true},
|
||||
{ message = "^style", group = "Styling"},
|
||||
{ message = "^test", group = "Testing"},
|
||||
{ message = "^chore", skip = true},
|
||||
{ message = "^ci", skip = true},
|
||||
{ message = "^build", skip = true},
|
||||
{ body = ".*security", group = "Security"},
|
||||
]
|
||||
filter_commits = true
|
||||
tag_pattern = "@discordjs\\/builders@.*"
|
||||
skip_tags = "v[0-9]*|11|12"
|
||||
ignore_tags = ""
|
||||
topo_order = false
|
||||
sort_commits = "newest"
|
||||
10
packages/builders/codecov.yml
Normal file
10
packages/builders/codecov.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 70%
|
||||
threshold: 5%
|
||||
patch:
|
||||
default:
|
||||
target: 70%
|
||||
threshold: 5%
|
||||
1
packages/builders/docs/README.md
Normal file
1
packages/builders/docs/README.md
Normal file
@@ -0,0 +1 @@
|
||||
## [View the documentation here.](https://discord.js.org/#/docs/builders)
|
||||
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();
|
||||
```
|
||||
19
packages/builders/jest.config.js
Normal file
19
packages/builders/jest.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @type {import('@jest/types').Config.InitialOptions}
|
||||
*/
|
||||
module.exports = {
|
||||
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
|
||||
testEnvironment: 'node',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts'],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'clover'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 70,
|
||||
lines: 70,
|
||||
statements: 70,
|
||||
},
|
||||
},
|
||||
coveragePathIgnorePatterns: ['src/index.ts'],
|
||||
};
|
||||
89
packages/builders/package.json
Normal file
89
packages/builders/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "@discordjs/builders",
|
||||
"version": "0.14.0-dev",
|
||||
"description": "A set of builders that you can use when creating your bot",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"test": "jest --pass-with-no-tests",
|
||||
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
|
||||
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix",
|
||||
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && node scripts/docs.mjs",
|
||||
"prepublishOnly": "yarn build && yarn lint && yarn test",
|
||||
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/builders/*'"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "src",
|
||||
"test": "__tests__"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"contributors": [
|
||||
"Vlad Frangu <kingdgrizzle@gmail.com>",
|
||||
"Crawl <icrawltogo@gmail.com>",
|
||||
"Amish Shah <amishshah.2k@gmail.com>",
|
||||
"SpaceEEC <spaceeec@yahoo.com>",
|
||||
"Antonio Roman <kyradiscord@gmail.com>"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"discord",
|
||||
"api",
|
||||
"bot",
|
||||
"client",
|
||||
"node",
|
||||
"discordapp",
|
||||
"discordjs"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/discordjs/discord.js.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/discordjs/discord.js/issues"
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"dependencies": {
|
||||
"@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.17.9",
|
||||
"@babel/plugin-proposal-decorators": "^7.17.9",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@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.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"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
7
packages/builders/scripts/docs.mjs
Normal file
7
packages/builders/scripts/docs.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
import { runGenerator } from '@discordjs/ts-docgen';
|
||||
|
||||
runGenerator({
|
||||
existingOutput: 'docs/typedoc-out.json',
|
||||
custom: 'docs/index.yml',
|
||||
output: 'docs/docs.json',
|
||||
});
|
||||
62
packages/builders/src/components/ActionRow.ts
Normal file
62
packages/builders/src/components/ActionRow.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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 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 ActionRowBuilder<T extends AnyComponentBuilder> extends ComponentBuilder<
|
||||
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
||||
> {
|
||||
/**
|
||||
* The components within this action row
|
||||
*/
|
||||
public readonly components: T[];
|
||||
|
||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
||||
super({ type: ComponentType.ActionRow, ...data });
|
||||
this.components = (components?.map((c) => createComponentBuilder(c)) ?? []) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds components to this action row.
|
||||
* @param components The components to add to this action row.
|
||||
* @returns
|
||||
*/
|
||||
public addComponents(...components: T[]) {
|
||||
this.components.push(...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the components in this action row
|
||||
* @param components The components to set this row to
|
||||
*/
|
||||
public setComponents(...components: T[]) {
|
||||
this.components.splice(0, this.components.length, ...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIActionRowComponent<ReturnType<T['toJSON']>> {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
} as APIActionRowComponent<ReturnType<T['toJSON']>>;
|
||||
}
|
||||
}
|
||||
76
packages/builders/src/components/Assertions.ts
Normal file
76
packages/builders/src/components/Assertions.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
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 = s.string.lengthGe(1).lengthLe(100);
|
||||
|
||||
export const emojiValidator = s.object({
|
||||
id: s.string,
|
||||
name: s.string,
|
||||
animated: s.boolean,
|
||||
}).partial.strict;
|
||||
|
||||
export const disabledValidator = s.boolean;
|
||||
|
||||
export const buttonLabelValidator = s.string.lengthGe(1).lengthLe(80);
|
||||
|
||||
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
|
||||
|
||||
export const placeholderValidator = s.string.lengthLe(150);
|
||||
export const minMaxValidator = s.number.int.ge(0).le(25);
|
||||
|
||||
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: SelectMenuOptionBuilder[], customId?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
optionsValidator.parse(options);
|
||||
}
|
||||
|
||||
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 = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:', 'discord:'],
|
||||
});
|
||||
|
||||
export function validateRequiredButtonParameters(
|
||||
style?: ButtonStyle,
|
||||
label?: string,
|
||||
emoji?: APIMessageComponentEmoji,
|
||||
customId?: string,
|
||||
url?: string,
|
||||
) {
|
||||
if (url && customId) {
|
||||
throw new RangeError('URL and custom id are mutually exclusive');
|
||||
}
|
||||
|
||||
if (!label && !emoji) {
|
||||
throw new RangeError('Buttons must have a label and/or an emoji');
|
||||
}
|
||||
|
||||
if (style === ButtonStyle.Link) {
|
||||
if (!url) {
|
||||
throw new RangeError('Link buttons must have a url');
|
||||
}
|
||||
} else if (url) {
|
||||
throw new RangeError('Non-link buttons cannot have a url');
|
||||
}
|
||||
}
|
||||
28
packages/builders/src/components/Component.ts
Normal file
28
packages/builders/src/components/Component.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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 abstract class ComponentBuilder<
|
||||
DataType extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>,
|
||||
> implements JSONEncodable<AnyAPIActionRowComponent>
|
||||
{
|
||||
/**
|
||||
* The API data associated with this component
|
||||
*/
|
||||
public readonly data: Partial<DataType>;
|
||||
|
||||
public abstract toJSON(): AnyAPIActionRowComponent;
|
||||
|
||||
public constructor(data: Partial<DataType>) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
41
packages/builders/src/components/Components.ts
Normal file
41
packages/builders/src/components/Components.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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]: 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 createComponentBuilder<T extends keyof MappedComponentTypes>(
|
||||
data: (APIMessageComponent | APIModalComponent) & { type: T },
|
||||
): MappedComponentTypes[T];
|
||||
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 ActionRowBuilder(data);
|
||||
case ComponentType.Button:
|
||||
return new ButtonBuilder(data);
|
||||
case ComponentType.SelectMenu:
|
||||
return new SelectMenuBuilder(data);
|
||||
case ComponentType.TextInput:
|
||||
return new TextInputBuilder(data);
|
||||
default:
|
||||
// @ts-expect-error
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
68
packages/builders/src/components/selectMenu/SelectMenu.ts
Normal file
68
packages/builders/src/components/selectMenu/SelectMenu.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* Represents a validated select menu component
|
||||
*/
|
||||
export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
|
||||
public override setPlaceholder(placeholder: string) {
|
||||
return super.setPlaceholder(placeholderValidator.parse(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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public override toJSON(): APISelectMenuComponent {
|
||||
validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
import {
|
||||
defaultValidator,
|
||||
emojiValidator,
|
||||
labelValueValidator,
|
||||
validateRequiredSelectMenuOptionParameters,
|
||||
} from '../Assertions';
|
||||
|
||||
/**
|
||||
* Represents a validated option within a select menu component
|
||||
*/
|
||||
export class SelectMenuOptionBuilder extends UnsafeSelectMenuOptionBuilder {
|
||||
public override setDescription(description: string) {
|
||||
return super.setDescription(labelValueValidator.parse(description));
|
||||
}
|
||||
|
||||
public override setDefault(isDefault = true) {
|
||||
return super.setDefault(defaultValidator.parse(isDefault));
|
||||
}
|
||||
|
||||
public override setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
return super.setEmoji(emojiValidator.parse(emoji));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
41
packages/builders/src/index.ts
Normal file
41
packages/builders/src/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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/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';
|
||||
export * from './interactions/slashCommands/SlashCommandSubcommands';
|
||||
export * from './interactions/slashCommands/options/boolean';
|
||||
export * from './interactions/slashCommands/options/channel';
|
||||
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';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ApplicationCommandType } from 'discord-api-types/v10';
|
||||
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder';
|
||||
|
||||
const namePredicate = s.string
|
||||
.lengthGe(1)
|
||||
.lengthLe(32)
|
||||
.regex(/^( *[\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+ *)+$/u);
|
||||
|
||||
const typePredicate = s.union(s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message));
|
||||
|
||||
const booleanPredicate = s.boolean;
|
||||
|
||||
export function validateDefaultPermission(value: unknown): asserts value is boolean {
|
||||
booleanPredicate.parse(value);
|
||||
}
|
||||
|
||||
export function validateName(name: unknown): asserts name is string {
|
||||
namePredicate.parse(name);
|
||||
}
|
||||
|
||||
export function validateType(type: unknown): asserts type is ContextMenuCommandType {
|
||||
typePredicate.parse(type);
|
||||
}
|
||||
|
||||
export function validateRequiredParameters(name: string, type: number) {
|
||||
// Assert name matches all conditions
|
||||
validateName(name);
|
||||
|
||||
// Assert type is valid
|
||||
validateType(type);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { ApplicationCommandType, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10';
|
||||
import { validateRequiredParameters, validateName, validateType, validateDefaultPermission } from './Assertions';
|
||||
|
||||
export class ContextMenuCommandBuilder {
|
||||
/**
|
||||
* The name of this context menu command
|
||||
*/
|
||||
public readonly name: string = undefined!;
|
||||
|
||||
/**
|
||||
* The type of this context menu command
|
||||
*/
|
||||
public readonly type: ContextMenuCommandType = undefined!;
|
||||
|
||||
/**
|
||||
* Whether the command is enabled by default when the app is added to a guild
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
public readonly defaultPermission: boolean | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Sets the name
|
||||
*
|
||||
* @param name The name
|
||||
*/
|
||||
public setName(name: string) {
|
||||
// Assert the name matches the conditions
|
||||
validateName(name);
|
||||
|
||||
Reflect.set(this, 'name', name);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type
|
||||
*
|
||||
* @param type The type
|
||||
*/
|
||||
public setType(type: ContextMenuCommandType) {
|
||||
// Assert the type is valid
|
||||
validateType(type);
|
||||
|
||||
Reflect.set(this, 'type', type);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the command is enabled by default when the application is added to a guild.
|
||||
*
|
||||
* **Note**: If set to `false`, you will have to later `PUT` the permissions for this command.
|
||||
*
|
||||
* @param value Whether or not to enable this command by default
|
||||
*
|
||||
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
|
||||
*/
|
||||
public setDefaultPermission(value: boolean) {
|
||||
// Assert the value matches the conditions
|
||||
validateDefaultPermission(value);
|
||||
|
||||
Reflect.set(this, 'defaultPermission', value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final data that should be sent to Discord.
|
||||
*
|
||||
* **Note:** Calling this function will validate required properties based on their conditions.
|
||||
*/
|
||||
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
|
||||
validateRequiredParameters(this.name, this.type);
|
||||
return {
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
default_permission: this.defaultPermission,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type ContextMenuCommandType = ApplicationCommandType.User | ApplicationCommandType.Message;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import is from '@sindresorhus/is';
|
||||
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 = 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 = 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 = 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);
|
||||
}
|
||||
|
||||
export function validateRequiredParameters(
|
||||
name: string,
|
||||
description: string,
|
||||
options: ToAPIApplicationCommandOptions[],
|
||||
) {
|
||||
// Assert name matches all conditions
|
||||
validateName(name);
|
||||
|
||||
// Assert description conditions
|
||||
validateDescription(description);
|
||||
|
||||
// Assert options conditions
|
||||
validateMaxOptionsLength(options);
|
||||
}
|
||||
|
||||
const booleanPredicate = s.boolean;
|
||||
|
||||
export function validateDefaultPermission(value: unknown): asserts value is boolean {
|
||||
booleanPredicate.parse(value);
|
||||
}
|
||||
|
||||
export function validateRequired(required: unknown): asserts required is boolean {
|
||||
booleanPredicate.parse(required);
|
||||
}
|
||||
|
||||
const choicesLengthPredicate = s.number.le(25);
|
||||
|
||||
export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void {
|
||||
choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding);
|
||||
}
|
||||
|
||||
export function assertReturnOfBuilder<
|
||||
T extends ApplicationCommandOptionBase | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder,
|
||||
>(input: unknown, ExpectedInstanceOf: new () => T): asserts input is T {
|
||||
const instanceName = ExpectedInstanceOf.name;
|
||||
|
||||
if (is.nullOrUndefined(input)) {
|
||||
throw new TypeError(
|
||||
`Expected to receive a ${instanceName} builder, got ${input === null ? 'null' : 'undefined'} instead.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (is.primitive(input)) {
|
||||
throw new TypeError(`Expected to receive a ${instanceName} builder, got a primitive (${typeof input}) instead.`);
|
||||
}
|
||||
|
||||
if (!(input instanceof ExpectedInstanceOf)) {
|
||||
const casted = input as Record<PropertyKey, unknown>;
|
||||
|
||||
const constructorName = is.function_(input) ? input.name : casted.constructor.name;
|
||||
const stringTag = Reflect.get(casted, Symbol.toStringTag) as string | undefined;
|
||||
|
||||
const fullResultName = stringTag ? `${constructorName} [${stringTag}]` : constructorName;
|
||||
|
||||
throw new TypeError(`Expected to receive a ${instanceName} builder, got ${fullResultName} instead.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import type {
|
||||
APIApplicationCommandOption,
|
||||
LocalizationMap,
|
||||
RESTPostAPIApplicationCommandsJSONBody,
|
||||
} from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import {
|
||||
assertReturnOfBuilder,
|
||||
validateDefaultPermission,
|
||||
validateMaxOptionsLength,
|
||||
validateRequiredParameters,
|
||||
} from './Assertions';
|
||||
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
|
||||
import { SharedNameAndDescription } from './mixins/NameAndDescription';
|
||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
|
||||
|
||||
@mix(SharedSlashCommandOptions, SharedNameAndDescription)
|
||||
export class SlashCommandBuilder {
|
||||
/**
|
||||
* The name of this slash command
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public readonly options: ToAPIApplicationCommandOptions[] = [];
|
||||
|
||||
/**
|
||||
* Whether the command is enabled by default when the app is added to a guild
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
public readonly defaultPermission: boolean | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Returns the final data that should be sent to Discord.
|
||||
*
|
||||
* **Note:** Calling this function will validate required properties based on their conditions.
|
||||
*/
|
||||
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
|
||||
validateRequiredParameters(this.name, this.description, this.options);
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the command is enabled by default when the application is added to a guild.
|
||||
*
|
||||
* **Note**: If set to `false`, you will have to later `PUT` the permissions for this command.
|
||||
*
|
||||
* @param value Whether or not to enable this command by default
|
||||
*
|
||||
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
|
||||
*/
|
||||
public setDefaultPermission(value: boolean) {
|
||||
// Assert the value matches the conditions
|
||||
validateDefaultPermission(value);
|
||||
|
||||
Reflect.set(this, 'defaultPermission', value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new subcommand group to this command
|
||||
*
|
||||
* @param input A function that returns a subcommand group builder, or an already built builder
|
||||
*/
|
||||
public addSubcommandGroup(
|
||||
input:
|
||||
| SlashCommandSubcommandGroupBuilder
|
||||
| ((subcommandGroup: SlashCommandSubcommandGroupBuilder) => SlashCommandSubcommandGroupBuilder),
|
||||
): SlashCommandSubcommandsOnlyBuilder {
|
||||
const { options } = this;
|
||||
|
||||
// First, assert options conditions - we cannot have more than 25 options
|
||||
validateMaxOptionsLength(options);
|
||||
|
||||
// Get the final result
|
||||
const result = typeof input === 'function' ? input(new SlashCommandSubcommandGroupBuilder()) : input;
|
||||
|
||||
assertReturnOfBuilder(result, SlashCommandSubcommandGroupBuilder);
|
||||
|
||||
// Push it
|
||||
options.push(result);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new subcommand to this command
|
||||
*
|
||||
* @param input A function that returns a subcommand builder, or an already built builder
|
||||
*/
|
||||
public addSubcommand(
|
||||
input:
|
||||
| SlashCommandSubcommandBuilder
|
||||
| ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder),
|
||||
): SlashCommandSubcommandsOnlyBuilder {
|
||||
const { options } = this;
|
||||
|
||||
// First, assert options conditions - we cannot have more than 25 options
|
||||
validateMaxOptionsLength(options);
|
||||
|
||||
// Get the final result
|
||||
const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input;
|
||||
|
||||
assertReturnOfBuilder(result, SlashCommandSubcommandBuilder);
|
||||
|
||||
// Push it
|
||||
options.push(result);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlashCommandBuilder extends SharedNameAndDescription, SharedSlashCommandOptions {}
|
||||
|
||||
export interface SlashCommandSubcommandsOnlyBuilder
|
||||
extends SharedNameAndDescription,
|
||||
Pick<SlashCommandBuilder, 'toJSON' | 'addSubcommand' | 'addSubcommandGroup'> {}
|
||||
|
||||
export interface SlashCommandOptionsOnlyBuilder
|
||||
extends SharedNameAndDescription,
|
||||
SharedSlashCommandOptions,
|
||||
Pick<SlashCommandBuilder, 'toJSON'> {}
|
||||
|
||||
export interface ToAPIApplicationCommandOptions {
|
||||
toJSON: () => APIApplicationCommandOption;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
APIApplicationCommandSubcommandGroupOption,
|
||||
APIApplicationCommandSubcommandOption,
|
||||
ApplicationCommandOptionType,
|
||||
} 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';
|
||||
|
||||
/**
|
||||
* Represents a folder for subcommands
|
||||
*
|
||||
* For more information, go to https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups
|
||||
*/
|
||||
@mix(SharedNameAndDescription)
|
||||
export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationCommandOptions {
|
||||
/**
|
||||
* The name of this subcommand group
|
||||
*/
|
||||
public readonly name: string = undefined!;
|
||||
|
||||
/**
|
||||
* The description of this subcommand group
|
||||
*/
|
||||
public readonly description: string = undefined!;
|
||||
|
||||
/**
|
||||
* The subcommands part of this subcommand group
|
||||
*/
|
||||
public readonly options: SlashCommandSubcommandBuilder[] = [];
|
||||
|
||||
/**
|
||||
* Adds a new subcommand to this group
|
||||
*
|
||||
* @param input A function that returns a subcommand builder, or an already built builder
|
||||
*/
|
||||
public addSubcommand(
|
||||
input:
|
||||
| SlashCommandSubcommandBuilder
|
||||
| ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder),
|
||||
) {
|
||||
const { options } = this;
|
||||
|
||||
// First, assert options conditions - we cannot have more than 25 options
|
||||
validateMaxOptionsLength(options);
|
||||
|
||||
// Get the final result
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
assertReturnOfBuilder(result, SlashCommandSubcommandBuilder);
|
||||
|
||||
// Push it
|
||||
options.push(result);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIApplicationCommandSubcommandGroupOption {
|
||||
validateRequiredParameters(this.name, this.description, this.options);
|
||||
|
||||
return {
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlashCommandSubcommandGroupBuilder extends SharedNameAndDescription {}
|
||||
|
||||
/**
|
||||
* Represents a subcommand
|
||||
*
|
||||
* For more information, go to https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups
|
||||
*/
|
||||
@mix(SharedNameAndDescription, SharedSlashCommandOptions)
|
||||
export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOptions {
|
||||
/**
|
||||
* The name of this subcommand
|
||||
*/
|
||||
public readonly name: string = undefined!;
|
||||
|
||||
/**
|
||||
* The description of this subcommand
|
||||
*/
|
||||
public readonly description: string = undefined!;
|
||||
|
||||
/**
|
||||
* The options of this subcommand
|
||||
*/
|
||||
public readonly options: ApplicationCommandOptionBase[] = [];
|
||||
|
||||
public toJSON(): APIApplicationCommandSubcommandOption {
|
||||
validateRequiredParameters(this.name, this.description, this.options);
|
||||
|
||||
return {
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlashCommandSubcommandBuilder extends SharedNameAndDescription, SharedSlashCommandOptions<false> {}
|
||||
@@ -0,0 +1,16 @@
|
||||
export abstract class ApplicationCommandNumericOptionMinMaxValueMixin {
|
||||
public readonly max_value?: number;
|
||||
public readonly min_value?: number;
|
||||
|
||||
/**
|
||||
* Sets the maximum number value of this option
|
||||
* @param max The maximum value this option can be
|
||||
*/
|
||||
public abstract setMaxValue(max: number): this;
|
||||
|
||||
/**
|
||||
* Sets the minimum number value of this option
|
||||
* @param min The minimum value this option can be
|
||||
*/
|
||||
public abstract setMinValue(min: number): this;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
|
||||
public readonly required: boolean = false;
|
||||
|
||||
/**
|
||||
* Marks the option as required
|
||||
*
|
||||
* @param required If this option should be required
|
||||
*/
|
||||
public setRequired(required: boolean) {
|
||||
// Assert that you actually passed a boolean
|
||||
validateRequired(required);
|
||||
|
||||
Reflect.set(this, 'required', required);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract toJSON(): APIApplicationCommandBasicOption;
|
||||
|
||||
protected runRequiredValidations() {
|
||||
validateRequiredParameters(this.name, this.description, []);
|
||||
|
||||
// Assert that you actually passed a boolean
|
||||
validateRequired(this.required);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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 = [
|
||||
ChannelType.GuildText,
|
||||
ChannelType.GuildVoice,
|
||||
ChannelType.GuildCategory,
|
||||
ChannelType.GuildNews,
|
||||
ChannelType.GuildNewsThread,
|
||||
ChannelType.GuildPublicThread,
|
||||
ChannelType.GuildPrivateThread,
|
||||
ChannelType.GuildStageVoice,
|
||||
] as const;
|
||||
|
||||
export type ApplicationCommandOptionAllowedChannelTypes = typeof allowedChannelTypes[number];
|
||||
|
||||
const channelTypesPredicate = s.array(s.union(...allowedChannelTypes.map((type) => s.literal(type))));
|
||||
|
||||
export class ApplicationCommandOptionChannelTypesMixin {
|
||||
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
|
||||
|
||||
/**
|
||||
* Adds channel types to this option
|
||||
*
|
||||
* @param channelTypes The channel types to add
|
||||
*/
|
||||
public addChannelTypes(...channelTypes: ApplicationCommandOptionAllowedChannelTypes[]) {
|
||||
if (this.channel_types === undefined) {
|
||||
Reflect.set(this, 'channel_types', []);
|
||||
}
|
||||
|
||||
this.channel_types!.push(...channelTypesPredicate.parse(channelTypes));
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { validateChoicesLength } from '../Assertions';
|
||||
|
||||
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>[];
|
||||
public readonly autocomplete?: boolean;
|
||||
|
||||
// Since this is present and this is a mixin, this is needed
|
||||
public readonly type!: ApplicationCommandOptionType;
|
||||
|
||||
/**
|
||||
* Adds multiple choices for this option
|
||||
*
|
||||
* @param choices The choices to add
|
||||
*/
|
||||
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);
|
||||
|
||||
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 APIApplicationCommandOptionChoice<T>[]>(...choices: Input): this {
|
||||
if (choices.length > 0 && this.autocomplete) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
choicesPredicate.parse(choices);
|
||||
|
||||
Reflect.set(this, 'choices', []);
|
||||
this.addChoices(...choices);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the option as autocompletable
|
||||
* @param autocomplete If this option should be autocompletable
|
||||
*/
|
||||
public setAutocomplete(autocomplete: boolean): this {
|
||||
// Assert that you actually passed a boolean
|
||||
booleanPredicate.parse(autocomplete);
|
||||
|
||||
if (autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
Reflect.set(this, 'autocomplete', autocomplete);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
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
|
||||
*
|
||||
* @param name The name
|
||||
*/
|
||||
public setName(name: string): this {
|
||||
// Assert the name matches the conditions
|
||||
validateName(name);
|
||||
|
||||
Reflect.set(this, 'name', name);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description
|
||||
*
|
||||
* @param description The description
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
// Assert the description matches the conditions
|
||||
validateDescription(description);
|
||||
|
||||
Reflect.set(this, 'description', description);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
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';
|
||||
import { SlashCommandMentionableOption } from '../options/mentionable';
|
||||
import { SlashCommandNumberOption } from '../options/number';
|
||||
import { SlashCommandRoleOption } from '../options/role';
|
||||
import { SlashCommandStringOption } from '../options/string';
|
||||
import { SlashCommandUserOption } from '../options/user';
|
||||
|
||||
export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
public readonly options!: ToAPIApplicationCommandOptions[];
|
||||
|
||||
/**
|
||||
* Adds a boolean option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addBooleanOption(
|
||||
input: SlashCommandBooleanOption | ((builder: SlashCommandBooleanOption) => SlashCommandBooleanOption),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandBooleanOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a user option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addUserOption(input: SlashCommandUserOption | ((builder: SlashCommandUserOption) => SlashCommandUserOption)) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandUserOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a channel option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addChannelOption(
|
||||
input: SlashCommandChannelOption | ((builder: SlashCommandChannelOption) => SlashCommandChannelOption),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandChannelOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addRoleOption(input: SlashCommandRoleOption | ((builder: SlashCommandRoleOption) => SlashCommandRoleOption)) {
|
||||
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
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addMentionableOption(
|
||||
input: SlashCommandMentionableOption | ((builder: SlashCommandMentionableOption) => SlashCommandMentionableOption),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandMentionableOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a string option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addStringOption(
|
||||
input:
|
||||
| SlashCommandStringOption
|
||||
| Omit<SlashCommandStringOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandStringOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandStringOption,
|
||||
) =>
|
||||
| SlashCommandStringOption
|
||||
| Omit<SlashCommandStringOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandStringOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandStringOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an integer option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addIntegerOption(
|
||||
input:
|
||||
| SlashCommandIntegerOption
|
||||
| Omit<SlashCommandIntegerOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandIntegerOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandIntegerOption,
|
||||
) =>
|
||||
| SlashCommandIntegerOption
|
||||
| Omit<SlashCommandIntegerOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandIntegerOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandIntegerOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a number option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addNumberOption(
|
||||
input:
|
||||
| SlashCommandNumberOption
|
||||
| Omit<SlashCommandNumberOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandNumberOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandNumberOption,
|
||||
) =>
|
||||
| SlashCommandNumberOption
|
||||
| Omit<SlashCommandNumberOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandNumberOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandNumberOption);
|
||||
}
|
||||
|
||||
private _sharedAddOptionMethod<T extends ApplicationCommandOptionBase>(
|
||||
input:
|
||||
| T
|
||||
| Omit<T, 'setAutocomplete'>
|
||||
| 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;
|
||||
|
||||
// First, assert options conditions - we cannot have more than 25 options
|
||||
validateMaxOptionsLength(options);
|
||||
|
||||
// Get the final result
|
||||
const result = typeof input === 'function' ? input(new Instance()) : input;
|
||||
|
||||
assertReturnOfBuilder(result, Instance);
|
||||
|
||||
// Push it
|
||||
options.push(result);
|
||||
|
||||
return 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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { APIApplicationCommandBooleanOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandBooleanOption extends ApplicationCommandOptionBase {
|
||||
public readonly type = ApplicationCommandOptionType.Boolean as const;
|
||||
|
||||
public toJSON(): APIApplicationCommandBooleanOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { APIApplicationCommandChannelOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin';
|
||||
|
||||
@mix(ApplicationCommandOptionChannelTypesMixin)
|
||||
export class SlashCommandChannelOption extends ApplicationCommandOptionBase {
|
||||
public override readonly type = ApplicationCommandOptionType.Channel as const;
|
||||
|
||||
public toJSON(): APIApplicationCommandChannelOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlashCommandChannelOption extends ApplicationCommandOptionChannelTypesMixin {}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
const numberValidator = s.number.int;
|
||||
|
||||
@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
|
||||
export class SlashCommandIntegerOption
|
||||
extends ApplicationCommandOptionBase
|
||||
implements ApplicationCommandNumericOptionMinMaxValueMixin
|
||||
{
|
||||
public readonly type = ApplicationCommandOptionType.Integer as const;
|
||||
|
||||
public setMaxValue(max: number): this {
|
||||
numberValidator.parse(max);
|
||||
|
||||
Reflect.set(this, 'max_value', max);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public setMinValue(min: number): this {
|
||||
numberValidator.parse(min);
|
||||
|
||||
Reflect.set(this, 'min_value', min);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIApplicationCommandIntegerOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlashCommandIntegerOption
|
||||
extends ApplicationCommandNumericOptionMinMaxValueMixin,
|
||||
ApplicationCommandOptionWithChoicesAndAutocompleteMixin<number> {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { APIApplicationCommandMentionableOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandMentionableOption extends ApplicationCommandOptionBase {
|
||||
public readonly type = ApplicationCommandOptionType.Mentionable as const;
|
||||
|
||||
public toJSON(): APIApplicationCommandMentionableOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
const numberValidator = s.number;
|
||||
|
||||
@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
|
||||
export class SlashCommandNumberOption
|
||||
extends ApplicationCommandOptionBase
|
||||
implements ApplicationCommandNumericOptionMinMaxValueMixin
|
||||
{
|
||||
public readonly type = ApplicationCommandOptionType.Number as const;
|
||||
|
||||
public setMaxValue(max: number): this {
|
||||
numberValidator.parse(max);
|
||||
|
||||
Reflect.set(this, 'max_value', max);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public setMinValue(min: number): this {
|
||||
numberValidator.parse(min);
|
||||
|
||||
Reflect.set(this, 'min_value', min);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIApplicationCommandNumberOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlashCommandNumberOption
|
||||
extends ApplicationCommandNumericOptionMinMaxValueMixin,
|
||||
ApplicationCommandOptionWithChoicesAndAutocompleteMixin<number> {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { APIApplicationCommandRoleOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandRoleOption extends ApplicationCommandOptionBase {
|
||||
public override readonly type = ApplicationCommandOptionType.Role as const;
|
||||
|
||||
public toJSON(): APIApplicationCommandRoleOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { APIApplicationCommandStringOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
@mix(ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
|
||||
export class SlashCommandStringOption extends ApplicationCommandOptionBase {
|
||||
public readonly type = ApplicationCommandOptionType.String as const;
|
||||
|
||||
public toJSON(): APIApplicationCommandStringOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
|
||||
export interface SlashCommandStringOption extends ApplicationCommandOptionWithChoicesAndAutocompleteMixin<string> {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { APIApplicationCommandUserOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandUserOption extends ApplicationCommandOptionBase {
|
||||
public readonly type = ApplicationCommandOptionType.User as const;
|
||||
|
||||
public toJSON(): APIApplicationCommandUserOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
46
packages/builders/src/messages/embed/Assertions.ts
Normal file
46
packages/builders/src/messages/embed/Assertions.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||
|
||||
export const fieldNamePredicate = s.string.lengthGe(1).lengthLe(256);
|
||||
|
||||
export const fieldValuePredicate = s.string.lengthGe(1).lengthLe(1024);
|
||||
|
||||
export const fieldInlinePredicate = s.boolean.optional;
|
||||
|
||||
export const embedFieldPredicate = s.object({
|
||||
name: fieldNamePredicate,
|
||||
value: fieldValuePredicate,
|
||||
inline: fieldInlinePredicate,
|
||||
});
|
||||
|
||||
export const embedFieldsArrayPredicate = embedFieldPredicate.array;
|
||||
|
||||
export const fieldLengthPredicate = s.number.le(25);
|
||||
|
||||
export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void {
|
||||
fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding);
|
||||
}
|
||||
|
||||
export const authorNamePredicate = fieldNamePredicate.nullable;
|
||||
|
||||
export const imageURLPredicate = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:', 'attachment:'],
|
||||
}).nullish;
|
||||
|
||||
export const urlPredicate = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:'],
|
||||
}).nullish;
|
||||
|
||||
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 descriptionPredicate = s.string.lengthGe(1).lengthLe(4096).nullable;
|
||||
|
||||
export const footerTextPredicate = s.string.lengthGe(1).lengthLe(2048).nullable;
|
||||
|
||||
export const timestampPredicate = s.union(s.number, s.date).nullable;
|
||||
|
||||
export const titlePredicate = fieldNamePredicate.nullable;
|
||||
95
packages/builders/src/messages/embed/Embed.ts
Normal file
95
packages/builders/src/messages/embed/Embed.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||
import {
|
||||
authorNamePredicate,
|
||||
colorPredicate,
|
||||
descriptionPredicate,
|
||||
embedFieldsArrayPredicate,
|
||||
footerTextPredicate,
|
||||
imageURLPredicate,
|
||||
timestampPredicate,
|
||||
titlePredicate,
|
||||
urlPredicate,
|
||||
validateFieldLength,
|
||||
} from './Assertions';
|
||||
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbedBuilder } from './UnsafeEmbed';
|
||||
|
||||
/**
|
||||
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
|
||||
*/
|
||||
export class EmbedBuilder extends UnsafeEmbedBuilder {
|
||||
public override addFields(...fields: APIEmbedField[]): this {
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(fields.length, this.data.fields);
|
||||
|
||||
// Data assertions
|
||||
return super.addFields(...embedFieldsArrayPredicate.parse(fields));
|
||||
}
|
||||
|
||||
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) {
|
||||
return super.setAuthor(null);
|
||||
}
|
||||
|
||||
// Data assertions
|
||||
authorNamePredicate.parse(options.name);
|
||||
urlPredicate.parse(options.iconURL);
|
||||
urlPredicate.parse(options.url);
|
||||
|
||||
return super.setAuthor(options);
|
||||
}
|
||||
|
||||
public override setColor(color: number | RGBTuple | null): this {
|
||||
// Data assertions
|
||||
return super.setColor(colorPredicate.parse(color));
|
||||
}
|
||||
|
||||
public override setDescription(description: string | null): this {
|
||||
// Data assertions
|
||||
return super.setDescription(descriptionPredicate.parse(description));
|
||||
}
|
||||
|
||||
public override setFooter(options: EmbedFooterOptions | null): this {
|
||||
if (options === null) {
|
||||
return super.setFooter(null);
|
||||
}
|
||||
|
||||
// Data assertions
|
||||
footerTextPredicate.parse(options.text);
|
||||
urlPredicate.parse(options.iconURL);
|
||||
|
||||
return super.setFooter(options);
|
||||
}
|
||||
|
||||
public override setImage(url: string | null): this {
|
||||
// Data assertions
|
||||
return super.setImage(imageURLPredicate.parse(url)!);
|
||||
}
|
||||
|
||||
public override setThumbnail(url: string | null): this {
|
||||
// Data assertions
|
||||
return super.setThumbnail(imageURLPredicate.parse(url)!);
|
||||
}
|
||||
|
||||
public override setTimestamp(timestamp: number | Date | null = Date.now()): this {
|
||||
// Data assertions
|
||||
return super.setTimestamp(timestampPredicate.parse(timestamp));
|
||||
}
|
||||
|
||||
public override setTitle(title: string | null): this {
|
||||
// Data assertions
|
||||
return super.setTitle(titlePredicate.parse(title));
|
||||
}
|
||||
|
||||
public override setURL(url: string | null): this {
|
||||
// Data assertions
|
||||
return super.setURL(urlPredicate.parse(url)!);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user