Compare commits

...

133 Commits

Author SHA1 Message Date
iCrawl
a319607af6 chore(release): builders, collection, rest and voice 2022-01-24 17:14:02 +01:00
iCrawl
487a5a2669 chore: update changelogs 2022-01-24 16:42:33 +01:00
Suneet Tipirneni
c10afeadc7 types: make required a boolean (#7307) 2022-01-20 14:30:03 +01:00
ckohen
d0fa5aaa26 fix(rest): don't add empty query (#7308) 2022-01-20 12:14:31 +01:00
ckohen
d8ea572fb8 fix(rest): use http agent when protocol is not https (#7309) 2022-01-20 12:14:12 +01:00
Jiralite
1c6c9449ad types: Fix channel create overloads (#7294) 2022-01-19 22:06:07 +01:00
Jan
b4ed8fd3ed refactor: remove transformPermissions (#7303) 2022-01-19 22:05:54 +01:00
Suneet Tipirneni
857bba4480 fix(guildchannelmanager): remove reverse enum lookup (#7304) 2022-01-19 22:05:38 +01:00
Shubham Parihar
bd33ebb507 docs(StageInstance): deprecate discoverableDisabled (#7179) 2022-01-19 20:37:05 +01:00
Jiralite
cf25de9373 feat(GuildPreview): Add stickers (#7152) 2022-01-19 20:36:05 +01:00
SpaceEEC
5b4dbc3359 ci(publish-dev): only build dependencies when releasing (#7302) 2022-01-19 14:54:25 +01:00
Suneet Tipirneni
213acd7997 feat: Enum resolvers & internal enum string removal (#7290)
Co-authored-by: Almeida <almeidx@pm.me>
2022-01-19 14:11:10 +01:00
Jiralite
164589c551 refactor: Remove VoiceChannel#editable (#7291) 2022-01-19 07:14:33 +01:00
Jiralite
347ff80bbc refactor: Remove boolean option for Webhook#fetchMessage (#7293) 2022-01-19 07:13:38 +01:00
Khafra
307389a335 BaseClient: pass captureRejections option (#7288) 2022-01-18 11:21:05 +01:00
awned
9181a31e0b feat(guildemojimanager): add delete and edit methods (#7286)
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
2022-01-18 08:35:36 +01:00
Almeida
86d9d06743 fix(builders): dont export Button component stuff twice (#7289) 2022-01-17 22:13:51 +01:00
Suneet Tipirneni
da05a8856b feat(interaction): add isRepliable type guard (#7259)
Co-authored-by: Almeida <almeidx@pm.me>
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
2022-01-17 20:17:34 +01:00
Parbez
6112767128 refactor(subcommandgroup): required default to false (#7217) 2022-01-17 16:46:31 +01:00
Rodry
26a9dc3206 types: fix regressions and inconsistencies (#7260) 2022-01-17 16:45:23 +01:00
Almeida
18b0ed4cbe refactor(invite): make channel and inviter getters (#7278) 2022-01-17 13:51:34 +01:00
Parbez
c90e47f904 refactor: remove required from getMember (#7188) 2022-01-17 13:51:13 +01:00
Suneet Tipirneni
3bb4829800 feat: rest hash and handler sweeping (#7255)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2022-01-17 13:50:41 +01:00
Almeida
e65da44d9c docs(shardingmanager): fix type of execArgv option (#7284) 2022-01-17 13:15:46 +01:00
Suneet Tipirneni
0951309b32 chore(cdn): default to animated images (#7265)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Almeida <almeidx@pm.me>
2022-01-17 13:14:03 +01:00
Suneet Tipirneni
101d7c5ffa refactor: remove djs components and use /builders components instead (#7252)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2022-01-17 13:13:48 +01:00
Jan
599f96740e ci: fix docs workflow (#7287) 2022-01-17 11:40:53 +01:00
Jan
9054f2f7ad feat: rest docs (#7281) 2022-01-17 10:56:48 +01:00
Almeida
ecf2430f6e ci: dont run auto-deprecated and publish-dev on forks (#7279) 2022-01-16 21:24:31 +01:00
Jiralite
7c07976018 refactor: Default *URL methods to animated hash (#7149)
Co-authored-by: Jan <66554238+vaporox@users.noreply.github.com>
2022-01-14 18:03:09 +01:00
Antonio Román
8ddd44ed85 fix: import clear{Timeout,Interval} from node:timers (#7269) 2022-01-14 13:34:59 +01:00
Jan
0b23b7f039 refactor: use setPosition inside edit (#7263) 2022-01-14 00:21:32 +01:00
Koyamie
06f5210f58 fix(ApplicationCommand): use new ApplicationCommandOptionType enums (#7257) 2022-01-14 00:20:45 +01:00
Almeida
f284a4641f fix: use enums from discord-api-types (#7258) 2022-01-14 00:19:35 +01:00
Rodry
37ec0bda6d docs(interaction): add locale list link (#7261) 2022-01-13 18:19:30 +01:00
Jan
0a5d5f38c0 refactor: don't disable import order lint (#7262) 2022-01-13 17:59:49 +01:00
Vitor
0dd56afe1c docs: fix a typo and use milliseconds instead of ms (#7251) 2022-01-13 17:59:28 +01:00
Jiralite
7b53978a73 chore: Add prefer-object-has-own rule (#7264) 2022-01-13 17:59:10 +01:00
Suneet Tipirneni
2bb40fd767 feat: add components to /builders (#7195)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2022-01-12 20:50:08 +01:00
Rodry
37a22e04c2 feat(Channel): improve typeguards (#6957)
Co-authored-by: Papageorgiadis Savvas <50584606+papsavas@users.noreply.github.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Almeida <almeidx@pm.me>
2022-01-12 20:30:06 +01:00
Koyamie
f753882592 fix(exports): export ApplicationCommandType properly (#7256) 2022-01-12 20:27:41 +01:00
Suneet Tipirneni
aa6d1c74de refactor: remove discord.js enums and use discord-api-types enums instead (#7077) 2022-01-12 02:00:41 +01:00
Jan
1479e40bce refactor(application): remove fetchAssets (#7250) 2022-01-11 22:03:17 +01:00
Voxelli
1745973024 refactor: PresenceUpdate and demuxProbe (#7248) 2022-01-11 22:02:54 +01:00
iCrawl
62c74b8333 test(voice): fix tests 2022-01-11 21:53:08 +01:00
Suneet Tipirneni
db25f529b2 types: add ReadonlyCollection (#7245)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2022-01-11 12:30:16 +01:00
Noel
16938da355 chore: improve linting (#7244) 2022-01-11 12:30:08 +01:00
Voxelli
ff3a8b8323 fix(Shard): EventEmitter listener warning (#7240)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2022-01-11 12:30:00 +01:00
ckohen
86ab526d49 refactor(files): file data can be much more than buffer (#7238) 2022-01-11 12:29:48 +01:00
Suneet Tipirneni
d8efba24e0 refactor: make intersect perform a true intersection (#7211)
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
2022-01-11 12:29:41 +01:00
Suneet Tipirneni
9052e321d1 feat: Add Locales to Interactions (#7131)
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
2022-01-11 08:55:49 +01:00
Parbez
22fdbb0a29 chore: commit scope name in lowercase (#7246) 2022-01-11 08:53:26 +01:00
Suneet Tipirneni
ed0cfd91ed fix: ref delay for rate limited requests (#7239) 2022-01-10 10:21:44 +01:00
Noel
b2dfb2ec70 revert "chore: optimize Turbo pipelines" (#7237) 2022-01-09 23:22:25 +01:00
iCrawl
56b69a749a ci: only run assignment when a pr opens 2022-01-09 22:44:06 +01:00
Jonah Snider
fae39efc7f chore: optimize Turbo pipelines (#7236) 2022-01-09 22:32:02 +01:00
iCrawl
7ddd337c4a ci: more automation for prs 2022-01-09 22:30:52 +01:00
Rodry
a8509c91ca fix(timestamps): account for timestamps of 0 when creating Dates (#7226) 2022-01-09 21:48:45 +01:00
Jonah Snider
a0a4a2258a chore: format all supported Prettier files (#7233) 2022-01-09 21:42:04 +01:00
Almeida
4770c814b2 chore: update readme links (#7229) 2022-01-09 19:35:18 +01:00
iCrawl
4917eb3539 ci: revert auto-labeling 2022-01-09 19:28:32 +01:00
iCrawl
c0824ae419 ci: try different syntax in yaml file 2022-01-09 19:23:33 +01:00
iCrawl
174782e132 ci: update version of labeler 2022-01-09 19:15:21 +01:00
iCrawl
538d6683bc ci: escape backslashes in yml 2022-01-09 18:57:19 +01:00
iCrawl
0236308581 ci: better labeler for issues and prs 2022-01-09 18:33:27 +01:00
Rodry
822dc678da refactor(embeds): don't create new embed instances when unnecessary (#7227) 2022-01-09 17:35:32 +01:00
Suneet Tipirneni
f1d4bd68ec chore: allow typescript to typecheck .d.ts files (#7222) 2022-01-09 15:14:29 +01:00
Hyro
e38d79988a chore: license for REST module (#7223) 2022-01-09 13:21:43 +01:00
Vitor
bd7a6f2652 docs(SlashCommandSubcommands): updating old links from Discord developer portal (#7224) 2022-01-09 13:20:43 +01:00
Raiden-Ei
37cad54dbd fix(MessageEmbed): createdAt field can be zero (#7218) 2022-01-09 13:20:07 +01:00
Rodry
4fd127e79e refactor(GuildMember): throw better errors on #kickable and #bannable (#7137) 2022-01-09 13:18:50 +01:00
J-Human
ab3ff5a262 feat(Permissions): remove deprecated thread-related permissions (#6755) 2022-01-09 11:49:27 +01:00
J-Human
b92a7d7233 fix(BaseClient): do not append default options if provided is not an object (#6453) 2022-01-09 11:49:03 +01:00
J-Human
caaef53dd9 feat(VoiceRegion): remove the unsent vip field (#6759) 2022-01-09 11:48:15 +01:00
muchnameless
68b9564f18 types(interaction): remove renamed typeguards (#7220) 2022-01-09 11:44:24 +01:00
muchnameless
962f4bf882 fix: snowflakeutil import (#7219) 2022-01-09 11:43:35 +01:00
iCrawl
e07c374337 ci: fix publishing dev versions 2022-01-08 23:30:28 +01:00
iCrawl
7832dbc07a chore: yarn lock 2022-01-08 23:17:01 +01:00
iCrawl
4a2dbd6238 test: fix voice secretbox tests 2022-01-08 23:16:38 +01:00
iCrawl
b593bd32a9 test: fix voice tests 2022-01-08 22:57:29 +01:00
iCrawl
1d532d9468 ci: don't cache deps for publish 2022-01-08 22:53:10 +01:00
iCrawl
3f36746561 types: assertType -> expectType 2022-01-08 20:52:11 +01:00
Antonio Román
e082dfb158 refactor(SnowflakeUtil): switch to @sapphire/snowflake (#7079) 2022-01-08 19:11:33 +01:00
Matt (IPv4) Cowley
ae876d9624 refactor(InteractionCreate): remove interaction event (#6326) 2022-01-08 14:43:30 +01:00
Matt (IPv4) Cowley
171e917fb9 refactor(MessageCreate): remove message event (#6324) 2022-01-08 14:43:12 +01:00
Koyamie
63034b44c9 fix(Role): remove unused process (#7215) 2022-01-08 14:10:56 +01:00
Rodry
13eb78256d refactor(integration): turn undefined into null and consistency (#7209) 2022-01-08 12:44:28 +01:00
IlluminatiFish
1c615d1bb2 fix(WebhookClient): Updated webhook url regex (#6804) 2022-01-08 12:42:50 +01:00
Suneet Tipirneni
1db6c2d37e chore(ThreadMemberManager): make #fetch only take one object parameter (#6997) 2022-01-08 12:42:30 +01:00
Antonio Román
cee7fd181c refactor: remove deleted field (#7092) 2022-01-08 12:41:35 +01:00
Jan
55e21f5366 refactor(Dates): save timestamps everywhere and use Date.parse (#7108) 2022-01-08 12:41:20 +01:00
Commandtechno
d06d70ccf2 feat(richpresenceassets): add YouTube and custom image support (#7184) 2022-01-08 12:40:35 +01:00
Koyamie
95f8375d42 fix(Sweepers): add sweepStickers function (#7213) 2022-01-08 12:39:48 +01:00
Suneet Tipirneni
137ea249df docs: fix command interaction docs (#7212) 2022-01-08 11:21:06 +01:00
Koyamie
49dada35f9 fix(InteractionCreate): use ChatInputCommandInteraction instead (#7210) 2022-01-08 02:31:23 +01:00
iCrawl
53971f0de9 ci: use yarn to create new version 2022-01-08 01:47:00 +01:00
iCrawl
9760fd1b77 ci: fix publishing dev version 2022-01-08 01:44:17 +01:00
Raiden-Ei
fbef454894 fix(Structues): rename old module's name (#7207) 2022-01-08 01:41:22 +01:00
Jiralite
fbbac27978 refactor(RoleManager): Remove comparePositions() (#7201) 2022-01-08 01:35:32 +01:00
Rodry
dbf5ce3e6c ci(documentation): add repository_owner check (#7205) 2022-01-08 01:35:20 +01:00
Antonio Román
ef6f035b5d chore: add myself to contributors (#7206) 2022-01-08 01:35:07 +01:00
iCrawl
9d17904eff chore: deps 2022-01-08 01:33:47 +01:00
Raiden-Ei
033151cf92 fix(WebSocket): Remove application command handler (#7202) 2022-01-08 01:29:56 +01:00
Rodry
3269ec1984 chore(VSCode): add extension recommendations (#6666) 2022-01-08 00:51:19 +01:00
ckohen
c969cbf652 refactor(rest): rename attachment to file (#7199) 2022-01-08 00:49:34 +01:00
Suneet Tipirneni
3872acfeb8 fix(User): bannerURL() should not throw when not present (#6789) 2022-01-08 00:48:46 +01:00
ckohen
12ffa069aa chore(Caching): clean up deprecated cache sweeping (#7118)
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
2022-01-08 00:48:17 +01:00
Suneet Tipirneni
1f9b9225f2 types(Collection): union types on intersect and difference (#7196) 2022-01-08 00:47:55 +01:00
Suneet Tipirneni
b7856e7809 refactor: Better Command Terminology (#7197) 2022-01-08 00:47:41 +01:00
Jan
5022b14da0 refactor(Actions): remove deleted maps (#7076) 2022-01-08 00:34:47 +01:00
iCrawl
fbe40031cf chore: manually update changelog 2022-01-08 00:03:51 +01:00
Jiralite
a0d5f13dd9 fix(VoiceState): Ensure suppress & streaming have proper fallback values (#6377) 2022-01-07 23:40:58 +01:00
Rodry
6085b4f727 refactor(Client): remove applicationCommand events (#6492) 2022-01-07 23:40:50 +01:00
Jiralite
0c24cc8ec0 refactor: Remove Util.removeMentions() (#6530) 2022-01-07 23:40:36 +01:00
Rodry
75616a305f refactor(Constants): change APPLICATION_COMMAND -> CHAT_INPUT_COMMAND (#7018) 2022-01-07 23:40:01 +01:00
Antonio Román
2b480cb14e feat(Collector): yield all collected values (#7073) 2022-01-07 23:39:49 +01:00
LePtitMetalleux
b454740ae8 docs(TextBasedChannel): fixed syntax error in examples (#7163) 2022-01-07 23:38:58 +01:00
Suneet Tipirneni
c978dbb623 types: fix cache types resolving to never (#7164) 2022-01-07 23:37:17 +01:00
n1ck_pro
d4e6e03708 docs(TextBasedChannel): fix #createMessageComponentCollector description (#7168) 2022-01-07 23:18:46 +01:00
GoldenAngel
db669b8971 docs: fixes the examples for kick and ban (#7170) 2022-01-07 23:18:25 +01:00
awned
f79ea67d3a docs(RoleManager): fix incorrect example (#7174) 2022-01-07 23:17:57 +01:00
ckohen
ae2f013653 fix(sweepers): provide default for object param (#7182) 2022-01-07 23:16:00 +01:00
Souji
37587e2bcb chore: issue template changes for monorepo (#7198) 2022-01-07 23:13:04 +01:00
Almeida
43e5e3c339 refactor(Guild): remove deprecated setXPositions methods (#6897) 2022-01-07 23:12:31 +01:00
Almeida
b246fc4101 refactor(UserFlags): update flag names (#6938) 2022-01-07 23:12:05 +01:00
Antonio Román
4293b74253 chore: bump Node.js to 16.9.0 (#7035) 2022-01-07 21:03:00 +01:00
iCrawl
c8bfb24589 ci: use pull_request_target for auto labeling 2022-01-07 20:54:51 +01:00
iCrawl
b08c41d425 chore: remove labeling chore for now 2022-01-07 20:49:47 +01:00
iCrawl
4a80710e7a chore: add auto-labeler 2022-01-07 20:42:43 +01:00
iCrawl
1060dcdd4e chore: add more labels 2022-01-07 20:17:48 +01:00
iCrawl
967e9ace21 ci: max simultaneous of 1 for documentation runners 2022-01-07 17:27:26 +01:00
iCrawl
3c9a9c2c18 ci: force push to avoid non-fast-forward errors 2022-01-07 17:23:13 +01:00
Noel
16390efe6e chore: monorepo setup (#7175) 2022-01-07 17:18:25 +01:00
540 changed files with 29822 additions and 28094 deletions

View File

@@ -1,10 +1,10 @@
{
"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"]
]
}
}

View File

@@ -15,13 +15,13 @@ Messages must be matched by the following regex:
Appears under "Features" header, `GuildMember` subheader:
```
feat(GuildMember): add 'tag' method
feat(guildmember): add 'tag' method
```
Appears under "Bug Fixes" header, `Guild` subheader, with a link to issue #28:
```
fix(Guild): handle events correctly
fix(guild): handle events correctly
close #28
```
@@ -37,7 +37,7 @@ BREAKING CHANGE: The 'bar' option has been removed.
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
```
revert: feat(Managers): add Managers
revert: feat(managers): add Managers
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
```

View File

@@ -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,6 +88,7 @@ body:
Tip: you can select multiple items
options:
- Not applicable (subpackage bug)
- No Partials
- USER
- CHANNEL
@@ -104,10 +104,12 @@ 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:
- Not applicable (subpackage bug)
- No Intents
- GUILDS
- GUILD_MEMBERS
- GUILD_BANS
@@ -131,8 +133,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`)

View File

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

View File

@@ -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
View File

@@ -0,0 +1,7 @@
addReviewers: true
reviewers:
- iCrawl
- SpaceEEC
- kyranet
- vladfrangu
numberOfReviewers: 0

27
.github/labeler.yml vendored Normal file
View 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
View File

@@ -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
View File

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

View File

@@ -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 "discord.js"'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

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

99
.github/workflows/documentation.yml vendored Normal file
View File

@@ -0,0 +1,99 @@
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: Move docs to correct directory
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

View File

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

View File

@@ -7,38 +7,53 @@ jobs:
npm:
name: npm
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 discord.js@dev version | grep -e "$(jq --raw-output '.version' packages/discord.js/package.json).*.$(git rev-parse --short HEAD | cut -b1-3)") ]]; \
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 discord.js@"~$(jq --raw-output '.version' packages/discord.js/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 discord.js version --no-git-tag-version --new-version $(jq --raw-output '.version' packages/discord.js/package.json).$(date +%s).$(git rev-parse --short HEAD)
yarn workspace discord.js publish --tag dev || true
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

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

12
.gitignore vendored
View File

@@ -13,18 +13,16 @@ 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

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1
yarn commitlint --edit $1

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install lint-staged
yarn format

View File

@@ -1,4 +0,0 @@
{
"*.{mjs,js}": "eslint --fix --ext mjs,js,ts",
"*.{ts,json,yml,yaml}": "prettier --write"
}

5
.npmrc
View File

@@ -1,5 +0,0 @@
audit=false
fund=false
legacy-peer-deps=true
tag-version-prefix=""
message="chore(Release): %s"

View File

@@ -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"
}

View File

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

View File

@@ -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 commands = [{
name: 'ping',
description: 'Replies with Pong!'
}];
const commands = [
{
name: 'ping',
description: 'Replies with Pong!',
},
];
const rest = new REST({ version: '9' }).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] });
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +1,102 @@
{
"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 --latest"
},
"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.1.0",
"@commitlint/config-angular": "^16.0.0",
"@favware/npm-deprecate": "^1.0.4",
"conventional-changelog-cli": "^2.2.2",
"husky": "^7.0.4",
"prettier": "^2.5.1",
"turbo": "1.0.28"
},
"engines": {
"node": ">=16.9.0"
},
"workspaces": [
"packages/*"
],
"turbo": {
"baseBranch": "origin/main",
"pipeline": {
"build": {
"dependsOn": [
"^build"
],
"outputs": [
"dist/**",
"docs/docs.json"
]
},
"test": {
"dependsOn": [
"^build"
],
"outputs": [
"coverage/**"
]
},
"lint": {
"dependsOn": [
"^build"
],
"outputs": []
},
"format": {
"outputs": []
},
"docs": {
"dependsOn": [
"^build"
],
"outputs": [
"docs/docs.json"
]
},
"changelog": {
"dependsOn": [
"^build"
],
"outputs": [
"CHANGELOG.md"
]
}
}
}
}

View File

@@ -0,0 +1,12 @@
{
"root": true,
"extends": "marine/prettier/node",
"parserOptions": {
"project": "./tsconfig.eslint.json",
"extraFileExtensions": [".mjs"]
},
"ignorePatterns": ["**/dist/*"],
"env": {
"jest": true
}
}

26
packages/builders/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# 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
# Miscellaneous
.tmp/
coverage/

View File

@@ -0,0 +1,8 @@
# Autogenerated
CHANGELOG.md
.turbo
dist/
docs/**/*
!docs/index.yml
!docs/README.md
coverage/

View File

@@ -0,0 +1,8 @@
{
"printWidth": 120,
"useTabs": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf"
}

View File

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

View File

@@ -0,0 +1,126 @@
# Changelog
All notable changes to this project will be documented in this file.
# [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
View 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.

View 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](./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).

View File

@@ -0,0 +1,96 @@
import { APIActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src';
describe('Action Row Components', () => {
describe('Assertion Tests', () => {
test('GIVEN valid components THEN do not throw', () => {
expect(() => new ActionRow().addComponents(new ButtonComponent())).not.toThrowError();
expect(() => new ActionRow().setComponents([new ButtonComponent()])).not.toThrowError();
});
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
const actionRowData: APIActionRowComponent = {
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 ActionRow(actionRowData).toJSON()).toEqual(actionRowData);
expect(new ActionRow().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
expect(() => createComponent({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
// @ts-expect-error
expect(() => createComponent({ type: 42, components: [] })).toThrowError();
});
test('GIVEN valid builder options THEN valid JSON output is given', () => {
const rowWithButtonData: APIActionRowComponent = {
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.Button,
label: 'test',
custom_id: '123',
style: ButtonStyle.Primary,
},
],
};
const rowWithSelectMenuData: APIActionRowComponent = {
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,
},
],
};
const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
const selectMenu = new SelectMenuComponent()
.setCustomId('1234')
.setMaxValues(10)
.setMinValues(12)
.setOptions([
new SelectMenuOption().setLabel('one').setValue('one'),
new SelectMenuOption().setLabel('two').setValue('two'),
]);
expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData);
expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
});
});
});

View File

@@ -0,0 +1,146 @@
import {
APIButtonComponentWithCustomId,
APIButtonComponentWithURL,
ButtonStyle,
ComponentType,
} from 'discord-api-types/v9';
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions';
import { ButtonComponent } from '../../src/components/Button';
const buttonComponent = () => new ButtonComponent();
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 ButtonComponent(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 ButtonComponent(linkData).toJSON()).toEqual(linkData);
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
});
});
});

View File

@@ -0,0 +1,72 @@
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v9';
import { SelectMenuComponent, SelectMenuOption } from '../../src/index';
const selectMenu = () => new SelectMenuComponent();
const selectMenuOption = () => new SelectMenuOption();
const longStr =
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
describe('Button 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().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();
});
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();
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', () => {
const selectMenuOptionData: APISelectMenuOption = {
label: 'test',
value: 'test',
emoji: { name: 'test' },
default: true,
description: 'test',
};
const selectMenuData: APISelectMenuComponent = {
type: ComponentType.SelectMenu,
custom_id: 'test',
max_values: 10,
min_values: 3,
disabled: true,
options: [selectMenuOptionData],
placeholder: 'test',
};
expect(new SelectMenuComponent(selectMenuData).toJSON()).toEqual(selectMenuData);
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
});
});
});

View File

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

View File

@@ -0,0 +1,201 @@
import {
APIApplicationCommandBooleanOption,
APIApplicationCommandChannelOption,
APIApplicationCommandIntegerOption,
APIApplicationCommandMentionableOption,
APIApplicationCommandNumberOption,
APIApplicationCommandRoleOption,
APIApplicationCommandStringOption,
APIApplicationCommandUserOption,
ApplicationCommandOptionType,
ChannelType,
} from 'discord-api-types/v9';
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)
.addChannelType(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)
.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().addChoice('uwu', 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,
});
expect(getNumberOption().setAutocomplete(true).setChoices([]).toJSON()).toEqual<APIApplicationCommandNumberOption>({
name: 'owo',
description: 'Testing 123',
type: ApplicationCommandOptionType.Number,
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(getNumberOption().addChoice('uwu', 1).toJSON()).toEqual<APIApplicationCommandNumberOption>({
name: 'owo',
description: 'Testing 123',
type: ApplicationCommandOptionType.Number,
required: true,
max_value: 10,
min_value: 1,
choices: [{ name: 'uwu', value: 1 }],
});
});
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().addChoice('uwu', '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,
});
});
});

View File

@@ -0,0 +1,428 @@
import { APIApplicationCommandOptionChoice, ChannelType } from 'discord-api-types/v9';
import {
SlashCommandAssertions,
SlashCommandBooleanOption,
SlashCommandBuilder,
SlashCommandChannelOption,
SlashCommandIntegerOption,
SlashCommandMentionableOption,
SlashCommandNumberOption,
SlashCommandRoleOption,
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 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.validateMaxChoicesLength([])).not.toThrowError();
});
test('GIVEN invalid options or choices THEN throw error', () => {
expect(() => SlashCommandAssertions.validateMaxOptionsLength(null)).toThrowError();
expect(() => SlashCommandAssertions.validateMaxChoicesLength(null)).toThrowError();
// Given an array that's too big
expect(() => SlashCommandAssertions.validateMaxOptionsLength(largeArray)).toThrowError();
expect(() => SlashCommandAssertions.validateMaxChoicesLength(largeArray)).toThrowError();
});
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([['Very cool', 1_000]]),
)
.addNumberOption((number) =>
number
.setName('iscool')
.setDescription('Are we cool or what?')
.addChoices([['Very cool', 1.5]]),
)
.addStringOption((string) =>
string
.setName('iscool')
.setDescription('Are we cool or what?')
.addChoices([
['Fancy Pants', 'fp_1'],
['Fancy Shoes', 'fs_1'],
['The Whole shebang', 'all'],
]),
)
.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(
// @ts-expect-error Checking if check works JS-side too
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
getStringOption().setAutocomplete(true).addChoice('Fancy Pants', 'fp_1'),
),
).toThrowError();
expect(() =>
getBuilder().addStringOption(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
getStringOption()
.setAutocomplete(true)
// @ts-expect-error Checking if check works JS-side too
.addChoices([
['Fancy Pants', 'fp_1'],
['Fancy Shoes', 'fs_1'],
['The Whole shebang', 'all'],
]),
),
).toThrowError();
expect(() =>
getBuilder().addStringOption(
// @ts-expect-error Checking if check works JS-side too
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
getStringOption().addChoice('Fancy Pants', 'fp_1').setAutocomplete(true),
),
).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().addChannelType(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().addChannelType(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().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 setting choices to an empty array 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([['owo', 'uwu']]),
),
).toThrowError();
});
test('GIVEN an option, THEN setting choices should not throw an error', () => {
expect(() => getBuilder().addStringOption(getStringOption().setChoices([['owo', '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();
});
});
});
});

View File

@@ -0,0 +1,428 @@
import { Embed } from '../../src';
import type { APIEmbed } from 'discord-api-types/v9';
const emptyEmbed: APIEmbed = {
author: undefined,
color: undefined,
description: undefined,
fields: [],
footer: undefined,
image: undefined,
provider: undefined,
thumbnail: undefined,
title: undefined,
url: undefined,
video: undefined,
};
const alpha = 'abcdefghijklmnopqrstuvwxyz';
describe('Embed', () => {
describe('Embed getters', () => {
test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => {
const embed = new Embed({
title: alpha,
description: alpha,
fields: [{ name: alpha, value: alpha }],
author: { name: alpha },
footer: { text: alpha },
});
expect(embed.length).toBe(alpha.length * 6);
});
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
const embed = new Embed();
expect(embed.length).toBe(0);
});
});
describe('Embed title', () => {
test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => {
const embed = new Embed({ title: 'foo' });
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: 'foo' });
});
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
const embed = new Embed();
embed.setTitle('foo');
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: 'foo' });
});
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
const embed = new Embed({ title: 'foo' });
embed.setTitle(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
});
test('GIVEN an embed with an invalid title THEN throws error', () => {
const embed = new Embed();
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 Embed({ ...emptyEmbed, description: 'foo' });
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: 'foo' });
});
test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => {
const embed = new Embed();
embed.setDescription('foo');
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: 'foo' });
});
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
const embed = new Embed({ description: 'foo' });
embed.setDescription(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
});
test('GIVEN an embed with an invalid description THEN throws error', () => {
const embed = new Embed();
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 Embed({ url: 'https://discord.js.org/' });
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
url: 'https://discord.js.org/',
});
});
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setURL('https://discord.js.org/');
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
url: 'https://discord.js.org/',
});
});
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
const embed = new Embed({ url: 'https://discord.js.org' });
embed.setURL(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
});
test('GIVEN an embed with an invalid URL THEN throws error', () => {
const embed = new Embed();
expect(() => embed.setURL('owo')).toThrowError();
});
});
describe('Embed Color', () => {
test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => {
const embed = new Embed({ color: 0xff0000 });
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: 0xff0000 });
});
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setColor(0xff0000);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: 0xff0000 });
});
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
const embed = new Embed({ color: 0xff0000 });
embed.setColor(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
});
test('GIVEN an embed with an invalid color THEN throws error', () => {
const embed = new Embed();
// @ts-expect-error
expect(() => embed.setColor('RED')).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 Embed({ timestamp: now.toISOString() });
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
});
test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setTimestamp(now);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setTimestamp(now.getTime());
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
});
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setTimestamp();
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: embed.timestamp });
});
test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => {
const embed = new Embed({ timestamp: now.toISOString() });
embed.setTimestamp(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: undefined });
});
});
describe('Embed Thumbnail', () => {
test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => {
const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setThumbnail('https://discord.js.org/static/logo.svg');
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => {
const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
embed.setThumbnail(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
});
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
const embed = new Embed();
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 Embed({ image: { url: 'https://discord.js.org/static/logo.svg' } });
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
image: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setImage('https://discord.js.org/static/logo.svg');
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
image: { url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => {
const embed = new Embed({ image: { url: 'https://discord.js/org/static/logo.svg' } });
embed.setImage(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
});
test('GIVEN an embed with an invalid image THEN throws error', () => {
const embed = new Embed();
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 Embed({
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
});
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setAuthor({
name: 'Wumpus',
iconURL: 'https://discord.js.org/static/logo.svg',
url: 'https://discord.js.org',
});
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
});
test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => {
const embed = new Embed({
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
});
embed.setAuthor(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
});
test('GIVEN an embed with an invalid author name THEN throws error', () => {
const embed = new Embed();
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 Embed({
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' });
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
});
});
test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => {
const embed = new Embed({ footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' } });
embed.setFooter(null);
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
});
test('GIVEN an embed with invalid footer text THEN throws error', () => {
const embed = new Embed();
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 Embed({
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
});
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
});
});
test('GIVEN an embed using Embed#addField THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.addField({ name: 'foo', value: 'bar' });
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
});
});
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.addFields({ name: 'foo', value: 'bar' });
expect(embed.toJSON()).toStrictEqual({
...emptyEmbed,
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
});
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
const embed = new Embed();
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
...emptyEmbed,
fields: [{ name: 'foo', value: 'baz', inline: undefined }],
});
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
const embed = new Embed();
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 Embed();
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
expect(() =>
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
});
describe('GIVEN invalid field amount THEN throws error', () => {
test('', () => {
const embed = new Embed();
expect(() =>
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
});
});
describe('GIVEN invalid field name THEN throws error', () => {
test('', () => {
const embed = new Embed();
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
});
});
describe('GIVEN invalid field name length THEN throws error', () => {
test('', () => {
const embed = new Embed();
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
});
});
describe('GIVEN invalid field value length THEN throws error', () => {
test('', () => {
const embed = new Embed();
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError();
});
});
});
});

View File

@@ -0,0 +1,206 @@
import {
blockQuote,
bold,
channelMention,
codeBlock,
Faces,
formatEmoji,
hideLinkEmbed,
hyperlink,
inlineCode,
italic,
memberNicknameMention,
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('memberNicknameMention', () => {
test('GIVEN memberId THEN returns "<@![memberId]>"', () => {
expect(memberNicknameMention('139836912335716352')).toBe('<@!139836912335716352>');
});
});
describe('channelMention', () => {
test('GIVEN channelId THEN returns "<#[channelId]>"', () => {
expect(channelMention('829924760309334087')).toBe('<#829924760309334087>');
});
});
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('┬─┬ ( ゜-゜ノ)');
});
});
});

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

View 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"

View File

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

View File

@@ -0,0 +1 @@
## [View the documentation here.](https://discord.js.org/#/docs/builders)

View 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'],
};

View File

@@ -0,0 +1,88 @@
{
"name": "@discordjs/builders",
"version": "0.12.0",
"description": "A set of builders that you can use when creating your bot",
"scripts": {
"build": "tsup",
"test": "jest --pass-with-no-tests",
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && node scripts/docs.mjs",
"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"
},
"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": {
"@sindresorhus/is": "^4.3.0",
"discord-api-types": "^0.26.1",
"ts-mixer": "^6.0.0",
"tslib": "^2.3.1",
"zod": "^3.11.6"
},
"devDependencies": {
"@babel/core": "^7.16.12",
"@babel/plugin-proposal-decorators": "^7.16.5",
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.5",
"@discordjs/ts-docgen": "^0.3.4",
"@types/jest": "^27.0.3",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"babel-plugin-transform-typescript-metadata": "^0.3.2",
"eslint": "^8.7.0",
"eslint-config-marine": "^9.3.2",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.4.7",
"prettier": "^2.5.1",
"standard-version": "^9.3.2",
"tsup": "^5.11.11",
"typedoc": "^0.22.11",
"typescript": "^4.5.5"
},
"engines": {
"node": ">=16.9.0"
},
"publishConfig": {
"access": "public"
}
}

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

View File

@@ -0,0 +1,46 @@
import { APIActionRowComponent, ComponentType } from 'discord-api-types/v9';
import type { ButtonComponent, SelectMenuComponent } from '..';
import type { Component } from './Component';
import { createComponent } from './Components';
export type ActionRowComponent = ButtonComponent | SelectMenuComponent;
// TODO: Add valid form component types
/**
* Represents an action row component
*/
export class ActionRow<T extends ActionRowComponent> implements Component {
public readonly components: T[] = [];
public readonly type = ComponentType.ActionRow;
public constructor(data?: APIActionRowComponent) {
this.components = (data?.components.map(createComponent) ?? []) 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[]) {
Reflect.set(this, 'components', [...components]);
return this;
}
public toJSON(): APIActionRowComponent {
return {
...this,
components: this.components.map((component) => component.toJSON()),
};
}
}

View File

@@ -0,0 +1,64 @@
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9';
import { z } from 'zod';
import type { SelectMenuOption } from './selectMenu/SelectMenuOption';
export const customIdValidator = z.string().min(1).max(100);
export const emojiValidator = z
.object({
id: z.string(),
name: z.string(),
animated: z.boolean(),
})
.partial()
.strict();
export const disabledValidator = z.boolean();
export const buttonLabelValidator = z.string().nonempty().max(80);
export const buttonStyleValidator = z.number().int().min(ButtonStyle.Primary).max(ButtonStyle.Link);
export const placeholderValidator = z.string().max(100);
export const minMaxValidator = z.number().int().min(0).max(25);
export const optionsValidator = z.object({}).array().nonempty();
export function validateRequiredSelectMenuParameters(options: SelectMenuOption[], customId?: string) {
customIdValidator.parse(customId);
optionsValidator.parse(options);
}
export const labelValueValidator = z.string().min(1).max(100);
export const defaultValidator = z.boolean();
export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {
labelValueValidator.parse(label);
labelValueValidator.parse(value);
}
export const urlValidator = z.string().url();
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');
}
}

View File

@@ -0,0 +1,105 @@
import { APIButtonComponent, APIMessageComponentEmoji, ButtonStyle, ComponentType } from 'discord-api-types/v9';
import {
buttonLabelValidator,
buttonStyleValidator,
customIdValidator,
disabledValidator,
emojiValidator,
urlValidator,
validateRequiredButtonParameters,
} from './Assertions';
import type { Component } from './Component';
export class ButtonComponent implements Component {
public readonly type = ComponentType.Button as const;
public readonly style!: ButtonStyle;
public readonly label?: string;
public readonly emoji?: APIMessageComponentEmoji;
public readonly disabled?: boolean;
public readonly custom_id!: string;
public readonly url!: string;
public constructor(data?: APIButtonComponent) {
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
this.style = data?.style as ButtonStyle;
this.label = data?.label;
this.emoji = data?.emoji;
this.disabled = data?.disabled;
// This if/else makes typescript happy
if (data?.style === ButtonStyle.Link) {
this.url = data.url;
} else {
this.custom_id = data?.custom_id as string;
}
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
}
/**
* Sets the style of this button
* @param style The style of the button
*/
public setStyle(style: ButtonStyle) {
buttonStyleValidator.parse(style);
Reflect.set(this, 'style', style);
return this;
}
/**
* Sets the URL for this button
* @param url The URL to open when this button is clicked
*/
public setURL(url: string) {
urlValidator.parse(url);
Reflect.set(this, 'url', url);
return this;
}
/**
* Sets the custom Id for this button
* @param customId The custom ID to use for this button
*/
public setCustomId(customId: string) {
customIdValidator.parse(customId);
Reflect.set(this, 'custom_id', customId);
return this;
}
/**
* Sets the emoji to display on this button
* @param emoji The emoji to display on this button
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
emojiValidator.parse(emoji);
Reflect.set(this, 'emoji', emoji);
return this;
}
/**
* Sets whether this button is disable or not
* @param disabled Whether or not to disable this button or not
*/
public setDisabled(disabled: boolean) {
disabledValidator.parse(disabled);
Reflect.set(this, 'disabled', disabled);
return this;
}
/**
* Sets the label for this button
* @param label The label to display on this button
*/
public setLabel(label: string) {
buttonLabelValidator.parse(label);
Reflect.set(this, 'label', label);
return this;
}
public toJSON(): APIButtonComponent {
validateRequiredButtonParameters(this.style, this.label, this.emoji, this.custom_id, this.url);
return {
...this,
};
}
}

View File

@@ -0,0 +1,15 @@
import type { APIMessageComponent, ComponentType } from 'discord-api-types/v9';
/**
* Represents a discord component
*/
export interface Component {
/**
* The type of this component
*/
readonly type: ComponentType;
/**
* Converts this component to an API-compatible JSON object
*/
toJSON: () => APIMessageComponent;
}

View File

@@ -0,0 +1,30 @@
import { APIMessageComponent, ComponentType } from 'discord-api-types/v9';
import { ActionRow, ButtonComponent, Component, SelectMenuComponent } from '../index';
import type { ActionRowComponent } from './ActionRow';
export interface MappedComponentTypes {
[ComponentType.ActionRow]: ActionRow<ActionRowComponent>;
[ComponentType.Button]: ButtonComponent;
[ComponentType.SelectMenu]: SelectMenuComponent;
}
/**
* Factory for creating components from API data
* @param data The api data to transform to a component class
*/
export function createComponent<T extends keyof MappedComponentTypes>(
data: APIMessageComponent & { type: T },
): MappedComponentTypes[T];
export function createComponent(data: APIMessageComponent): Component {
switch (data.type) {
case ComponentType.ActionRow:
return new ActionRow(data);
case ComponentType.Button:
return new ButtonComponent(data);
case ComponentType.SelectMenu:
return new SelectMenuComponent(data);
default:
// @ts-expect-error
throw new Error(`Cannot serialize component type: ${data.type as number}`);
}
}

View File

@@ -0,0 +1,111 @@
import { APISelectMenuComponent, ComponentType } from 'discord-api-types/v9';
import {
customIdValidator,
disabledValidator,
minMaxValidator,
placeholderValidator,
validateRequiredSelectMenuParameters,
} from '../Assertions';
import type { Component } from '../Component';
import { SelectMenuOption } from './SelectMenuOption';
/**
* Represents a select menu component
*/
export class SelectMenuComponent implements Component {
public readonly type = ComponentType.SelectMenu as const;
public readonly options: SelectMenuOption[];
public readonly placeholder?: string;
public readonly min_values?: number;
public readonly max_values?: number;
public readonly custom_id!: string;
public readonly disabled?: boolean;
public constructor(data?: APISelectMenuComponent) {
this.options = data?.options.map((option) => new SelectMenuOption(option)) ?? [];
this.placeholder = data?.placeholder;
this.min_values = data?.min_values;
this.max_values = data?.max_values;
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
this.custom_id = data?.custom_id as string;
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
this.disabled = data?.disabled;
}
/**
* Sets the placeholder for this select menu
* @param placeholder The placeholder to use for this select menu
*/
public setPlaceholder(placeholder: string) {
placeholderValidator.parse(placeholder);
Reflect.set(this, 'placeholder', placeholder);
return this;
}
/**
* Sets thes minimum values that must be selected in the select menu
* @param minValues The minimum values that must be selected
*/
public setMinValues(minValues: number) {
minMaxValidator.parse(minValues);
Reflect.set(this, 'min_values', minValues);
return this;
}
/**
* Sets thes maximum values that must be selected in the select menu
* @param minValues The maximum values that must be selected
*/
public setMaxValues(maxValues: number) {
minMaxValidator.parse(maxValues);
Reflect.set(this, 'max_values', maxValues);
return this;
}
/**
* Sets the custom Id for this select menu
* @param customId The custom ID to use for this select menu
*/
public setCustomId(customId: string) {
customIdValidator.parse(customId);
Reflect.set(this, 'custom_id', customId);
return this;
}
/**
* Sets whether or not this select menu is disabled
* @param disabled Whether or not this select menu is disabled
*/
public setDisabled(disabled: boolean) {
disabledValidator.parse(disabled);
Reflect.set(this, 'disabled', disabled);
return this;
}
/**
* Adds options to this select menu
* @param options The options to add to this select menu
* @returns
*/
public addOptions(...options: SelectMenuOption[]) {
this.options.push(...options);
return this;
}
/**
* Sets the options on this select menu
* @param options The options to set on this select menu
*/
public setOptions(options: SelectMenuOption[]) {
Reflect.set(this, 'options', [...options]);
return this;
}
public toJSON(): APISelectMenuComponent {
validateRequiredSelectMenuParameters(this.options, this.custom_id);
return {
...this,
options: this.options.map((option) => option.toJSON()),
};
}
}

View File

@@ -0,0 +1,83 @@
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v9';
import {
defaultValidator,
emojiValidator,
labelValueValidator,
validateRequiredSelectMenuOptionParameters,
} from '../Assertions';
/**
* Represents an option within a select menu component
*/
export class SelectMenuOption {
public readonly label!: string;
public readonly value!: string;
public readonly description?: string;
public readonly emoji?: APIMessageComponentEmoji;
public readonly default?: boolean;
public constructor(data?: APISelectMenuOption) {
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
this.label = data?.label as string;
this.value = data?.value as string;
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
this.description = data?.description;
this.emoji = data?.emoji;
this.default = data?.default;
}
/**
* Sets the label of this option
* @param label The label to show on this option
*/
public setLabel(label: string) {
Reflect.set(this, 'label', label);
return this;
}
/**
* Sets the value of this option
* @param value The value of this option
*/
public setValue(value: string) {
Reflect.set(this, 'value', value);
return this;
}
/**
* Sets the description of this option.
* @param description The description of this option
*/
public setDescription(description: string) {
labelValueValidator.parse(description);
Reflect.set(this, 'description', description);
return this;
}
/**
* Sets whether this option is selected by default
* @param isDefault Whether or not this option is selected by default
*/
public setDefault(isDefault: boolean) {
defaultValidator.parse(isDefault);
Reflect.set(this, 'default', isDefault);
return this;
}
/**
* Sets the emoji to display on this button
* @param emoji The emoji to display on this button
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
emojiValidator.parse(emoji);
Reflect.set(this, 'emoji', emoji);
return this;
}
public toJSON(): APISelectMenuOption {
validateRequiredSelectMenuOptionParameters(this.label, this.value);
return {
...this,
};
}
}

View File

@@ -0,0 +1,26 @@
export * as EmbedAssertions from './messages/embed/Assertions';
export * from './messages/embed/Embed';
export * from './messages/formatters';
export * as ComponentAssertions from './components/Assertions';
export * from './components/ActionRow';
export * from './components/Button';
export * from './components/Component';
export * from './components/Components';
export * from './components/selectMenu/SelectMenu';
export * from './components/selectMenu/SelectMenuOption';
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/string';
export * from './interactions/slashCommands/options/user';
export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions';
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder';

View File

@@ -0,0 +1,33 @@
import { z } from 'zod';
import { ApplicationCommandType } from 'discord-api-types/v9';
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder';
const namePredicate = z
.string()
.min(1)
.max(32)
.regex(/^( *[\p{L}\p{N}_-]+ *)+$/u);
const typePredicate = z.union([z.literal(ApplicationCommandType.User), z.literal(ApplicationCommandType.Message)]);
const booleanPredicate = z.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);
}

View File

@@ -0,0 +1,83 @@
import { validateRequiredParameters, validateName, validateType, validateDefaultPermission } from './Assertions';
import type { ApplicationCommandType, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v9';
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;

View File

@@ -0,0 +1,84 @@
import is from '@sindresorhus/is';
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v9';
import { z } from 'zod';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
const namePredicate = z
.string()
.min(1)
.max(32)
.regex(/^[\P{Lu}\p{N}_-]+$/u);
export function validateName(name: unknown): asserts name is string {
namePredicate.parse(name);
}
const descriptionPredicate = z.string().min(1).max(100);
export function validateDescription(description: unknown): asserts description is string {
descriptionPredicate.parse(description);
}
const maxArrayLengthPredicate = z.unknown().array().max(25);
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 = z.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);
}
export function validateMaxChoicesLength(choices: APIApplicationCommandOptionChoice[]) {
maxArrayLengthPredicate.parse(choices);
}
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.`);
}
}

View File

@@ -0,0 +1,137 @@
import type { APIApplicationCommandOption, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v9';
import { mix } from 'ts-mixer';
import {
assertReturnOfBuilder,
validateDefaultPermission,
validateMaxOptionsLength,
validateRequiredParameters,
} from './Assertions';
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
import { SharedNameAndDescription } from './mixins/NameAndDescription';
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
@mix(SharedSlashCommandOptions, SharedNameAndDescription)
export class SlashCommandBuilder {
/**
* The name of this slash command
*/
public readonly name: string = undefined!;
/**
* The description of this slash command
*/
public readonly description: string = undefined!;
/**
* 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,
description: this.description,
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;
}

View File

@@ -0,0 +1,111 @@
import {
APIApplicationCommandSubcommandGroupOption,
APIApplicationCommandSubcommandOption,
ApplicationCommandOptionType,
} from 'discord-api-types/v9';
import { mix } from 'ts-mixer';
import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
import { SharedNameAndDescription } from './mixins/NameAndDescription';
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
/**
* Represents a folder for subcommands
*
* 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> {}

View File

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

View File

@@ -0,0 +1,32 @@
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { validateRequiredParameters, validateRequired } from '../Assertions';
import { SharedNameAndDescription } from './NameAndDescription';
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);
}
}

View File

@@ -0,0 +1,55 @@
import { ChannelType } from 'discord-api-types/v9';
import { z, ZodLiteral } from 'zod';
// 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.GuildStore,
ChannelType.GuildNewsThread,
ChannelType.GuildPublicThread,
ChannelType.GuildPrivateThread,
ChannelType.GuildStageVoice,
] as const;
export type ApplicationCommandOptionAllowedChannelTypes = typeof allowedChannelTypes[number];
const channelTypePredicate = z.union(
allowedChannelTypes.map((type) => z.literal(type)) as [
ZodLiteral<ChannelType>,
ZodLiteral<ChannelType>,
...ZodLiteral<ChannelType>[]
],
);
export class ApplicationCommandOptionChannelTypesMixin {
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
/**
* Adds a channel type to this option
*
* @param channelType The type of channel to allow
*/
public addChannelType(channelType: ApplicationCommandOptionAllowedChannelTypes) {
if (this.channel_types === undefined) {
Reflect.set(this, 'channel_types', []);
}
channelTypePredicate.parse(channelType);
this.channel_types!.push(channelType);
return this;
}
/**
* Adds channel types to this option
*
* @param channelTypes The channel types to add
*/
public addChannelTypes(channelTypes: ApplicationCommandOptionAllowedChannelTypes[]) {
channelTypes.forEach((channelType) => this.addChannelType(channelType));
return this;
}
}

View File

@@ -0,0 +1,102 @@
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { z } from 'zod';
import { validateMaxChoicesLength } from '../Assertions';
const stringPredicate = z.string().min(1).max(100);
const numberPredicate = z.number().gt(-Infinity).lt(Infinity);
const choicesPredicate = z.tuple([stringPredicate, z.union([stringPredicate, numberPredicate])]).array();
const booleanPredicate = z.boolean();
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 a choice for this option
*
* @param name The name of the choice
* @param value The value of the choice
*/
public addChoice(name: string, value: T): Omit<this, 'setAutocomplete'> {
if (this.autocomplete) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}
if (this.choices === undefined) {
Reflect.set(this, 'choices', []);
}
validateMaxChoicesLength(this.choices!);
// Validate name
stringPredicate.parse(name);
// Validate the value
if (this.type === ApplicationCommandOptionType.String) {
stringPredicate.parse(value);
} else {
numberPredicate.parse(value);
}
this.choices!.push({ name, value });
return this;
}
/**
* Adds multiple choices for this option
*
* @param choices The choices to add
*/
public addChoices(choices: [name: string, value: T][]): Omit<this, 'setAutocomplete'> {
if (this.autocomplete) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}
choicesPredicate.parse(choices);
for (const [label, value] of choices) this.addChoice(label, value);
return this;
}
public setChoices<Input extends [name: string, value: T][]>(
choices: Input,
): Input extends []
? this & Pick<ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T>, 'setAutocomplete'>
: Omit<this, 'setAutocomplete'> {
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', []);
for (const [label, value] of choices) this.addChoice(label, value);
return this;
}
/**
* Marks the option as autocompletable
* @param autocomplete If this option should be autocompletable
*/
public setAutocomplete<U extends boolean>(
autocomplete: U,
): U extends true
? Omit<this, 'addChoice' | 'addChoices'>
: this & Pick<ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T>, 'addChoice' | 'addChoices'> {
// 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;
}
}

View File

@@ -0,0 +1,34 @@
import { validateDescription, validateName } from '../Assertions';
export class SharedNameAndDescription {
public readonly name!: string;
public readonly description!: string;
/**
* 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;
}
}

View File

@@ -0,0 +1,150 @@
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions';
import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase';
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';
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder';
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 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, 'addChoice' | 'addChoices'>
| ((
builder: SlashCommandStringOption,
) =>
| SlashCommandStringOption
| Omit<SlashCommandStringOption, 'setAutocomplete'>
| Omit<SlashCommandStringOption, 'addChoice' | '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, 'addChoice' | 'addChoices'>
| ((
builder: SlashCommandIntegerOption,
) =>
| SlashCommandIntegerOption
| Omit<SlashCommandIntegerOption, 'setAutocomplete'>
| Omit<SlashCommandIntegerOption, 'addChoice' | '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, 'addChoice' | 'addChoices'>
| ((
builder: SlashCommandNumberOption,
) =>
| SlashCommandNumberOption
| Omit<SlashCommandNumberOption, 'setAutocomplete'>
| Omit<SlashCommandNumberOption, 'addChoice' | 'addChoices'>),
) {
return this._sharedAddOptionMethod(input, SlashCommandNumberOption);
}
private _sharedAddOptionMethod<T extends ApplicationCommandOptionBase>(
input:
| T
| Omit<T, 'setAutocomplete'>
| Omit<T, 'addChoice' | 'addChoices'>
| ((builder: T) => T | Omit<T, 'setAutocomplete'> | Omit<T, 'addChoice' | '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;
}
}

View File

@@ -0,0 +1,12 @@
import { APIApplicationCommandBooleanOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
export class SlashCommandBooleanOption extends ApplicationCommandOptionBase {
public readonly type = ApplicationCommandOptionType.Boolean as const;
public toJSON(): APIApplicationCommandBooleanOption {
this.runRequiredValidations();
return { ...this };
}
}

View File

@@ -0,0 +1,17 @@
import { APIApplicationCommandChannelOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
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 {}

View File

@@ -0,0 +1,46 @@
import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { mix } from 'ts-mixer';
import { z } from 'zod';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
const numberValidator = z.number().int().nonnegative();
@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> {}

View File

@@ -0,0 +1,12 @@
import { APIApplicationCommandMentionableOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
export class SlashCommandMentionableOption extends ApplicationCommandOptionBase {
public readonly type = ApplicationCommandOptionType.Mentionable as const;
public toJSON(): APIApplicationCommandMentionableOption {
this.runRequiredValidations();
return { ...this };
}
}

View File

@@ -0,0 +1,46 @@
import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { mix } from 'ts-mixer';
import { z } from 'zod';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
const numberValidator = z.number().nonnegative();
@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> {}

View File

@@ -0,0 +1,12 @@
import { APIApplicationCommandRoleOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
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 };
}
}

View File

@@ -0,0 +1,21 @@
import { APIApplicationCommandStringOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
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> {}

View File

@@ -0,0 +1,12 @@
import { APIApplicationCommandUserOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
export class SlashCommandUserOption extends ApplicationCommandOptionBase {
public readonly type = ApplicationCommandOptionType.User as const;
public toJSON(): APIApplicationCommandUserOption {
this.runRequiredValidations();
return { ...this };
}
}

View File

@@ -0,0 +1,36 @@
import type { APIEmbedField } from 'discord-api-types/v9';
import { z } from 'zod';
export const fieldNamePredicate = z.string().min(1).max(256);
export const fieldValuePredicate = z.string().min(1).max(1024);
export const fieldInlinePredicate = z.boolean().optional();
export const embedFieldPredicate = z.object({
name: fieldNamePredicate,
value: fieldValuePredicate,
inline: fieldInlinePredicate,
});
export const embedFieldsArrayPredicate = embedFieldPredicate.array();
export const fieldLengthPredicate = z.number().lte(25);
export function validateFieldLength(fields: APIEmbedField[], amountAdding: number): void {
fieldLengthPredicate.parse(fields.length + amountAdding);
}
export const authorNamePredicate = fieldNamePredicate.nullable();
export const urlPredicate = z.string().url().nullish();
export const colorPredicate = z.number().gte(0).lte(0xffffff).nullable();
export const descriptionPredicate = z.string().min(1).max(4096).nullable();
export const footerTextPredicate = z.string().min(1).max(2048).nullable();
export const timestampPredicate = z.union([z.number(), z.date()]).nullable();
export const titlePredicate = fieldNamePredicate.nullable();

View File

@@ -0,0 +1,326 @@
import type {
APIEmbed,
APIEmbedAuthor,
APIEmbedField,
APIEmbedFooter,
APIEmbedImage,
APIEmbedProvider,
APIEmbedThumbnail,
APIEmbedVideo,
} from 'discord-api-types/v9';
import {
authorNamePredicate,
colorPredicate,
descriptionPredicate,
embedFieldsArrayPredicate,
fieldInlinePredicate,
fieldNamePredicate,
fieldValuePredicate,
footerTextPredicate,
timestampPredicate,
titlePredicate,
urlPredicate,
validateFieldLength,
} from './Assertions';
export interface AuthorOptions {
name: string;
url?: string;
iconURL?: string;
}
export interface FooterOptions {
text: string;
iconURL?: string;
}
/**
* Represents an embed in a message (image/video preview, rich embed, etc.)
*/
export class Embed implements APIEmbed {
/**
* An array of fields of this embed
*/
public fields: APIEmbedField[];
/**
* The embed title
*/
public title?: string;
/**
* The embed description
*/
public description?: string;
/**
* The embed url
*/
public url?: string;
/**
* The embed color
*/
public color?: number;
/**
* The timestamp of the embed in the ISO format
*/
public timestamp?: string;
/**
* The embed thumbnail data
*/
public thumbnail?: APIEmbedThumbnail;
/**
* The embed image data
*/
public image?: APIEmbedImage;
/**
* Received video data
*/
public video?: APIEmbedVideo;
/**
* The embed author data
*/
public author?: APIEmbedAuthor;
/**
* Received data about the embed provider
*/
public provider?: APIEmbedProvider;
/**
* The embed footer data
*/
public footer?: APIEmbedFooter;
public constructor(data: APIEmbed = {}) {
this.title = data.title;
this.description = data.description;
this.url = data.url;
this.color = data.color;
this.thumbnail = data.thumbnail;
this.image = data.image;
this.video = data.video;
this.author = data.author;
this.provider = data.provider;
this.footer = data.footer;
this.fields = data.fields ?? [];
if (data.timestamp) this.timestamp = new Date(data.timestamp).toISOString();
}
/**
* The accumulated length for the embed title, description, fields, footer text, and author name
*/
public get length(): number {
return (
(this.title?.length ?? 0) +
(this.description?.length ?? 0) +
this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) +
(this.footer?.text.length ?? 0) +
(this.author?.name.length ?? 0)
);
}
/**
* Adds a field to the embed (max 25)
*
* @param field The field to add.
*/
public addField(field: APIEmbedField): this {
return this.addFields(field);
}
/**
* Adds fields to the embed (max 25)
*
* @param fields The fields to add
*/
public addFields(...fields: APIEmbedField[]): this {
// Data assertions
embedFieldsArrayPredicate.parse(fields);
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(this.fields, fields.length);
this.fields.push(...Embed.normalizeFields(...fields));
return this;
}
/**
* Removes, replaces, or inserts fields in the embed (max 25)
*
* @param index The index to start at
* @param deleteCount The number of fields to remove
* @param fields The replacing field objects
*/
public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
// Data assertions
embedFieldsArrayPredicate.parse(fields);
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(this.fields, fields.length - deleteCount);
this.fields.splice(index, deleteCount, ...Embed.normalizeFields(...fields));
return this;
}
/**
* Sets the author of this embed
*
* @param options The options for the author
*/
public setAuthor(options: AuthorOptions | null): this {
if (options === null) {
this.author = undefined;
return this;
}
const { name, iconURL, url } = options;
// Data assertions
authorNamePredicate.parse(name);
urlPredicate.parse(iconURL);
urlPredicate.parse(url);
this.author = { name, url, icon_url: iconURL };
return this;
}
/**
* Sets the color of this embed
*
* @param color The color of the embed
*/
public setColor(color: number | null): this {
// Data assertions
colorPredicate.parse(color);
this.color = color ?? undefined;
return this;
}
/**
* Sets the description of this embed
*
* @param description The description
*/
public setDescription(description: string | null): this {
// Data assertions
descriptionPredicate.parse(description);
this.description = description ?? undefined;
return this;
}
/**
* Sets the footer of this embed
*
* @param options The options for the footer
*/
public setFooter(options: FooterOptions | null): this {
if (options === null) {
this.footer = undefined;
return this;
}
const { text, iconURL } = options;
// Data assertions
footerTextPredicate.parse(text);
urlPredicate.parse(iconURL);
this.footer = { text, icon_url: iconURL };
return this;
}
/**
* Sets the image of this embed
*
* @param url The URL of the image
*/
public setImage(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
this.image = url ? { url } : undefined;
return this;
}
/**
* Sets the thumbnail of this embed
*
* @param url The URL of the thumbnail
*/
public setThumbnail(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
this.thumbnail = url ? { url } : undefined;
return this;
}
/**
* Sets the timestamp of this embed
*
* @param timestamp The timestamp or date
*/
public setTimestamp(timestamp: number | Date | null = Date.now()): this {
// Data assertions
timestampPredicate.parse(timestamp);
this.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined;
return this;
}
/**
* Sets the title of this embed
*
* @param title The title
*/
public setTitle(title: string | null): this {
// Data assertions
titlePredicate.parse(title);
this.title = title ?? undefined;
return this;
}
/**
* Sets the URL of this embed
*
* @param url The URL
*/
public setURL(url: string | null): this {
// Data assertions
urlPredicate.parse(url);
this.url = url ?? undefined;
return this;
}
/**
* Transforms the embed to a plain object
*/
public toJSON(): APIEmbed {
return { ...this };
}
/**
* Normalizes field input and resolves strings
*
* @param fields Fields to normalize
*/
public static normalizeFields(...fields: APIEmbedField[]): APIEmbedField[] {
return fields.flat(Infinity).map((field) => {
fieldNamePredicate.parse(field.name);
fieldValuePredicate.parse(field.value);
fieldInlinePredicate.parse(field.inline);
return { name: field.name, value: field.value, inline: field.inline ?? undefined };
});
}
}

View File

@@ -0,0 +1,319 @@
import type { Snowflake } from 'discord-api-types/globals';
import type { URL } from 'url';
/**
* Wraps the content inside a codeblock with no language
*
* @param content The content to wrap
*/
export function codeBlock<C extends string>(content: C): `\`\`\`\n${C}\`\`\``;
/**
* Wraps the content inside a codeblock with the specified language
*
* @param language The language for the codeblock
* @param content The content to wrap
*/
export function codeBlock<L extends string, C extends string>(language: L, content: C): `\`\`\`${L}\n${C}\`\`\``;
export function codeBlock(language: string, content?: string): string {
return typeof content === 'undefined' ? `\`\`\`\n${language}\`\`\`` : `\`\`\`${language}\n${content}\`\`\``;
}
/**
* Wraps the content inside \`backticks\`, which formats it as inline code
*
* @param content The content to wrap
*/
export function inlineCode<C extends string>(content: C): `\`${C}\`` {
return `\`${content}\``;
}
/**
* Formats the content into italic text
*
* @param content The content to wrap
*/
export function italic<C extends string>(content: C): `_${C}_` {
return `_${content}_`;
}
/**
* Formats the content into bold text
*
* @param content The content to wrap
*/
export function bold<C extends string>(content: C): `**${C}**` {
return `**${content}**`;
}
/**
* Formats the content into underscored text
*
* @param content The content to wrap
*/
export function underscore<C extends string>(content: C): `__${C}__` {
return `__${content}__`;
}
/**
* Formats the content into strike-through text
*
* @param content The content to wrap
*/
export function strikethrough<C extends string>(content: C): `~~${C}~~` {
return `~~${content}~~`;
}
/**
* Formats the content into a quote. This needs to be at the start of the line for Discord to format it
*
* @param content The content to wrap
*/
export function quote<C extends string>(content: C): `> ${C}` {
return `> ${content}`;
}
/**
* Formats the content into a block quote. This needs to be at the start of the line for Discord to format it
*
* @param content The content to wrap
*/
export function blockQuote<C extends string>(content: C): `>>> ${C}` {
return `>>> ${content}`;
}
/**
* Wraps the URL into `<>`, which stops it from embedding
*
* @param url The URL to wrap
*/
export function hideLinkEmbed<C extends string>(url: C): `<${C}>`;
/**
* Wraps the URL into `<>`, which stops it from embedding
*
* @param url The URL to wrap
*/
export function hideLinkEmbed(url: URL): `<${string}>`;
export function hideLinkEmbed(url: string | URL) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return `<${url}>`;
}
/**
* Formats the content and the URL into a masked URL
*
* @param content The content to display
* @param url The URL the content links to
*/
export function hyperlink<C extends string>(content: C, url: URL): `[${C}](${string})`;
/**
* Formats the content and the URL into a masked URL
*
* @param content The content to display
* @param url The URL the content links to
*/
export function hyperlink<C extends string, U extends string>(content: C, url: U): `[${C}](${U})`;
/**
* Formats the content and the URL into a masked URL
*
* @param content The content to display
* @param url The URL the content links to
* @param title The title shown when hovering on the masked link
*/
export function hyperlink<C extends string, T extends string>(
content: C,
url: URL,
title: T,
): `[${C}](${string} "${T}")`;
/**
* Formats the content and the URL into a masked URL
*
* @param content The content to display
* @param url The URL the content links to
* @param title The title shown when hovering on the masked link
*/
export function hyperlink<C extends string, U extends string, T extends string>(
content: C,
url: U,
title: T,
): `[${C}](${U} "${T}")`;
export function hyperlink(content: string, url: string | URL, title?: string) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
return title ? `[${content}](${url} "${title}")` : `[${content}](${url})`;
}
/**
* Wraps the content inside spoiler (hidden text)
*
* @param content The content to wrap
*/
export function spoiler<C extends string>(content: C): `||${C}||` {
return `||${content}||`;
}
/**
* Formats a user ID into a user mention
*
* @param userId The user ID to format
*/
export function userMention<C extends Snowflake>(userId: C): `<@${C}>` {
return `<@${userId}>`;
}
/**
* Formats a user ID into a member-nickname mention
*
* @param memberId The user ID to format
*/
export function memberNicknameMention<C extends Snowflake>(memberId: C): `<@!${C}>` {
return `<@!${memberId}>`;
}
/**
* Formats a channel ID into a channel mention
*
* @param channelId The channel ID to format
*/
export function channelMention<C extends Snowflake>(channelId: C): `<#${C}>` {
return `<#${channelId}>`;
}
/**
* Formats a role ID into a role mention
*
* @param roleId The role ID to format
*/
export function roleMention<C extends Snowflake>(roleId: C): `<@&${C}>` {
return `<@&${roleId}>`;
}
/**
* Formats an emoji ID into a fully qualified emoji identifier
*
* @param emojiId The emoji ID to format
*/
export function formatEmoji<C extends Snowflake>(emojiId: C, animated?: false): `<:_:${C}>`;
/**
* Formats an emoji ID into a fully qualified emoji identifier
*
* @param emojiId The emoji ID to format
* @param animated Whether the emoji is animated or not. Defaults to `false`
*/
export function formatEmoji<C extends Snowflake>(emojiId: C, animated?: true): `<a:_:${C}>`;
/**
* Formats an emoji ID into a fully qualified emoji identifier
*
* @param emojiId The emoji ID to format
* @param animated Whether the emoji is animated or not. Defaults to `false`
*/
export function formatEmoji<C extends Snowflake>(emojiId: C, animated = false): `<a:_:${C}>` | `<:_:${C}>` {
return `<${animated ? 'a' : ''}:_:${emojiId}>`;
}
/**
* Formats a date into a short date-time string
*
* @param date The date to format, defaults to the current time
*/
export function time(date?: Date): `<t:${bigint}>`;
/**
* Formats a date given a format style
*
* @param date The date to format
* @param style The style to use
*/
export function time<S extends TimestampStylesString>(date: Date, style: S): `<t:${bigint}:${S}>`;
/**
* Formats the given timestamp into a short date-time string
*
* @param seconds The time to format, represents an UNIX timestamp in seconds
*/
export function time<C extends number>(seconds: C): `<t:${C}>`;
/**
* Formats the given timestamp into a short date-time string
*
* @param seconds The time to format, represents an UNIX timestamp in seconds
* @param style The style to use
*/
export function time<C extends number, S extends TimestampStylesString>(seconds: C, style: S): `<t:${C}:${S}>`;
export function time(timeOrSeconds?: number | Date, style?: TimestampStylesString): string {
if (typeof timeOrSeconds !== 'number') {
timeOrSeconds = Math.floor((timeOrSeconds?.getTime() ?? Date.now()) / 1000);
}
return typeof style === 'string' ? `<t:${timeOrSeconds}:${style}>` : `<t:${timeOrSeconds}>`;
}
/**
* The [message formatting timestamp styles](https://discord.com/developers/docs/reference#message-formatting-timestamp-styles) supported by Discord
*/
export const TimestampStyles = {
/**
* Short time format, consisting of hours and minutes, e.g. 16:20
*/
ShortTime: 't',
/**
* Long time format, consisting of hours, minutes, and seconds, e.g. 16:20:30
*/
LongTime: 'T',
/**
* Short date format, consisting of day, month, and year, e.g. 20/04/2021
*/
ShortDate: 'd',
/**
* Long date format, consisting of day, month, and year, e.g. 20 April 2021
*/
LongDate: 'D',
/**
* Short date-time format, consisting of short date and short time formats, e.g. 20 April 2021 16:20
*/
ShortDateTime: 'f',
/**
* Long date-time format, consisting of long date and short time formats, e.g. Tuesday, 20 April 2021 16:20
*/
LongDateTime: 'F',
/**
* Relative time format, consisting of a relative duration format, e.g. 2 months ago
*/
RelativeTime: 'R',
} as const;
/**
* The possible values, see {@link TimestampStyles} for more information
*/
export type TimestampStylesString = typeof TimestampStyles[keyof typeof TimestampStyles];
/**
* An enum with all the available faces from Discord's native slash commands
*/
export enum Faces {
/**
* ¯\\_(ツ)\\_/¯
*/
Shrug = '¯\\_(ツ)\\_/¯',
/**
* (╯°□°)╯︵ ┻━┻
*/
Tableflip = '(╯°□°)╯︵ ┻━┻',
/**
* ┬─┬ ( ゜-゜ノ)
*/
Unflip = '┬─┬ ( ゜-゜ノ)',
}

View File

@@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": true
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.mjs",
"**/*.jsx",
"**/*.test.ts",
"**/*.test.js",
"**/*.test.mjs",
"**/*.spec.ts",
"**/*.spec.js",
"**/*.spec.mjs"
],
"exclude": []
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,12 @@
import type { Options } from 'tsup';
export const tsup: Options = {
clean: true,
dts: true,
entryPoints: ['src/index.ts'],
format: ['esm', 'cjs'],
minify: true,
skipNodeModulesBundle: true,
sourcemap: true,
target: 'es2021',
};

View File

@@ -0,0 +1,12 @@
{
"root": true,
"extends": "marine/prettier/node",
"parserOptions": {
"project": "./tsconfig.eslint.json",
"extraFileExtensions": [".mjs"]
},
"ignorePatterns": ["**/dist/*"],
"env": {
"jest": true
}
}

27
packages/collection/.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# 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
# Miscellaneous
.tmp/
coverage/
tsconfig.tsbuildinfo

View File

@@ -0,0 +1,8 @@
# Autogenerated
CHANGELOG.md
.turbo
dist/
docs/**/*
!docs/index.yml
!docs/README.md
coverage/

View File

@@ -0,0 +1,8 @@
{
"printWidth": 120,
"useTabs": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf"
}

View File

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

View File

@@ -0,0 +1,47 @@
# Changelog
All notable changes to this project will be documented in this file.
# [0.5.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@0.4.0...@discordjs/collection@0.5.0) (2021-12-08)
## Refactor
- Make `intersect` perform a true intersection (#7211) ([d8efba2](https://github.com/discordjs/discord.js/commit/d8efba24e09aa2a8dbf028fc57a561a56e7833fd))
## Typings
- Add `ReadonlyCollection` (#7245) ([db25f52](https://github.com/discordjs/discord.js/commit/db25f529b26d7c819c1c42ad3e26c2263ea2da0e))
- **Collection:** Union types on `intersect` and `difference` (#7196) ([1f9b922](https://github.com/discordjs/discord.js/commit/1f9b9225f2066e9cc66c3355417139fd25cc403c))
# [0.4.0](https://github.com/discordjs/collection/compare/v0.3.2...v0.4.0) (2021-12-24)
## Features
- add #reverse ([#48](https://github.com/discordjs/collection/issues/48)) ([8bcb5e2](https://github.com/discordjs/collection/commit/8bcb5e21bcc15f5b77612d8ff03dec6c37f4d449))
- add Collection#ensure ([#52](https://github.com/discordjs/collection/issues/52)) ([3809eb4](https://github.com/discordjs/collection/commit/3809eb4d18e70459355d310919a3f57747eee3dd))
# [0.3.2](https://github.com/discordjs/collection/compare/v0.3.1...v0.3.2) (2021-10-29)
## Bug Fixes
- update doc engine ([4c0e24f](https://github.com/discordjs/collection/commit/4c0e24fae0323db9de1991db9cfacc093d529abc))
# [0.3.0](https://github.com/discordjs/collection/compare/v0.2.4...v0.3.0) (2021-10-29)
## Features
- add Collection#at() and Collection#keyAt() ([#46](https://github.com/discordjs/collection/issues/46)) ([66b30b9](https://github.com/discordjs/collection/commit/66b30b91069502493383c059cc38e27c152bf541))
- improve documentation and resolve [#49](https://github.com/discordjs/collection/issues/49) ([aec01c6](https://github.com/discordjs/collection/commit/aec01c6ae3ff50b0b5f7c070bff10f01bf98d803))
- ts-docgen ([463b131](https://github.com/discordjs/collection/commit/463b1314e60f2debc526454a6ccd7ce8a9a4ae8a))
# [0.2.4](https://github.com/discordjs/collection/compare/v0.2.3...v0.2.4) (2021-10-27)
## Bug Fixes
- minification of names ([bd2fe2a](https://github.com/discordjs/collection/commit/bd2fe2a47c38f634b0334fe6e89f30f6f6a0b1f5))
# [0.2.3](https://github.com/discordjs/collection/compare/v0.2.2...v0.2.3) (2021-10-27)
### Bug Fixes
- building with useDefineForClassFields false ([2a571d5](https://github.com/discordjs/collection/commit/2a571d5a2c90ed8b708c3c5c017e2f225cd494e9))

View File

@@ -0,0 +1,46 @@
<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/collection"><img src="https://img.shields.io/npm/v/@discordjs/collection.svg?maxAge=3600" alt="npm version" /></a>
<a href="https://www.npmjs.com/package/@discordjs/collection"><img src="https://img.shields.io/npm/dt/@discordjs/collection.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>
</p>
</div>
## About
`@discordjs/collection` is a powerful utility data structure used in discord.js.
## Installation
**Node.js 16.9.0 or newer is required.**
```sh-session
npm install @discordjs/collection
yarn add @discordjs/collection
pnpm add @discordjs/collection
```
## Links
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
- [Documentation](https://discord.js.org/#/docs/collection)
- [discord.js Discord server](https://discord.gg/djs)
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/collection)
- [npm](https://www.npmjs.com/package/@discordjs/collection)
## 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/collection).
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).

View File

@@ -0,0 +1,462 @@
import Collection from '../src';
type TestCollection = Collection<string, number>;
test('do basic map operations', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
expect(coll.size).toEqual(1);
expect(coll.has('a')).toBeTruthy();
expect(coll.get('a')).toStrictEqual(1);
coll.delete('a');
expect(coll.has('a')).toBeFalsy();
expect(coll.get('a')).toStrictEqual(undefined);
coll.clear();
expect(coll.size).toStrictEqual(0);
});
test('get the first item of the collection', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
expect(coll.first()).toStrictEqual(1);
});
test('get the first 3 items of the collection where size equals', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
expect(coll.first(3)).toStrictEqual([1, 2, 3]);
});
test('get the first 3 items of the collection where size is less', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
expect(coll.first(3)).toStrictEqual([1, 2]);
});
test('get the last item of the collection', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
expect(coll.last()).toStrictEqual(2);
});
test('get the last 3 items of the collection', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
expect(coll.last(3)).toStrictEqual([1, 2, 3]);
});
test('get the last 3 items of the collection where size is less', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
expect(coll.last(3)).toStrictEqual([1, 2]);
});
test('find an item in the collection', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
expect(coll.find((x) => x === 1)).toStrictEqual(1);
expect(coll.find((x) => x === 10)).toStrictEqual(undefined);
});
test('sweep items from the collection', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
const n1 = coll.sweep((x) => x === 2);
expect(n1).toStrictEqual(1);
expect([...coll.values()]).toStrictEqual([1, 3]);
const n2 = coll.sweep((x) => x === 4);
expect(n2).toStrictEqual(0);
expect([...coll.values()]).toStrictEqual([1, 3]);
});
test('filter items from the collection', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
const filtered = coll.filter((x) => x % 2 === 1);
expect(coll.size).toStrictEqual(3);
expect(filtered.size).toStrictEqual(2);
expect([...filtered.values()]).toStrictEqual([1, 3]);
});
test('partition a collection into two collections', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
coll.set('d', 4);
coll.set('e', 5);
coll.set('f', 6);
const [even, odd] = coll.partition((x) => x % 2 === 0);
expect([...even.values()]).toStrictEqual([2, 4, 6]);
expect([...odd.values()]).toStrictEqual([1, 3, 5]);
});
test('map items in a collection into an array', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
const mapped = coll.map((x) => x + 1);
expect(mapped).toStrictEqual([2, 3, 4]);
});
test('map items in a collection into a collection', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
const mapped = coll.mapValues((x) => x + 1);
expect([...mapped.values()]).toStrictEqual([2, 3, 4]);
});
test('flatMap items in a collection into a single collection', () => {
const coll = new Collection<string, { a: Collection<string, number> }>();
const coll1 = new Collection<string, number>();
const coll2 = new Collection<string, number>();
coll1.set('z', 1);
coll1.set('x', 2);
coll2.set('c', 3);
coll2.set('v', 4);
coll.set('a', { a: coll1 });
coll.set('b', { a: coll2 });
const mapped = coll.flatMap((x) => x.a);
expect([...mapped.values()]).toStrictEqual([1, 2, 3, 4]);
});
test('check if some items pass a predicate', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
expect(coll.some((x) => x === 2)).toBeTruthy();
});
test('check if every items pass a predicate', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
expect(!coll.every((x) => x === 2)).toBeTruthy();
});
test('reduce collection into a single value with initial value', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
const sum = coll.reduce((a, x) => a + x, 0);
expect(sum).toStrictEqual(6);
});
test('reduce collection into a single value without initial value', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
const sum = coll.reduce((a, x) => a + x, 0);
expect(sum).toStrictEqual(6);
});
test('reduce empty collection without initial value', () => {
const coll: TestCollection = new Collection();
expect(() => coll.reduce((a: number, x) => a + x)).toThrowError(
new TypeError('Reduce of empty collection with no initial value'),
);
});
test('iterate over each item', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
const a: [string, number][] = [];
coll.each((v, k) => a.push([k, v]));
expect(a).toStrictEqual([
['a', 1],
['b', 2],
['c', 3],
]);
});
test('tap the collection', () => {
const coll = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
coll.tap((c) => expect(c).toStrictEqual(coll));
});
test('shallow clone the collection', () => {
const coll = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
const clone = coll.clone();
expect([...coll.values()]).toStrictEqual([...clone.values()]);
});
test('merge multiple collections', () => {
const coll1 = new Collection();
coll1.set('a', 1);
const coll2 = new Collection();
coll2.set('b', 2);
const coll3 = new Collection();
coll3.set('c', 3);
const merged = coll1.concat(coll2, coll3);
expect([...merged.values()]).toStrictEqual([1, 2, 3]);
expect(coll1 !== merged).toBeTruthy();
});
test('check equality of two collections', () => {
const coll1 = new Collection<string, number>();
coll1.set('a', 1);
const coll2 = new Collection<string, number>();
coll2.set('a', 1);
expect(coll1.equals(coll2)).toBeTruthy();
coll2.set('b', 2);
expect(!coll1.equals(coll2)).toBeTruthy();
coll2.clear();
expect(!coll1.equals(coll2)).toBeTruthy();
});
test('sort a collection in place', () => {
const coll: TestCollection = new Collection();
coll.set('a', 3);
coll.set('b', 2);
coll.set('c', 1);
expect([...coll.values()]).toStrictEqual([3, 2, 1]);
coll.sort((a, b) => a - b);
expect([...coll.values()]).toStrictEqual([1, 2, 3]);
});
test('random select from a collection', () => {
const coll: TestCollection = new Collection();
const chars = 'abcdefghijklmnopqrstuvwxyz';
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26];
for (let i = 0; i < chars.length; i++) coll.set(chars[i], numbers[i]);
const random = coll.random(5);
expect(random.length === 5).toBeTruthy();
const set = new Set(random);
expect(set.size === random.length).toBeTruthy();
});
test('when random param > collection size', () => {
const coll = new Collection();
coll.set('a', 3);
coll.set('b', 2);
coll.set('c', 1);
const random = coll.random(5);
expect(random.length).toEqual(coll.size);
});
test('sort a collection', () => {
const coll: TestCollection = new Collection();
coll.set('a', 3);
coll.set('b', 2);
coll.set('c', 1);
expect([...coll.values()]).toStrictEqual([3, 2, 1]);
const sorted = coll.sorted((a, b) => a - b);
expect([...coll.values()]).toStrictEqual([3, 2, 1]);
expect([...sorted.values()]).toStrictEqual([1, 2, 3]);
});
describe('at() tests', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
test('Positive index', () => {
expect(coll.at(0)).toStrictEqual(1);
});
test('Negative index', () => {
expect(coll.at(-1)).toStrictEqual(2);
});
test('Invalid positive index', () => {
expect(coll.at(3)).toBeUndefined();
});
test('Invalid negative index', () => {
expect(coll.at(-3)).toBeUndefined();
});
});
describe('keyAt() tests', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
test('Positive index', () => {
expect(coll.keyAt(0)).toStrictEqual('a');
});
test('Negative index', () => {
expect(coll.keyAt(-1)).toStrictEqual('b');
});
test('Invalid positive index', () => {
expect(coll.keyAt(3)).toBeUndefined();
});
test('Invalid negative index', () => {
expect(coll.keyAt(-3)).toBeUndefined();
});
});
describe('hasAll() tests', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
test('All keys exist in the collection', () => {
expect(coll.hasAll('a', 'b')).toBeTruthy();
});
test('Some keys exist in the collection', () => {
expect(coll.hasAll('b', 'c')).toBeFalsy();
});
test('No keys exist in the collection', () => {
expect(coll.hasAll('c', 'd')).toBeFalsy();
});
});
describe('hasAny() tests', () => {
const coll: TestCollection = new Collection();
coll.set('a', 1);
coll.set('b', 2);
test('All keys exist in the collection', () => {
expect(coll.hasAny('a', 'b')).toBeTruthy();
});
test('Some keys exist in the collection', () => {
expect(coll.hasAny('b', 'c')).toBeTruthy();
});
test('No keys exist in the collection', () => {
expect(coll.hasAny('c', 'd')).toBeFalsy();
});
});
describe('reverse() tests', () => {
const coll = new Collection();
coll.set('a', 1);
coll.set('b', 2);
coll.set('c', 3);
coll.reverse();
expect([...coll.values()]).toStrictEqual([3, 2, 1]);
expect([...coll.keys()]).toStrictEqual(['c', 'b', 'a']);
});
describe('random thisArg tests', () => {
const coll = new Collection();
coll.set('a', 3);
coll.set('b', 2);
coll.set('c', 1);
const object = {};
const string = 'Hi';
const boolean = false;
const symbol = Symbol('testArg');
const array = [1, 2, 3];
coll.set('d', object);
coll.set('e', string);
coll.set('f', boolean);
coll.set('g', symbol);
coll.set('h', array);
test('thisArg test: number', () => {
coll.some(function thisArgTest1(value) {
expect(this.valueOf()).toStrictEqual(1);
expect(typeof this).toEqual('number');
return value === this;
}, 1);
});
test('thisArg test: object', () => {
coll.some(function thisArgTest2(value) {
expect(this).toStrictEqual(object);
expect(this.constructor === Object).toBeTruthy();
return value === this;
}, object);
});
test('thisArg test: string', () => {
coll.some(function thisArgTest3(value) {
expect(this.valueOf()).toStrictEqual(string);
expect(typeof this).toEqual('string');
return value === this;
}, string);
});
test('thisArg test: boolean', () => {
coll.some(function thisArgTest4(value) {
expect(this.valueOf()).toStrictEqual(boolean);
expect(typeof this).toEqual('boolean');
return value === this;
}, boolean);
});
test('thisArg test: symbol', () => {
coll.some(function thisArgTest5(value) {
expect(this.valueOf()).toStrictEqual(symbol);
expect(typeof this).toEqual('symbol');
return value === this;
}, symbol);
});
test('thisArg test: array', () => {
coll.some(function thisArgTest6(value) {
expect(this).toStrictEqual(array);
expect(Array.isArray(this)).toBeTruthy();
return value === this;
}, array);
});
});
describe('ensure() tests', () => {
function createTestCollection() {
return new Collection([
['a', 1],
['b', 2],
]);
}
test('set new value if key does not exist', () => {
const coll = createTestCollection();
coll.ensure('c', () => 3);
expect(coll.size).toStrictEqual(3);
expect(coll.get('c')).toStrictEqual(3);
});
test('return existing value if key exists', () => {
const coll = createTestCollection();
const ensureB = coll.ensure('b', () => 3);
const getB = coll.get('b');
expect(ensureB).toStrictEqual(2);
expect(getB).toStrictEqual(2);
expect(coll.size).toStrictEqual(2);
});
});

View File

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

View File

@@ -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\\/collection@.*"
skip_tags = "v[0-9]*|11|12"
ignore_tags = ""
topo_order = false
sort_commits = "newest"

View File

@@ -0,0 +1 @@
## [View the documentation here.](https://discord.js.org/#/docs/collection)

View File

@@ -0,0 +1,5 @@
- name: General
files:
- name: Welcome
id: welcome
path: ../../README.md

View File

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

View File

@@ -0,0 +1,75 @@
{
"name": "@discordjs/collection",
"version": "0.5.0",
"description": "Utility data structure used in discord.js",
"scripts": {
"test": "jest --pass-with-no-tests",
"build": "tsup",
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && node scripts/docs.mjs",
"prepublishOnly": "yarn build && yarn lint && yarn test",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/collection/*'"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"directories": {
"lib": "src",
"test": "__tests__"
},
"files": [
"dist"
],
"contributors": [
"Crawl <icrawltogo@gmail.com>",
"Amish Shah <amishshah.2k@gmail.com>",
"SpaceEEC <spaceeec@yahoo.com>",
"Vlad Frangu <kingdgrizzle@gmail.com>",
"Antonio Roman <kyradiscord@gmail.com>"
],
"license": "Apache-2.0",
"keywords": [
"map",
"collection",
"utility"
],
"repository": {
"type": "git",
"url": "git+https://github.com/discordjs/discord.js.git"
},
"bugs": {
"url": "https://github.com/discordjs/discord.js/issues"
},
"homepage": "https://discord.js.org",
"devDependencies": {
"@babel/core": "^7.16.12",
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.5",
"@discordjs/ts-docgen": "^0.3.4",
"@types/jest": "^27.0.3",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"eslint": "^8.7.0",
"eslint-config-marine": "^9.3.2",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.4.7",
"prettier": "^2.5.1",
"standard-version": "^9.3.2",
"tsup": "^5.11.11",
"typedoc": "^0.22.11",
"typescript": "^4.5.5"
},
"engines": {
"node": ">=16.9.0"
},
"publishConfig": {
"access": "public"
}
}

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