mirror of
https://github.com/discordjs/discord.js.git
synced 2026-05-23 20:10:08 +00:00
Compare commits
299 Commits
@discordjs
...
@discordjs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e92d937bcc | ||
|
|
091f1bd170 | ||
|
|
bd2cf20ecb | ||
|
|
8af916dba0 | ||
|
|
781a6e013c | ||
|
|
2172a00c50 | ||
|
|
271b1c8e5d | ||
|
|
6b8ef20cb3 | ||
|
|
a45bef4cad | ||
|
|
1ba2d2a898 | ||
|
|
e518c8a137 | ||
|
|
80cd4a4a43 | ||
|
|
8a7cd10554 | ||
|
|
251862ea57 | ||
|
|
2f03f9ad3f | ||
|
|
db81127886 | ||
|
|
cdd2ba036a | ||
|
|
97eaab35d7 | ||
|
|
d60c464e61 | ||
|
|
643dab3b1b | ||
|
|
68d5169f66 | ||
|
|
7f4d411e73 | ||
|
|
636d4f263b | ||
|
|
fdeac9d9fb | ||
|
|
adf461baf4 | ||
|
|
191510b7f8 | ||
|
|
e2f5a4a494 | ||
|
|
993eb74475 | ||
|
|
dfe449c253 | ||
|
|
5e9b757a37 | ||
|
|
28172ca7b5 | ||
|
|
7ce641d33a | ||
|
|
e92b17d855 | ||
|
|
d1504f2ae1 | ||
|
|
b1a3aa97ea | ||
|
|
d522320aa4 | ||
|
|
76694c1497 | ||
|
|
de3f1573f0 | ||
|
|
aed687b09f | ||
|
|
e07b910e68 | ||
|
|
d1ec8c37ff | ||
|
|
4515a1ea80 | ||
|
|
c1b5e731da | ||
|
|
64bdf53116 | ||
|
|
d308c66eec | ||
|
|
dfd9eb20b2 | ||
|
|
19eaed6390 | ||
|
|
4ba0f56b6a | ||
|
|
5eeef3f708 | ||
|
|
679dcda970 | ||
|
|
df64d3ea38 | ||
|
|
6239d83c4d | ||
|
|
0c18dab128 | ||
|
|
ece628986c | ||
|
|
f4ccc6772c | ||
|
|
5ba7740fcf | ||
|
|
bfeaf856f7 | ||
|
|
361709332b | ||
|
|
61a44c509c | ||
|
|
4ac91c61d0 | ||
|
|
4972bd87c1 | ||
|
|
a1b145e0ce | ||
|
|
29293d7bbb | ||
|
|
f22245e9d0 | ||
|
|
585169f2f0 | ||
|
|
ec8d87f932 | ||
|
|
440ac243ca | ||
|
|
c5fb548529 | ||
|
|
a6d9ce57c6 | ||
|
|
08574763eb | ||
|
|
9ef7ffdc6d | ||
|
|
9fa92ac0f9 | ||
|
|
2c2f88cd43 | ||
|
|
93defeccce | ||
|
|
443533ba99 | ||
|
|
3cc96d7940 | ||
|
|
b94a8761f8 | ||
|
|
9bf2a0d5cb | ||
|
|
9917981f24 | ||
|
|
ab8b946276 | ||
|
|
bcf7f1cfad | ||
|
|
a58556adc0 | ||
|
|
40b9a1d67d | ||
|
|
ab4c608b97 | ||
|
|
1b2d8decb6 | ||
|
|
a674f64e1d | ||
|
|
54e5629986 | ||
|
|
75b6770933 | ||
|
|
c2866504a3 | ||
|
|
f1d0084da2 | ||
|
|
b01f4147d4 | ||
|
|
fc2a8bb675 | ||
|
|
f094e33861 | ||
|
|
446eb390ce | ||
|
|
96a0d83a13 | ||
|
|
01a423d110 | ||
|
|
f0d0242c76 | ||
|
|
b577bcc1df | ||
|
|
0faac04b69 | ||
|
|
9ff54254d8 | ||
|
|
fd1dc72c0a | ||
|
|
29f8807955 | ||
|
|
6f4e97bfaf | ||
|
|
3582fe917d | ||
|
|
78a3afcd7f | ||
|
|
3db20abdd2 | ||
|
|
ebb4dfa262 | ||
|
|
8eaec114a9 | ||
|
|
8625d81714 | ||
|
|
3037fca196 | ||
|
|
e4f27051ca | ||
|
|
25fdb3894d | ||
|
|
a1329bd3eb | ||
|
|
3c0bbac82f | ||
|
|
3f3e4327c8 | ||
|
|
6fec25239d | ||
|
|
aedddb875e | ||
|
|
402514ff32 | ||
|
|
3b3dabf3da | ||
|
|
eb6b472f72 | ||
|
|
f88e1ac4be | ||
|
|
905a6a1166 | ||
|
|
5748dbe087 | ||
|
|
ac4bc3a6c8 | ||
|
|
520f471ac5 | ||
|
|
6708533376 | ||
|
|
9afc03054e | ||
|
|
74bf7d57ab | ||
|
|
8e3b2d7abd | ||
|
|
8880de0cec | ||
|
|
cedd0536ba | ||
|
|
85e531f22d | ||
|
|
07b23a99c7 | ||
|
|
0c32332a5a | ||
|
|
d5369a56e3 | ||
|
|
9a6e691eaa | ||
|
|
4d2b55955d | ||
|
|
cd79bef254 | ||
|
|
c684ac55e1 | ||
|
|
fb9a9c2211 | ||
|
|
daf2829cb5 | ||
|
|
98177aa38d | ||
|
|
b1d63d919a | ||
|
|
b520c3df3c | ||
|
|
e805777a7a | ||
|
|
72577c4bfd | ||
|
|
9b0d8cb2d8 | ||
|
|
8fb98165a9 | ||
|
|
f4729759f6 | ||
|
|
2297c2b947 | ||
|
|
87a6b8445b | ||
|
|
549716e4fc | ||
|
|
230c0c4cb1 | ||
|
|
dcd479767b | ||
|
|
3dff31f63f | ||
|
|
cbdb408dff | ||
|
|
e787cd5fa5 | ||
|
|
b162f27e46 | ||
|
|
b9ff7b0573 | ||
|
|
c12d61a342 | ||
|
|
e71c76c7f7 | ||
|
|
851f380eb1 | ||
|
|
10607dbdaf | ||
|
|
79d6c0489c | ||
|
|
89073903a2 | ||
|
|
8f1986a6aa | ||
|
|
0d7e4edd96 | ||
|
|
fac55bcfd1 | ||
|
|
4b08d9b376 | ||
|
|
93854a8013 | ||
|
|
cb566c8b6a | ||
|
|
6f7a366956 | ||
|
|
ed92015634 | ||
|
|
53defb82e3 | ||
|
|
8478d2f4de | ||
|
|
2d4971b032 | ||
|
|
c6cb5e9ebb | ||
|
|
a8321d8026 | ||
|
|
1a14c0ca56 | ||
|
|
44a57a1b0c | ||
|
|
0aa48516a4 | ||
|
|
83460037be | ||
|
|
8203c5d843 | ||
|
|
51583320d3 | ||
|
|
cf669301c7 | ||
|
|
d1d1b076be | ||
|
|
00728f72b3 | ||
|
|
4f306521d8 | ||
|
|
6a2fa70b8e | ||
|
|
46b53f4365 | ||
|
|
78aa36f9f5 | ||
|
|
3baa340821 | ||
|
|
ba31203a0a | ||
|
|
8dbd34544c | ||
|
|
942ea1acbf | ||
|
|
b3fa2ece40 | ||
|
|
ffecf08495 | ||
|
|
3e105a0bbb | ||
|
|
b12214922c | ||
|
|
71f4fa82ed | ||
|
|
f7257f0765 | ||
|
|
395a68ff49 | ||
|
|
dee27db35a | ||
|
|
d32db8833e | ||
|
|
5cf5071061 | ||
|
|
2d4554440e | ||
|
|
7959a68d8e | ||
|
|
d2bc9d444f | ||
|
|
868e2f3230 | ||
|
|
c1b27f8eed | ||
|
|
9311fa7b42 | ||
|
|
6d3da226d3 | ||
|
|
a8106f7c58 | ||
|
|
32985109c3 | ||
|
|
2d2de1d3fd | ||
|
|
d1bb36256f | ||
|
|
36173590a7 | ||
|
|
003439671d | ||
|
|
0dfdb2cf11 | ||
|
|
ae0f35f51d | ||
|
|
0af9bc841f | ||
|
|
e8252ed3b9 | ||
|
|
3ae6f3c313 | ||
|
|
6ce906a02f | ||
|
|
532846b1f8 | ||
|
|
94bf727cc3 | ||
|
|
f4953647ff | ||
|
|
861f0e2134 | ||
|
|
81d8b54ff6 | ||
|
|
fa97a31504 | ||
|
|
55b388a763 | ||
|
|
fbc71ef6b6 | ||
|
|
b97aedd8e1 | ||
|
|
298b22604b | ||
|
|
fe11ff5f6e | ||
|
|
dd751ae19d | ||
|
|
f59d6305cb | ||
|
|
09098240bf | ||
|
|
cc25455d2c | ||
|
|
3d8c77600b | ||
|
|
83458ff7c7 | ||
|
|
b936103395 | ||
|
|
a921ec7dc5 | ||
|
|
aadfbda586 | ||
|
|
538e9cef45 | ||
|
|
f2a7a9f1b3 | ||
|
|
2800e07e59 | ||
|
|
d8184f94dd | ||
|
|
cd2f566052 | ||
|
|
8bb3751340 | ||
|
|
733ac82d5d | ||
|
|
a7b80b9d9b | ||
|
|
5f4b44d580 | ||
|
|
c15100574b | ||
|
|
1c186fabeb | ||
|
|
6faeddcd0d | ||
|
|
87ca2854c2 | ||
|
|
741452b9be | ||
|
|
37c1cb4495 | ||
|
|
cd5c7fa20e | ||
|
|
6b6222bf51 | ||
|
|
04502ce702 | ||
|
|
51beda56f7 | ||
|
|
92a04f4d98 | ||
|
|
0b866c9fb2 | ||
|
|
4abb28c0a1 | ||
|
|
34120bba97 | ||
|
|
088394367b | ||
|
|
0803665183 | ||
|
|
875c86a4ef | ||
|
|
e6a26d25b3 | ||
|
|
388f53550c | ||
|
|
567db60475 | ||
|
|
fbb1d0328b | ||
|
|
74f627c379 | ||
|
|
3a5ab2c4e5 | ||
|
|
00ce1c56ac | ||
|
|
72767a1059 | ||
|
|
1a6c5ab145 | ||
|
|
355f579771 | ||
|
|
e4bd07b239 | ||
|
|
e1ecc1a80a | ||
|
|
11e5e5ac5b | ||
|
|
ec0fba1ed0 | ||
|
|
ac26d9b130 | ||
|
|
2db0cdd357 | ||
|
|
9a566e8068 | ||
|
|
d6b56d0080 | ||
|
|
b6402723c3 | ||
|
|
706db9228a | ||
|
|
47633f0fd2 | ||
|
|
67250382f9 | ||
|
|
5ccdb0ab26 | ||
|
|
9a1623425a | ||
|
|
c05b38873b | ||
|
|
31768fcd69 | ||
|
|
bcc5cda8a9 | ||
|
|
d2d3a80c55 | ||
|
|
2f16f879aa |
@@ -5,6 +5,7 @@
|
||||
2,
|
||||
"always",
|
||||
["chore", "build", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "types", "typings"]
|
||||
]
|
||||
],
|
||||
"scope-case": [0]
|
||||
}
|
||||
}
|
||||
|
||||
8
.github/COMMIT_CONVENTION.md
vendored
8
.github/COMMIT_CONVENTION.md
vendored
@@ -15,13 +15,13 @@ Messages must be matched by the following regex:
|
||||
Appears under "Features" header, `GuildMember` subheader:
|
||||
|
||||
```
|
||||
feat(guildmember): add 'tag' method
|
||||
feat(GuildMember): add 'tag' method
|
||||
```
|
||||
|
||||
Appears under "Bug Fixes" header, `Guild` subheader, with a link to issue #28:
|
||||
|
||||
```
|
||||
fix(guild): handle events correctly
|
||||
fix(Guild): handle events correctly
|
||||
|
||||
close #28
|
||||
```
|
||||
@@ -37,7 +37,7 @@ BREAKING CHANGE: The 'bar' option has been removed.
|
||||
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
|
||||
|
||||
```
|
||||
revert: feat(managers): add Managers
|
||||
revert: feat(Managers): add Managers
|
||||
|
||||
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
|
||||
```
|
||||
@@ -68,7 +68,7 @@ Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`
|
||||
|
||||
### Scope
|
||||
|
||||
The scope could be anything specifying the place of the commit change. For example `GuildMember`, `Guild`, `Message`, `MessageEmbed` etc...
|
||||
The scope could be anything specifying the place of the commit change. For example `GuildMember`, `Guild`, `Message`, `TextChannel` etc...
|
||||
|
||||
### Subject
|
||||
|
||||
|
||||
9
.github/CONTRIBUTING.md
vendored
9
.github/CONTRIBUTING.md
vendored
@@ -11,7 +11,8 @@ is a great boon to your development process.
|
||||
To get ready to work on the codebase, please do the following:
|
||||
|
||||
1. Fork & clone the repository, and make sure you're on the **main** branch
|
||||
2. Run `npm ci`
|
||||
3. Code your heart out!
|
||||
4. Run `npm test` to run ESLint and ensure any JSDoc changes are valid
|
||||
5. [Submit a pull request](https://github.com/discordjs/discord.js/compare) (Make sure you follow the [conventional commit format](https://github.com/discordjs/discord.js/blob/main/.github/COMMIT_CONVENTION.md))
|
||||
2. Run `yarn --immutable` ([install](https://yarnpkg.com/getting-started/install))
|
||||
3. Run `yarn build` to build local packages
|
||||
4. Code your heart out!
|
||||
5. Run `yarn test` to run ESLint and ensure any JSDoc changes are valid
|
||||
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare) (Make sure you follow the [conventional commit format](https://github.com/discordjs/discord.js/blob/main/.github/COMMIT_CONVENTION.md))
|
||||
|
||||
46
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
46
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -15,6 +15,7 @@ body:
|
||||
- builders
|
||||
- collection
|
||||
- rest
|
||||
- proxy
|
||||
- voice
|
||||
validations:
|
||||
required: true
|
||||
@@ -90,12 +91,13 @@ body:
|
||||
options:
|
||||
- Not applicable (subpackage bug)
|
||||
- No Partials
|
||||
- USER
|
||||
- CHANNEL
|
||||
- GUILD_MEMBER
|
||||
- MESSAGE
|
||||
- REACTION
|
||||
- GUILD_SCHEDULED_EVENT
|
||||
- User
|
||||
- Channel
|
||||
- GuildMember
|
||||
- Message
|
||||
- Reaction
|
||||
- GuildScheduledEvent
|
||||
- ThreadMember
|
||||
multiple: true
|
||||
validations:
|
||||
required: true
|
||||
@@ -110,22 +112,22 @@ body:
|
||||
options:
|
||||
- Not applicable (subpackage bug)
|
||||
- No Intents
|
||||
- GUILDS
|
||||
- GUILD_MEMBERS
|
||||
- GUILD_BANS
|
||||
- GUILD_EMOJIS_AND_STICKERS
|
||||
- GUILD_INTEGRATIONS
|
||||
- GUILD_WEBHOOKS
|
||||
- GUILD_INVITES
|
||||
- GUILD_VOICE_STATES
|
||||
- GUILD_PRESENCES
|
||||
- GUILD_MESSAGES
|
||||
- GUILD_MESSAGE_REACTIONS
|
||||
- GUILD_MESSAGE_TYPING
|
||||
- DIRECT_MESSAGES
|
||||
- DIRECT_MESSAGE_REACTIONS
|
||||
- DIRECT_MESSAGE_TYPING
|
||||
- GUILD_SCHEDULED_EVENTS
|
||||
- Guilds
|
||||
- GuildMembers
|
||||
- GuildBans
|
||||
- GuildEmojisAndStickers
|
||||
- GuildIntegrations
|
||||
- GuildWebhooks
|
||||
- GuildInvites
|
||||
- GuildVoiceStates
|
||||
- GuildPresences
|
||||
- GuildMessages
|
||||
- GuildMessageReactions
|
||||
- GuildMessageTyping
|
||||
- DirectMessages
|
||||
- DirectMessageReactions
|
||||
- DirectMessageTyping
|
||||
- GuildScheduledEvents
|
||||
multiple: true
|
||||
validations:
|
||||
required: true
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -17,6 +17,7 @@ body:
|
||||
- builders
|
||||
- collection
|
||||
- rest
|
||||
- proxy
|
||||
- voice
|
||||
validations:
|
||||
required: true
|
||||
|
||||
1
.github/auto_assign.yml
vendored
1
.github/auto_assign.yml
vendored
@@ -5,3 +5,4 @@ reviewers:
|
||||
- kyranet
|
||||
- vladfrangu
|
||||
numberOfReviewers: 0
|
||||
runOnDraft: true
|
||||
|
||||
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -14,6 +14,10 @@ chore:
|
||||
- packages/discord.js/*
|
||||
- packages/discord.js/**/*
|
||||
|
||||
'packages:proxy':
|
||||
- packages/proxy/*
|
||||
- packages/proxy/**/*
|
||||
|
||||
'packages:rest':
|
||||
- packages/rest/*
|
||||
- packages/rest/**/*
|
||||
|
||||
2
.github/labels.yml
vendored
2
.github/labels.yml
vendored
@@ -50,6 +50,8 @@
|
||||
color: 'fbca04'
|
||||
- name: 'packages:discord.js'
|
||||
color: 'fbca04'
|
||||
- name: 'packages:proxy'
|
||||
color: 'fbca04'
|
||||
- name: 'packages:rest'
|
||||
color: 'fbca04'
|
||||
- name: 'packages:voice'
|
||||
|
||||
2
.github/workflows/auto-deprecate.yml
vendored
2
.github/workflows/auto-deprecate.yml
vendored
@@ -23,6 +23,6 @@ jobs:
|
||||
run: yarn --immutable
|
||||
|
||||
- name: Deprecate versions
|
||||
run: 'yarn npm-deprecate --name "*dev*" --package "discord.js"'
|
||||
run: 'yarn npm-deprecate --name "*dev*" --package @discordjs/builders @discordjs/collection discord.js @discordjs/rest @discordjs/voice'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
46
.github/workflows/documentation.yml
vendored
46
.github/workflows/documentation.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- 'stable'
|
||||
- '!docs'
|
||||
tags:
|
||||
- '*'
|
||||
- '**'
|
||||
jobs:
|
||||
build:
|
||||
name: Build documentation
|
||||
@@ -62,13 +62,38 @@ jobs:
|
||||
max-parallel: 1
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: ['builders', 'collection', 'discord.js', 'rest', 'voice']
|
||||
package: ['builders', 'collection', 'discord.js', 'proxy', '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: 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 actions
|
||||
run: yarn build --cache-dir=".turbo"
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
@@ -82,7 +107,24 @@ jobs:
|
||||
token: ${{ secrets.DJS_DOCS }}
|
||||
path: 'out'
|
||||
|
||||
- name: Extract package and semver from tag
|
||||
if: env.BRANCH_OR_TAG == 'tag'
|
||||
id: extract-tag
|
||||
uses: ./packages/actions/src/formatTag
|
||||
with:
|
||||
tag: ${{ env.BRANCH_NAME }}
|
||||
|
||||
- name: Move docs to correct directory
|
||||
if: env.BRANCH_OR_TAG == 'tag'
|
||||
env:
|
||||
PACKAGE: ${{ steps.extract-tag.outputs.package }}
|
||||
SEMVER: ${{ steps.extract-tag.outputs.semver }}
|
||||
run: |
|
||||
mkdir -p out/${PACKAGE}
|
||||
mv docs/${PACKAGE}/docs/docs.json out/${PACKAGE}/${SEMVER}.json
|
||||
|
||||
- name: Move docs to correct directory
|
||||
if: env.BRANCH_OR_TAG == 'branch'
|
||||
env:
|
||||
PACKAGE: ${{ matrix.package }}
|
||||
run: |
|
||||
|
||||
4
.github/workflows/pr-automation.yml
vendored
4
.github/workflows/pr-automation.yml
vendored
@@ -12,5 +12,5 @@ jobs:
|
||||
sync-labels: true
|
||||
|
||||
- name: Automatically assign reviewers
|
||||
if: ${{ github.event.action == 'opened' }}
|
||||
uses: kentaro-m/auto-assign-action@v1.1.2
|
||||
if: github.event.action == 'opened'
|
||||
uses: kentaro-m/auto-assign-action@v1.2.1
|
||||
|
||||
26
.github/workflows/publish-dev.yml
vendored
26
.github/workflows/publish-dev.yml
vendored
@@ -6,6 +6,22 @@ on:
|
||||
jobs:
|
||||
npm:
|
||||
name: npm
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- package: '@discordjs/builders'
|
||||
folder: 'builders'
|
||||
- package: '@discordjs/collection'
|
||||
folder: 'collection'
|
||||
- package: 'discord.js'
|
||||
folder: 'discord.js'
|
||||
- package: '@discordjs/proxy'
|
||||
folder: 'proxy'
|
||||
- package: '@discordjs/rest'
|
||||
folder: 'rest'
|
||||
- package: '@discordjs/voice'
|
||||
folder: 'voice'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'discordjs'
|
||||
steps:
|
||||
@@ -32,7 +48,7 @@ jobs:
|
||||
- name: Check previous released version
|
||||
id: pre-release
|
||||
run: |
|
||||
if [[ $(npm view discord.js@dev version | grep -e "$(jq --raw-output '.version' packages/discord.js/package.json).*.$(git rev-parse --short HEAD | cut -b1-3)") ]]; \
|
||||
if [[ $(npm view ${{ matrix.package }}@dev version | grep -e "$(jq --raw-output '.version' packages/${{ matrix.folder }}/package.json)\..*-$(git rev-parse --short HEAD)") ]]; \
|
||||
then echo '::set-output name=release::false'; \
|
||||
else echo '::set-output name=release::true'; fi
|
||||
|
||||
@@ -46,14 +62,14 @@ jobs:
|
||||
|
||||
- name: Deprecate old versions
|
||||
if: steps.pre-release.outputs.release == 'true'
|
||||
run: npm deprecate discord.js@"~$(jq --raw-output '.version' packages/discord.js/package.json)" "no longer supported" || true
|
||||
run: npm deprecate ${{ matrix.package }}@"~$(jq --raw-output '.version' packages/${{ matrix.folder }}/package.json)" "no longer supported" || true
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
if: steps.pre-release.outputs.release == 'true'
|
||||
run: |
|
||||
yarn workspace discord.js version --no-git-tag-version --new-version $(jq --raw-output '.version' packages/discord.js/package.json).$(date +%s).$(git rev-parse --short HEAD)
|
||||
yarn workspace discord.js publish --tag dev || true
|
||||
yarn workspace ${{ matrix.package }} version $(jq --raw-output '.version' packages/${{ matrix.folder }}/package.json).$(date +%s)-$(git rev-parse --short HEAD)
|
||||
yarn workspace ${{ matrix.package }} npm publish --tag dev || true
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -26,3 +26,12 @@ dist/
|
||||
.DS_Store
|
||||
.turbo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# yarn
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -5,5 +5,8 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true,
|
||||
"source.organizeImports": false
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/node_modules": true
|
||||
}
|
||||
}
|
||||
|
||||
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
786
.yarn/releases/yarn-3.2.1.cjs
vendored
Executable file
786
.yarn/releases/yarn-3.2.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
11
.yarnrc.yml
Normal file
11
.yarnrc.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
|
||||
spec: "@yarnpkg/plugin-version"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
||||
@@ -54,7 +54,7 @@ Register a slash command against the Discord API:
|
||||
|
||||
```js
|
||||
const { REST } = require('@discordjs/rest');
|
||||
const { Routes } = require('discord-api-types/v9');
|
||||
const { Routes } = require('discord-api-types/v10');
|
||||
|
||||
const commands = [
|
||||
{
|
||||
@@ -63,7 +63,7 @@ const commands = [
|
||||
},
|
||||
];
|
||||
|
||||
const rest = new REST({ version: '9' }).setToken('token');
|
||||
const rest = new REST({ version: '10' }).setToken('token');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@@ -81,8 +81,8 @@ const rest = new REST({ version: '9' }).setToken('token');
|
||||
Afterwards we can create a quite simple example bot:
|
||||
|
||||
```js
|
||||
const { Client, Intents } = require('discord.js');
|
||||
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
|
||||
const { Client, GatewayIntentBits } = require('discord.js');
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`Logged in as ${client.user.tag}!`);
|
||||
|
||||
60
package.json
60
package.json
@@ -12,7 +12,7 @@
|
||||
"postinstall": "is-ci || husky install",
|
||||
"docs": "turbo run docs",
|
||||
"changelog": "turbo run changelog",
|
||||
"update": "yarn upgrade-interactive --latest"
|
||||
"update": "yarn upgrade-interactive"
|
||||
},
|
||||
"contributors": [
|
||||
"Crawl <icrawltogo@gmail.com>",
|
||||
@@ -38,13 +38,13 @@
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.1.0",
|
||||
"@commitlint/config-angular": "^16.0.0",
|
||||
"@commitlint/cli": "^17.0.2",
|
||||
"@commitlint/config-angular": "^17.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"
|
||||
"husky": "^8.0.1",
|
||||
"prettier": "^2.6.2",
|
||||
"turbo": "^1.2.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -52,51 +52,5 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"turbo": {
|
||||
"baseBranch": "origin/main",
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**",
|
||||
"docs/docs.json"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"coverage/**"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": []
|
||||
},
|
||||
"format": {
|
||||
"outputs": []
|
||||
},
|
||||
"docs": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"docs/docs.json"
|
||||
]
|
||||
},
|
||||
"changelog": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"packageManager": "yarn@3.2.1"
|
||||
}
|
||||
|
||||
12
packages/actions/.eslintrc.json
Normal file
12
packages/actions/.eslintrc.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": "marine/prettier/node",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.eslint.json",
|
||||
"extraFileExtensions": [".mjs"]
|
||||
},
|
||||
"ignorePatterns": ["**/dist/*"],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
29
packages/actions/.gitignore
vendored
Normal file
29
packages/actions/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Packages
|
||||
node_modules/
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Env
|
||||
.env
|
||||
|
||||
# Dist
|
||||
dist/
|
||||
typings/
|
||||
|
||||
docs/**/*
|
||||
!docs/index.yml
|
||||
!docs/README.md
|
||||
!docs/examples/
|
||||
!docs/examples/*.md
|
||||
|
||||
# Miscellaneous
|
||||
.tmp/
|
||||
coverage/
|
||||
8
packages/actions/.prettierignore
Normal file
8
packages/actions/.prettierignore
Normal file
@@ -0,0 +1,8 @@
|
||||
# Autogenerated
|
||||
CHANGELOG.md
|
||||
.turbo
|
||||
dist/
|
||||
docs/**/*
|
||||
!docs/index.yml
|
||||
!docs/README.md
|
||||
coverage/
|
||||
8
packages/actions/.prettierrc.json
Normal file
8
packages/actions/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
191
packages/actions/LICENSE
Normal file
191
packages/actions/LICENSE
Normal file
@@ -0,0 +1,191 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2021 Noel Buechler
|
||||
Copyright 2021 Vlad Frangu
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
53
packages/actions/README.md
Normal file
53
packages/actions/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
<div align="center">
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.gg/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&logo=discord&logoColor=white" alt="Discord server" /></a>
|
||||
<a href="https://www.npmjs.com/package/@discordjs/builders"><img src="https://img.shields.io/npm/v/@discordjs/builders.svg?maxAge=3600" alt="npm version" /></a>
|
||||
<a href="https://www.npmjs.com/package/@discordjs/builders"><img src="https://img.shields.io/npm/dt/@discordjs/builders.svg?maxAge=3600" alt="npm downloads" /></a>
|
||||
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/test.yml/badge.svg" alt="Build status" /></a>
|
||||
<a href="https://codecov.io/gh/discordjs/builders"><img src="https://codecov.io/gh/discordjs/builders/branch/main/graph/badge.svg" alt="Code coverage" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## Installation
|
||||
|
||||
**Node.js 16.9.0 or newer is required.**
|
||||
|
||||
```sh-session
|
||||
npm install @discordjs/builders
|
||||
yarn add @discordjs/builders
|
||||
pnpm add @discordjs/builders
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Here are some examples for the builders and utilities you can find in this package:
|
||||
|
||||
- [Slash Command Builders](https://github.com/discordjs/discord.js/blob/main/packages/builders/docs/examples/Slash%20Command%20Builders.md)
|
||||
|
||||
## Links
|
||||
|
||||
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
|
||||
- [Documentation](https://discord.js.org/#/docs/builders)
|
||||
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
|
||||
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
|
||||
- [discord.js Discord server](https://discord.gg/djs)
|
||||
- [Discord API Discord server](https://discord.gg/discord-api)
|
||||
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/builders)
|
||||
- [npm](https://www.npmjs.com/package/@discordjs/builders)
|
||||
- [Related libraries](https://discord.com/developers/docs/topics/community-resources#libraries)
|
||||
|
||||
## Contributing
|
||||
|
||||
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||
[documentation](https://discord.js.org/#/docs/builders).
|
||||
See [the contribution guide](https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR.
|
||||
|
||||
## Help
|
||||
|
||||
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||
nudge in the right direction, please don't hesitate to join our official [discord.js Server](https://discord.gg/djs).
|
||||
21
packages/actions/__tests__/formatTag.test.ts
Normal file
21
packages/actions/__tests__/formatTag.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { formatTag } from '../src';
|
||||
|
||||
describe('Format Tag', () => {
|
||||
test('GIVEN tag with a prefix THEN format tag to not contain the prefix', () => {
|
||||
expect(formatTag('@discordjs/rest@0.4.0')).toEqual({ package: 'rest', semver: '0.4.0' });
|
||||
expect(formatTag('@discordjs/collection@0.6.0')).toEqual({ package: 'collection', semver: '0.6.0' });
|
||||
expect(formatTag('@discordjs/proxy@0.1.0')).toEqual({ package: 'proxy', semver: '0.1.0' });
|
||||
expect(formatTag('@discordjs/builders@0.13.0')).toEqual({ package: 'builders', semver: '0.13.0' });
|
||||
expect(formatTag('@discordjs/voice@0.9.0')).toEqual({ package: 'voice', semver: '0.9.0' });
|
||||
});
|
||||
|
||||
test('GIVEN tag with no prefix THEN return tag', () => {
|
||||
expect(formatTag('13.5.1')).toEqual({ semver: '13.5.1' });
|
||||
expect(formatTag('13.7.0')).toEqual({ package: undefined, semver: '13.7.0' });
|
||||
});
|
||||
|
||||
test('GIVEN no or invalid tag THEN return null', () => {
|
||||
expect(formatTag('')).toEqual(null);
|
||||
expect(formatTag('abc')).toEqual(null);
|
||||
});
|
||||
});
|
||||
18
packages/actions/babel.config.js
Normal file
18
packages/actions/babel.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @type {import('@babel/core').TransformOptions}
|
||||
*/
|
||||
module.exports = {
|
||||
parserOpts: { strictMode: true },
|
||||
sourceMaps: 'inline',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: { node: 'current' },
|
||||
modules: 'commonjs',
|
||||
},
|
||||
],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['babel-plugin-transform-typescript-metadata', ['@babel/plugin-proposal-decorators', { legacy: true }]],
|
||||
};
|
||||
10
packages/actions/codecov.yml
Normal file
10
packages/actions/codecov.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 70%
|
||||
threshold: 5%
|
||||
patch:
|
||||
default:
|
||||
target: 70%
|
||||
threshold: 5%
|
||||
19
packages/actions/jest.config.js
Normal file
19
packages/actions/jest.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @type {import('@jest/types').Config.InitialOptions}
|
||||
*/
|
||||
module.exports = {
|
||||
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
|
||||
testEnvironment: 'node',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts'],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'clover'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 70,
|
||||
lines: 70,
|
||||
statements: 70,
|
||||
},
|
||||
},
|
||||
coveragePathIgnorePatterns: ['src/index.ts', 'src/formatTag/index.ts'],
|
||||
};
|
||||
73
packages/actions/package.json
Normal file
73
packages/actions/package.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@discordjs/actions",
|
||||
"version": "0.1.0-dev",
|
||||
"description": "A set of actions that we use for our workflows",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"test": "jest --pass-with-no-tests",
|
||||
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
|
||||
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "src",
|
||||
"test": "__tests__"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"contributors": [
|
||||
"Crawl <icrawltogo@gmail.com>"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"api",
|
||||
"bot",
|
||||
"client",
|
||||
"node",
|
||||
"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": {
|
||||
"@actions/core": "^1.8.2",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/plugin-proposal-decorators": "^7.18.2",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@types/jest": "^28.1.0",
|
||||
"@types/node": "^16.11.38",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"babel-plugin-transform-typescript-metadata": "^0.3.2",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-marine": "^9.4.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"jest": "^28.1.0",
|
||||
"prettier": "^2.6.2",
|
||||
"tsup": "^6.0.1",
|
||||
"typedoc": "^0.22.17",
|
||||
"typescript": "^4.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
}
|
||||
14
packages/actions/src/formatTag/action.yml
Normal file
14
packages/actions/src/formatTag/action.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
name: 'Format Tag'
|
||||
description: 'Formats a git tag to remove potentially prefixes'
|
||||
inputs:
|
||||
tag:
|
||||
description: 'The input tag'
|
||||
required: true
|
||||
outputs:
|
||||
package:
|
||||
description: 'The package string that was extracted from this tag'
|
||||
semver:
|
||||
description: 'The semver string that was extracted from this tag'
|
||||
runs:
|
||||
using: node16
|
||||
main: ../../dist/formatTag/index.mjs
|
||||
9
packages/actions/src/formatTag/formatTag.ts
Normal file
9
packages/actions/src/formatTag/formatTag.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function formatTag(tag: string) {
|
||||
const parsed = /(^@.*\/(?<package>.*)@v?)?(?<semver>\d+.\d+.\d+)-?.*/.exec(tag);
|
||||
|
||||
if (parsed?.groups) {
|
||||
return parsed.groups;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
10
packages/actions/src/formatTag/index.ts
Normal file
10
packages/actions/src/formatTag/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { getInput, setOutput } from '@actions/core';
|
||||
import { formatTag } from './formatTag';
|
||||
|
||||
const tag = getInput('tag', { required: true });
|
||||
const parsed = formatTag(tag);
|
||||
|
||||
if (parsed?.groups) {
|
||||
setOutput('package', parsed.package);
|
||||
setOutput('semver', parsed.semver);
|
||||
}
|
||||
1
packages/actions/src/index.ts
Normal file
1
packages/actions/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './formatTag/formatTag';
|
||||
20
packages/actions/tsconfig.eslint.json
Normal file
20
packages/actions/tsconfig.eslint.json
Normal 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": []
|
||||
}
|
||||
4
packages/actions/tsconfig.json
Normal file
4
packages/actions/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
13
packages/actions/tsup.config.ts
Normal file
13
packages/actions/tsup.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
dts: true,
|
||||
entryPoints: ['src/index.ts', 'src/formatTag/index.ts'],
|
||||
format: ['esm'],
|
||||
minify: true,
|
||||
skipNodeModulesBundle: false,
|
||||
noExternal: ['@actions/core'],
|
||||
sourcemap: true,
|
||||
target: 'es2021',
|
||||
});
|
||||
3
packages/builders/.gitignore
vendored
3
packages/builders/.gitignore
vendored
@@ -17,9 +17,12 @@ pids
|
||||
# Dist
|
||||
dist/
|
||||
typings/
|
||||
|
||||
docs/**/*
|
||||
!docs/index.yml
|
||||
!docs/README.md
|
||||
!docs/examples/
|
||||
!docs/examples/*.md
|
||||
|
||||
# Miscellaneous
|
||||
.tmp/
|
||||
|
||||
@@ -2,7 +2,80 @@
|
||||
|
||||
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)
|
||||
# [@discordjs/builders@0.14.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.13.0...@discordjs/builders@0.14.0) - (2022-06-04)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **builders:** Leftover invalid null type ([8a7cd10](https://github.com/discordjs/discord.js/commit/8a7cd10554a2a71cd2fe7f6a177b5f4f43464348))
|
||||
- **SlashCommandBuilder:** Import `Permissions` correctly (#7921) ([7ce641d](https://github.com/discordjs/discord.js/commit/7ce641d33a4af6586d5e7beffbe7d38619dcf1a2))
|
||||
- Add localizations for subcommand builders and option choices (#7862) ([c1b5e73](https://github.com/discordjs/discord.js/commit/c1b5e731daa9cbbfca03a046e47cb1221ee1ed7c))
|
||||
|
||||
## Features
|
||||
|
||||
- Export types from `interactions/slashCommands/mixins` (#7942) ([68d5169](https://github.com/discordjs/discord.js/commit/68d5169f66c96f8fe5be17a1c01cdd5155607ab2))
|
||||
- **builders:** Add new command permissions v2 (#7861) ([de3f157](https://github.com/discordjs/discord.js/commit/de3f1573f07dda294cc0fbb1ca4b659eb2388a12))
|
||||
- **builders:** Improve embed errors and predicates (#7795) ([ec8d87f](https://github.com/discordjs/discord.js/commit/ec8d87f93272cc9987f9613735c0361680c4ed1e))
|
||||
|
||||
## Refactor
|
||||
|
||||
- Use arrays instead of rest parameters for builders (#7759) ([29293d7](https://github.com/discordjs/discord.js/commit/29293d7bbb5ed463e52e5a5853817e5a09cf265b))
|
||||
|
||||
## Styling
|
||||
|
||||
- Cleanup tests and tsup configs ([6b8ef20](https://github.com/discordjs/discord.js/commit/6b8ef20cb3af5b5cfd176dd0aa0a1a1e98551629))
|
||||
|
||||
# [@discordjs/builders@0.13.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.12.0...@discordjs/builders@0.13.0) - (2022-04-17)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Validate select menu options (#7566) ([b1d63d9](https://github.com/discordjs/discord.js/commit/b1d63d919a61f309ac89f27016b0f148678dac2b))
|
||||
- **SelectMenu:** Set `placeholder` max to 150 (#7538) ([dcd4797](https://github.com/discordjs/discord.js/commit/dcd479767b6ec980a373f2ea1f22754f41661c1e))
|
||||
- Only check `instanceof Component` once (#7546) ([0aa4851](https://github.com/discordjs/discord.js/commit/0aa48516a4e33497e8e8dc50da164a57cdee09d3))
|
||||
- **builders:** Allow negative min/max value of number/integer option (#7484) ([3baa340](https://github.com/discordjs/discord.js/commit/3baa340821b8ecf8a16253bc0917a1033250d7c9))
|
||||
- **components:** SetX should take rest parameters (#7461) ([3617359](https://github.com/discordjs/discord.js/commit/36173590a712f041b087b7882054805a8bd42dae))
|
||||
- Unsafe embed builder field normalization (#7418) ([b936103](https://github.com/discordjs/discord.js/commit/b936103395121cb21a8c616f669ddab1d2efb0f1))
|
||||
- Fix some typos (#7393) ([92a04f4](https://github.com/discordjs/discord.js/commit/92a04f4d98f6c6760214034cc8f5a1eaa78893c7))
|
||||
- **builders:** Make type optional in constructor (#7391) ([4abb28c](https://github.com/discordjs/discord.js/commit/4abb28c0a1256c57a60369a6b8ec9e98c265b489))
|
||||
- Don't create new instances of builders classes (#7343) ([d6b56d0](https://github.com/discordjs/discord.js/commit/d6b56d0080c4c5f8ace731f1e8bcae0c9d3fb5a5))
|
||||
|
||||
## Documentation
|
||||
|
||||
- Completely fix builders example link (#7543) ([1a14c0c](https://github.com/discordjs/discord.js/commit/1a14c0ca562ea173d363a770a0437209f461fd23))
|
||||
- Add slash command builders example, fixes #7338 (#7339) ([3ae6f3c](https://github.com/discordjs/discord.js/commit/3ae6f3c313091151245d6e6b52337b459ecfc765))
|
||||
|
||||
## Features
|
||||
|
||||
- Slash command localization for builders (#7683) ([40b9a1d](https://github.com/discordjs/discord.js/commit/40b9a1d67d0b508ec593e030913acd8161cd17f8))
|
||||
- Add API v10 support (#7477) ([72577c4](https://github.com/discordjs/discord.js/commit/72577c4bfd02524a27afb6ff4aebba9301a690d3))
|
||||
- Add support for module: NodeNext in TS and ESM (#7598) ([8f1986a](https://github.com/discordjs/discord.js/commit/8f1986a6aa98365e09b00e84ad5f9f354ab61f3d))
|
||||
- Add Modals and Text Inputs (#7023) ([ed92015](https://github.com/discordjs/discord.js/commit/ed920156344233241a21b0c0b99736a3a855c23c))
|
||||
- Add missing `v13` component methods (#7466) ([f7257f0](https://github.com/discordjs/discord.js/commit/f7257f07655076eabfe355cb6a53260b39ca9670))
|
||||
- **builders:** Add attachment command option type (#7203) ([ae0f35f](https://github.com/discordjs/discord.js/commit/ae0f35f51d68dfa5a7dc43d161ef9365171debdb))
|
||||
- **components:** Add unsafe message component builders (#7387) ([6b6222b](https://github.com/discordjs/discord.js/commit/6b6222bf513d1ee8cd98fba0ad313def560b864f))
|
||||
- **embed:** Add setFields (#7322) ([bcc5cda](https://github.com/discordjs/discord.js/commit/bcc5cda8a902ddb28c7e3578e0f29b4272832624))
|
||||
|
||||
## Refactor
|
||||
|
||||
- Remove nickname parsing (#7736) ([78a3afc](https://github.com/discordjs/discord.js/commit/78a3afcd7fdac358e06764cc0d675e1215c785f3))
|
||||
- Replace zod with shapeshift (#7547) ([3c0bbac](https://github.com/discordjs/discord.js/commit/3c0bbac82fa9988af4a62ff00c66d149fbe6b921))
|
||||
- Remove store channels (#7634) ([aedddb8](https://github.com/discordjs/discord.js/commit/aedddb875e740e1f1bd77f06ce1b361fd3b7bc36))
|
||||
- Allow builders to accept emoji strings (#7616) ([fb9a9c2](https://github.com/discordjs/discord.js/commit/fb9a9c221121ee1c7986f9c775b77b9691a0ae15))
|
||||
- Don't return builders from API data (#7584) ([549716e](https://github.com/discordjs/discord.js/commit/549716e4fcec89ca81216a6d22aa8e623175e37a))
|
||||
- Remove obsolete builder methods (#7590) ([10607db](https://github.com/discordjs/discord.js/commit/10607dbdafe257c5cbf5b952b7eecec4919e8b4a))
|
||||
- **Embed:** Remove add field (#7522) ([8478d2f](https://github.com/discordjs/discord.js/commit/8478d2f4de9ac013733850cbbc67902f7c5abc55))
|
||||
- Make `data` public in builders (#7486) ([ba31203](https://github.com/discordjs/discord.js/commit/ba31203a0ad96e0a00f8312c397889351e4c5cfd))
|
||||
- **embed:** Remove array support in favor of rest params (#7498) ([b3fa2ec](https://github.com/discordjs/discord.js/commit/b3fa2ece402839008738ad3adce3db958445838d))
|
||||
- **components:** Default set boolean methods to true (#7502) ([b122149](https://github.com/discordjs/discord.js/commit/b12214922cea2f43afbe6b1555a74a3c8e16f798))
|
||||
- Make public builder props getters (#7422) ([e8252ed](https://github.com/discordjs/discord.js/commit/e8252ed3b981a4b7e4013f12efadd2f5d9318d3e))
|
||||
- **builders-methods:** Make methods consistent (#7395) ([f495364](https://github.com/discordjs/discord.js/commit/f4953647ff9f39127978c73bf8a62c08462802ca))
|
||||
- Remove conditional autocomplete option return types (#7396) ([0909824](https://github.com/discordjs/discord.js/commit/09098240bfb13b8afafa4ab549f06d236e0ff1c9))
|
||||
- **embed:** Mark properties as readonly (#7332) ([31768fc](https://github.com/discordjs/discord.js/commit/31768fcd69ed5b4566a340bda89ce881418e8272))
|
||||
|
||||
## Typings
|
||||
|
||||
- Fix regressions (#7649) ([5748dbe](https://github.com/discordjs/discord.js/commit/5748dbe08783beb80c526de38ccd105eb0e82664))
|
||||
|
||||
# [@discordjs/builders@0.12.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.11.0...@discordjs/builders@0.12.0) - (2022-01-24)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ pnpm add @discordjs/builders
|
||||
|
||||
Here are some examples for the builders and utilities you can find in this package:
|
||||
|
||||
- [Slash Command Builders](./docs/examples/Slash%20Command%20Builders.md)
|
||||
- [Slash Command Builders](https://github.com/discordjs/discord.js/blob/main/packages/builders/docs/examples/Slash%20Command%20Builders.md)
|
||||
|
||||
## Links
|
||||
|
||||
|
||||
@@ -1,15 +1,55 @@
|
||||
import { APIActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9';
|
||||
import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src';
|
||||
import { APIActionRowComponent, APIMessageActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
createComponentBuilder,
|
||||
SelectMenuBuilder,
|
||||
SelectMenuOptionBuilder,
|
||||
} from '../../src';
|
||||
|
||||
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
label: 'test',
|
||||
custom_id: '123',
|
||||
style: ButtonStyle.Primary,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.SelectMenu,
|
||||
custom_id: '1234',
|
||||
options: [
|
||||
{
|
||||
label: 'one',
|
||||
value: 'one',
|
||||
},
|
||||
{
|
||||
label: 'two',
|
||||
value: 'two',
|
||||
},
|
||||
],
|
||||
max_values: 10,
|
||||
min_values: 12,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Action Row Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid components THEN do not throw', () => {
|
||||
expect(() => new ActionRow().addComponents(new ButtonComponent())).not.toThrowError();
|
||||
expect(() => new ActionRow().setComponents([new ButtonComponent()])).not.toThrowError();
|
||||
expect(() => new ActionRowBuilder().addComponents([new ButtonBuilder()])).not.toThrowError();
|
||||
expect(() => new ActionRowBuilder().setComponents([new ButtonBuilder()])).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||
const actionRowData: APIActionRowComponent = {
|
||||
const actionRowData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -38,14 +78,12 @@ describe('Action Row Components', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(new ActionRow(actionRowData).toJSON()).toEqual(actionRowData);
|
||||
expect(new ActionRow().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
|
||||
expect(() => createComponent({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => createComponent({ type: 42, components: [] })).toThrowError();
|
||||
expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData);
|
||||
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
|
||||
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
||||
});
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const rowWithButtonData: APIActionRowComponent = {
|
||||
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -57,7 +95,7 @@ describe('Action Row Components', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const rowWithSelectMenuData: APIActionRowComponent = {
|
||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
@@ -79,18 +117,24 @@ describe('Action Row Components', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||
const selectMenu = new SelectMenuComponent()
|
||||
expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData);
|
||||
expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData);
|
||||
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
|
||||
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
||||
});
|
||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||
const selectMenu = new SelectMenuBuilder()
|
||||
.setCustomId('1234')
|
||||
.setMaxValues(10)
|
||||
.setMinValues(12)
|
||||
.setOptions([
|
||||
new SelectMenuOption().setLabel('one').setValue('one'),
|
||||
new SelectMenuOption().setLabel('two').setValue('two'),
|
||||
new SelectMenuOptionBuilder().setLabel('one').setValue('one'),
|
||||
new SelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
||||
]);
|
||||
|
||||
expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData);
|
||||
expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
|
||||
expect(new ActionRowBuilder().addComponents([button]).toJSON()).toEqual(rowWithButtonData);
|
||||
expect(new ActionRowBuilder().addComponents([selectMenu]).toJSON()).toEqual(rowWithSelectMenuData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
APIButtonComponentWithURL,
|
||||
ButtonStyle,
|
||||
ComponentType,
|
||||
} from 'discord-api-types/v9';
|
||||
} from 'discord-api-types/v10';
|
||||
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions';
|
||||
import { ButtonComponent } from '../../src/components/Button';
|
||||
import { ButtonBuilder } from '../../src/components/button/Button';
|
||||
|
||||
const buttonComponent = () => new ButtonComponent();
|
||||
const buttonComponent = () => new ButtonBuilder();
|
||||
|
||||
const longStr =
|
||||
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
||||
@@ -119,7 +119,7 @@ describe('Button Components', () => {
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
expect(new ButtonComponent(interactionData).toJSON()).toEqual(interactionData);
|
||||
expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
|
||||
|
||||
expect(
|
||||
buttonComponent()
|
||||
@@ -138,7 +138,7 @@ describe('Button Components', () => {
|
||||
url: 'https://google.com',
|
||||
};
|
||||
|
||||
expect(new ButtonComponent(linkData).toJSON()).toEqual(linkData);
|
||||
expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData);
|
||||
|
||||
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
|
||||
});
|
||||
|
||||
@@ -1,29 +1,74 @@
|
||||
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v9';
|
||||
import { SelectMenuComponent, SelectMenuOption } from '../../src/index';
|
||||
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v10';
|
||||
import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index';
|
||||
|
||||
const selectMenu = () => new SelectMenuComponent();
|
||||
const selectMenuOption = () => new SelectMenuOption();
|
||||
const selectMenu = () => new SelectMenuBuilder();
|
||||
const selectMenuOption = () => new SelectMenuOptionBuilder();
|
||||
|
||||
const longStr =
|
||||
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
||||
const longStr = 'a'.repeat(256);
|
||||
|
||||
describe('Button Components', () => {
|
||||
const selectMenuOptionData: APISelectMenuOption = {
|
||||
label: 'test',
|
||||
value: 'test',
|
||||
emoji: { name: 'test' },
|
||||
default: true,
|
||||
description: 'test',
|
||||
};
|
||||
|
||||
const selectMenuDataWithoutOptions = {
|
||||
type: ComponentType.SelectMenu,
|
||||
custom_id: 'test',
|
||||
max_values: 10,
|
||||
min_values: 3,
|
||||
disabled: true,
|
||||
placeholder: 'test',
|
||||
} as const;
|
||||
|
||||
const selectMenuData: APISelectMenuComponent = {
|
||||
...selectMenuDataWithoutOptions,
|
||||
options: [selectMenuOptionData],
|
||||
};
|
||||
|
||||
describe('Select Menu Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid inputs THEN Select Menu does not throw', () => {
|
||||
expect(() => selectMenu().setCustomId('foo')).not.toThrowError();
|
||||
expect(() => selectMenu().setMaxValues(10)).not.toThrowError();
|
||||
expect(() => selectMenu().setMinValues(3)).not.toThrowError();
|
||||
expect(() => selectMenu().setDisabled(true)).not.toThrowError();
|
||||
expect(() => selectMenu().setDisabled()).not.toThrowError();
|
||||
expect(() => selectMenu().setPlaceholder('description')).not.toThrowError();
|
||||
|
||||
const option = selectMenuOption()
|
||||
.setLabel('test')
|
||||
.setValue('test')
|
||||
.setDefault(true)
|
||||
.setEmoji({ name: 'test' })
|
||||
.setDescription('description');
|
||||
expect(() => selectMenu().addOptions(option)).not.toThrowError();
|
||||
expect(() => selectMenu().addOptions([option])).not.toThrowError();
|
||||
expect(() => selectMenu().setOptions([option])).not.toThrowError();
|
||||
expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError();
|
||||
expect(() =>
|
||||
selectMenu().addOptions([
|
||||
{
|
||||
label: 'test',
|
||||
value: 'test',
|
||||
emoji: {
|
||||
id: '123',
|
||||
name: 'test',
|
||||
animated: true,
|
||||
},
|
||||
},
|
||||
]),
|
||||
).not.toThrowError();
|
||||
|
||||
const options = new Array<APISelectMenuOption>(25).fill({ label: 'test', value: 'test' });
|
||||
expect(() => selectMenu().addOptions(options)).not.toThrowError();
|
||||
expect(() => selectMenu().setOptions(options)).not.toThrowError();
|
||||
|
||||
expect(() =>
|
||||
selectMenu()
|
||||
.addOptions([{ label: 'test', value: 'test' }])
|
||||
.addOptions(new Array<APISelectMenuOption>(24).fill({ label: 'test', value: 'test' })),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
|
||||
@@ -33,6 +78,26 @@ describe('Button Components', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => selectMenu().setDisabled(0)).toThrowError();
|
||||
expect(() => selectMenu().setPlaceholder(longStr)).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => selectMenu().addOptions([{ label: 'test' }])).toThrowError();
|
||||
expect(() => selectMenu().addOptions([{ label: longStr, value: 'test' }])).toThrowError();
|
||||
expect(() => selectMenu().addOptions([{ value: longStr, label: 'test' }])).toThrowError();
|
||||
expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', description: longStr }])).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', default: 100 }])).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => selectMenu().addOptions([{ value: 'test' }])).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => selectMenu().addOptions([{ default: true }])).toThrowError();
|
||||
|
||||
const tooManyOptions = new Array<APISelectMenuOption>(26).fill({ label: 'test', value: 'test' });
|
||||
expect(() => selectMenu().setOptions(tooManyOptions)).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
selectMenu()
|
||||
.addOptions([{ label: 'test', value: 'test' }])
|
||||
.addOptions(tooManyOptions),
|
||||
).toThrowError();
|
||||
|
||||
expect(() => {
|
||||
selectMenuOption()
|
||||
@@ -47,26 +112,12 @@ describe('Button Components', () => {
|
||||
});
|
||||
|
||||
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
|
||||
const selectMenuOptionData: APISelectMenuOption = {
|
||||
label: 'test',
|
||||
value: 'test',
|
||||
emoji: { name: 'test' },
|
||||
default: true,
|
||||
description: 'test',
|
||||
};
|
||||
|
||||
const selectMenuData: APISelectMenuComponent = {
|
||||
type: ComponentType.SelectMenu,
|
||||
custom_id: 'test',
|
||||
max_values: 10,
|
||||
min_values: 3,
|
||||
disabled: true,
|
||||
options: [selectMenuOptionData],
|
||||
placeholder: 'test',
|
||||
};
|
||||
|
||||
expect(new SelectMenuComponent(selectMenuData).toJSON()).toEqual(selectMenuData);
|
||||
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
|
||||
expect(
|
||||
new SelectMenuBuilder(selectMenuDataWithoutOptions)
|
||||
.addOptions([new SelectMenuOptionBuilder(selectMenuOptionData)])
|
||||
.toJSON(),
|
||||
).toEqual(selectMenuData);
|
||||
expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
126
packages/builders/__tests__/components/textInput.test.ts
Normal file
126
packages/builders/__tests__/components/textInput.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { APITextInputComponent, ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import {
|
||||
labelValidator,
|
||||
maxLengthValidator,
|
||||
minLengthValidator,
|
||||
placeholderValidator,
|
||||
valueValidator,
|
||||
textInputStyleValidator,
|
||||
} from '../../src/components/textInput/Assertions';
|
||||
import { TextInputBuilder } from '../../src/components/textInput/TextInput';
|
||||
|
||||
const superLongStr = 'a'.repeat(5000);
|
||||
|
||||
const textInputComponent = () => new TextInputBuilder();
|
||||
|
||||
describe('Text Input Components', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid label THEN validator does not throw', () => {
|
||||
expect(() => labelValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid label THEN validator does throw', () => {
|
||||
expect(() => labelValidator.parse(24)).toThrowError();
|
||||
expect(() => labelValidator.parse(undefined)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid style THEN validator does not throw', () => {
|
||||
expect(() => textInputStyleValidator.parse(TextInputStyle.Paragraph)).not.toThrowError();
|
||||
expect(() => textInputStyleValidator.parse(TextInputStyle.Short)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid style THEN validator does throw', () => {
|
||||
expect(() => textInputStyleValidator.parse(24)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid min length THEN validator does not throw', () => {
|
||||
expect(() => minLengthValidator.parse(10)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid min length THEN validator does throw', () => {
|
||||
expect(() => minLengthValidator.parse(-1)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid max length THEN validator does not throw', () => {
|
||||
expect(() => maxLengthValidator.parse(10)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid min length THEN validator does throw', () => {
|
||||
expect(() => maxLengthValidator.parse(4001)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid value THEN validator does not throw', () => {
|
||||
expect(() => valueValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid value THEN validator does throw', () => {
|
||||
expect(() => valueValidator.parse(superLongStr)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid placeholder THEN validator does not throw', () => {
|
||||
expect(() => placeholderValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid value THEN validator does throw', () => {
|
||||
expect(() => placeholderValidator.parse(superLongStr)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||
expect(() => {
|
||||
textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON();
|
||||
}).not.toThrowError();
|
||||
|
||||
expect(() => {
|
||||
textInputComponent()
|
||||
.setCustomId('foobar')
|
||||
.setLabel('test')
|
||||
.setMaxLength(100)
|
||||
.setMinLength(1)
|
||||
.setPlaceholder('bar')
|
||||
.setRequired(true)
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.toJSON();
|
||||
}).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN invalid fields THEN builder throws', () => {
|
||||
expect(() => textInputComponent().toJSON()).toThrowError();
|
||||
expect(() => {
|
||||
textInputComponent()
|
||||
.setCustomId('test')
|
||||
.setMaxLength(100)
|
||||
.setPlaceholder('hello')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.toJSON();
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const textInputData: APITextInputComponent = {
|
||||
type: ComponentType.TextInput,
|
||||
label: 'label',
|
||||
custom_id: 'custom id',
|
||||
placeholder: 'placeholder',
|
||||
max_length: 100,
|
||||
min_length: 10,
|
||||
value: 'value',
|
||||
required: false,
|
||||
style: TextInputStyle.Paragraph,
|
||||
};
|
||||
|
||||
expect(new TextInputBuilder(textInputData).toJSON()).toEqual(textInputData);
|
||||
expect(
|
||||
textInputComponent()
|
||||
.setCustomId(textInputData.custom_id)
|
||||
.setLabel(textInputData.label)
|
||||
.setPlaceholder(textInputData.placeholder)
|
||||
.setMaxLength(textInputData.max_length)
|
||||
.setMinLength(textInputData.min_length)
|
||||
.setValue(textInputData.value)
|
||||
.setRequired(textInputData.required)
|
||||
.setStyle(textInputData.style)
|
||||
.toJSON(),
|
||||
).toEqual(textInputData);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PermissionFlagsBits } from 'discord-api-types/v10';
|
||||
import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index';
|
||||
|
||||
const getBuilder = () => new ContextMenuCommandBuilder();
|
||||
@@ -85,5 +86,58 @@ describe('Context Menu Commands', () => {
|
||||
expect(() => getBuilder().setName('foo').setDefaultPermission(false)).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Context menu command localizations', () => {
|
||||
const expectedSingleLocale = { 'en-US': 'foobar' };
|
||||
const expectedMultipleLocales = {
|
||||
...expectedSingleLocale,
|
||||
bg: 'test',
|
||||
};
|
||||
|
||||
test('GIVEN valid name localizations THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
|
||||
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid name localizations THEN does throw error', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid name localizations THEN valid data is stored', () => {
|
||||
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
|
||||
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
|
||||
expectedMultipleLocales,
|
||||
);
|
||||
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
|
||||
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
|
||||
'en-US': null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('permissions', () => {
|
||||
test('GIVEN valid permission string THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid permission bitfield THEN does not throw error', () => {
|
||||
expect(() =>
|
||||
getBuilder().setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN null permissions THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid inputs THEN does throw error', () => {
|
||||
expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError();
|
||||
|
||||
expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
APIApplicationCommandUserOption,
|
||||
ApplicationCommandOptionType,
|
||||
ChannelType,
|
||||
} from 'discord-api-types/v9';
|
||||
} from 'discord-api-types/v10';
|
||||
import {
|
||||
SlashCommandBooleanOption,
|
||||
SlashCommandChannelOption,
|
||||
@@ -29,7 +29,7 @@ const getChannelOption = () =>
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.addChannelType(ChannelType.GuildText);
|
||||
.addChannelTypes(ChannelType.GuildText);
|
||||
|
||||
const getStringOption = () =>
|
||||
new SlashCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
@@ -39,7 +39,7 @@ const getIntegerOption = () =>
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMinValue(-1)
|
||||
.setMaxValue(10);
|
||||
|
||||
const getNumberOption = () =>
|
||||
@@ -47,7 +47,7 @@ const getNumberOption = () =>
|
||||
.setName('owo')
|
||||
.setDescription('Testing 123')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMinValue(-1.23)
|
||||
.setMaxValue(10);
|
||||
|
||||
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||
@@ -84,30 +84,30 @@ describe('Application Command toJSON() results', () => {
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
min_value: -1,
|
||||
});
|
||||
|
||||
expect(getIntegerOption().setAutocomplete(true).setChoices().toJSON()).toEqual<APIApplicationCommandIntegerOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: -1,
|
||||
autocomplete: true,
|
||||
// @ts-expect-error TODO: you *can* send an empty array with autocomplete: true, should correct that in types
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(
|
||||
getIntegerOption().setAutocomplete(true).setChoices([]).toJSON(),
|
||||
getIntegerOption().addChoices({ name: 'uwu', value: 1 }).toJSON(),
|
||||
).toEqual<APIApplicationCommandIntegerOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
autocomplete: true,
|
||||
// @ts-expect-error TODO: you *can* send an empty array with autocomplete: true, should correct that in types
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(getIntegerOption().addChoice('uwu', 1).toJSON()).toEqual<APIApplicationCommandIntegerOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Integer,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
min_value: -1,
|
||||
choices: [{ name: 'uwu', value: 1 }],
|
||||
});
|
||||
});
|
||||
@@ -128,30 +128,32 @@ describe('Application Command toJSON() results', () => {
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
min_value: -1.23,
|
||||
});
|
||||
|
||||
expect(getNumberOption().setAutocomplete(true).setChoices([]).toJSON()).toEqual<APIApplicationCommandNumberOption>({
|
||||
expect(getNumberOption().setAutocomplete(true).setChoices().toJSON()).toEqual<APIApplicationCommandNumberOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
min_value: -1.23,
|
||||
autocomplete: true,
|
||||
// @ts-expect-error TODO: you *can* send an empty array with autocomplete: true, should correct that in types
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(getNumberOption().addChoice('uwu', 1).toJSON()).toEqual<APIApplicationCommandNumberOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: 1,
|
||||
choices: [{ name: 'uwu', value: 1 }],
|
||||
});
|
||||
expect(getNumberOption().addChoices({ name: 'uwu', value: 1 }).toJSON()).toEqual<APIApplicationCommandNumberOption>(
|
||||
{
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.Number,
|
||||
required: true,
|
||||
max_value: 10,
|
||||
min_value: -1.23,
|
||||
choices: [{ name: 'uwu', value: 1 }],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN a role option THEN calling toJSON should return a valid JSON', () => {
|
||||
@@ -171,7 +173,7 @@ describe('Application Command toJSON() results', () => {
|
||||
required: true,
|
||||
});
|
||||
|
||||
expect(getStringOption().setAutocomplete(true).setChoices([]).toJSON()).toEqual<APIApplicationCommandStringOption>({
|
||||
expect(getStringOption().setAutocomplete(true).setChoices().toJSON()).toEqual<APIApplicationCommandStringOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
@@ -181,7 +183,9 @@ describe('Application Command toJSON() results', () => {
|
||||
choices: [],
|
||||
});
|
||||
|
||||
expect(getStringOption().addChoice('uwu', '1').toJSON()).toEqual<APIApplicationCommandStringOption>({
|
||||
expect(
|
||||
getStringOption().addChoices({ name: 'uwu', value: '1' }).toJSON(),
|
||||
).toEqual<APIApplicationCommandStringOption>({
|
||||
name: 'owo',
|
||||
description: 'Testing 123',
|
||||
type: ApplicationCommandOptionType.String,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandOptionChoice, ChannelType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandOptionChoice, ChannelType, PermissionFlagsBits } from 'discord-api-types/v10';
|
||||
import {
|
||||
SlashCommandAssertions,
|
||||
SlashCommandBooleanOption,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SlashCommandMentionableOption,
|
||||
SlashCommandNumberOption,
|
||||
SlashCommandRoleOption,
|
||||
SlashCommandAttachmentOption,
|
||||
SlashCommandStringOption,
|
||||
SlashCommandSubcommandBuilder,
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
@@ -25,6 +26,7 @@ const getBooleanOption = () => new SlashCommandBooleanOption().setName('owo').se
|
||||
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123');
|
||||
const getChannelOption = () => new SlashCommandChannelOption().setName('owo').setDescription('Testing 123');
|
||||
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123');
|
||||
const getAttachmentOption = () => new SlashCommandAttachmentOption().setName('owo').setDescription('Testing 123');
|
||||
const getMentionableOption = () => new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123');
|
||||
const getSubcommandGroup = () => new SlashCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123');
|
||||
const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
|
||||
@@ -85,18 +87,16 @@ describe('Slash Commands', () => {
|
||||
test('GIVEN valid array of options or choices THEN does not throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength([])).not.toThrowError();
|
||||
|
||||
expect(() => SlashCommandAssertions.validateMaxChoicesLength([])).not.toThrowError();
|
||||
expect(() => SlashCommandAssertions.validateChoicesLength(25, [])).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid options or choices THEN throw error', () => {
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength(null)).toThrowError();
|
||||
|
||||
expect(() => SlashCommandAssertions.validateMaxChoicesLength(null)).toThrowError();
|
||||
|
||||
// Given an array that's too big
|
||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength(largeArray)).toThrowError();
|
||||
|
||||
expect(() => SlashCommandAssertions.validateMaxChoicesLength(largeArray)).toThrowError();
|
||||
expect(() => SlashCommandAssertions.validateChoicesLength(1, largeArray)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid required parameters THEN does not throw error', () => {
|
||||
@@ -138,23 +138,23 @@ describe('Slash Commands', () => {
|
||||
integer
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.addChoices([['Very cool', 1_000]]),
|
||||
.addChoices({ name: 'Very cool', value: 1_000 }),
|
||||
)
|
||||
.addNumberOption((number) =>
|
||||
number
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.addChoices([['Very cool', 1.5]]),
|
||||
.addChoices({ name: 'Very cool', value: 1.5 }),
|
||||
)
|
||||
.addStringOption((string) =>
|
||||
string
|
||||
.setName('iscool')
|
||||
.setDescription('Are we cool or what?')
|
||||
.addChoices([
|
||||
['Fancy Pants', 'fp_1'],
|
||||
['Fancy Shoes', 'fs_1'],
|
||||
['The Whole shebang', 'all'],
|
||||
]),
|
||||
.addChoices(
|
||||
{ name: 'Fancy Pants', value: 'fp_1' },
|
||||
{ name: 'Fancy Shoes', value: 'fs_1' },
|
||||
{ name: 'The Whole shebang', value: 'all' },
|
||||
),
|
||||
)
|
||||
.addIntegerOption((integer) =>
|
||||
integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
||||
@@ -177,31 +177,25 @@ describe('Slash Commands', () => {
|
||||
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
// @ts-expect-error Checking if check works JS-side too
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||
getStringOption().setAutocomplete(true).addChoice('Fancy Pants', 'fp_1'),
|
||||
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||
getStringOption()
|
||||
.setAutocomplete(true)
|
||||
// @ts-expect-error Checking if check works JS-side too
|
||||
.addChoices([
|
||||
['Fancy Pants', 'fp_1'],
|
||||
['Fancy Shoes', 'fs_1'],
|
||||
['The Whole shebang', 'all'],
|
||||
]),
|
||||
.addChoices(
|
||||
{ name: 'Fancy Pants', value: 'fp_1' },
|
||||
{ name: 'Fancy Shoes', value: 'fs_1' },
|
||||
{ name: 'The Whole shebang', value: 'all' },
|
||||
),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
// @ts-expect-error Checking if check works JS-side too
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||
getStringOption().addChoice('Fancy Pants', 'fp_1').setAutocomplete(true),
|
||||
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
|
||||
),
|
||||
).toThrowError();
|
||||
|
||||
@@ -229,20 +223,20 @@ describe('Slash Commands', () => {
|
||||
|
||||
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addChannelOption(getChannelOption().addChannelType(ChannelType.GuildText)),
|
||||
getBuilder().addChannelOption(getChannelOption().addChannelTypes(ChannelType.GuildText)),
|
||||
).not.toThrowError();
|
||||
|
||||
expect(() => {
|
||||
getBuilder().addChannelOption(
|
||||
getChannelOption().addChannelTypes([ChannelType.GuildNews, ChannelType.GuildText]),
|
||||
getChannelOption().addChannelTypes(ChannelType.GuildNews, ChannelType.GuildText),
|
||||
);
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => {
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(100))).toThrowError();
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100))).toThrowError();
|
||||
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes([100, 200]))).toThrowError();
|
||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100, 200))).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
|
||||
@@ -286,6 +280,8 @@ describe('Slash Commands', () => {
|
||||
|
||||
expect(() => getBuilder().addRoleOption(getRoleOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addAttachmentOption(getAttachmentOption())).not.toThrowError();
|
||||
|
||||
expect(() => getBuilder().addMentionableOption(getMentionableOption())).not.toThrowError();
|
||||
});
|
||||
|
||||
@@ -331,24 +327,24 @@ describe('Slash Commands', () => {
|
||||
expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an option that is autocompletable and has choices, THEN setting choices to an empty array should not throw an error', () => {
|
||||
test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices([])),
|
||||
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices()),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(
|
||||
getStringOption()
|
||||
.setAutocomplete(true)
|
||||
.setChoices([['owo', 'uwu']]),
|
||||
getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }),
|
||||
),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an option, THEN setting choices should not throw an error', () => {
|
||||
expect(() => getBuilder().addStringOption(getStringOption().setChoices([['owo', 'uwu']]))).not.toThrowError();
|
||||
expect(() =>
|
||||
getBuilder().addStringOption(getStringOption().setChoices({ name: 'owo', value: 'uwu' })),
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -424,5 +420,83 @@ describe('Slash Commands', () => {
|
||||
expect(() => getSubcommand().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slash command localizations', () => {
|
||||
const expectedSingleLocale = { 'en-US': 'foobar' };
|
||||
const expectedMultipleLocales = {
|
||||
...expectedSingleLocale,
|
||||
bg: 'test',
|
||||
};
|
||||
|
||||
test('GIVEN valid name localizations THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
|
||||
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid name localizations THEN does throw error', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid name localizations THEN valid data is stored', () => {
|
||||
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
|
||||
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
|
||||
expectedMultipleLocales,
|
||||
);
|
||||
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
|
||||
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
|
||||
'en-US': null,
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN valid description localizations THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setDescriptionLocalization('en-US', 'foobar')).not.toThrowError();
|
||||
expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid description localizations THEN does throw error', () => {
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => getBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' })).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid description localizations THEN valid data is stored', () => {
|
||||
expect(getBuilder().setDescriptionLocalization('en-US', 'foobar').description_localizations).toEqual(
|
||||
expectedSingleLocale,
|
||||
);
|
||||
expect(
|
||||
getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).description_localizations,
|
||||
).toEqual(expectedMultipleLocales);
|
||||
expect(getBuilder().setDescriptionLocalizations(null).description_localizations).toBeNull();
|
||||
expect(getBuilder().setDescriptionLocalization('en-US', null).description_localizations).toEqual({
|
||||
'en-US': null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('permissions', () => {
|
||||
test('GIVEN valid permission string THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid permission bitfield THEN does not throw error', () => {
|
||||
expect(() =>
|
||||
getBuilder().setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN null permissions THEN does not throw error', () => {
|
||||
expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid inputs THEN does throw error', () => {
|
||||
expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError();
|
||||
|
||||
expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
94
packages/builders/__tests__/interactions/modal.test.ts
Normal file
94
packages/builders/__tests__/interactions/modal.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { APIModalInteractionResponseCallbackData, ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ModalBuilder,
|
||||
ModalActionRowComponentBuilder,
|
||||
TextInputBuilder,
|
||||
} from '../../src';
|
||||
import {
|
||||
componentsValidator,
|
||||
titleValidator,
|
||||
validateRequiredParameters,
|
||||
} from '../../src/interactions/modals/Assertions';
|
||||
|
||||
const modal = () => new ModalBuilder();
|
||||
|
||||
describe('Modals', () => {
|
||||
describe('Assertion Tests', () => {
|
||||
test('GIVEN valid title THEN validator does not throw', () => {
|
||||
expect(() => titleValidator.parse('foobar')).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid title THEN validator does throw', () => {
|
||||
expect(() => titleValidator.parse(42)).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid components THEN validator does not throw', () => {
|
||||
expect(() => componentsValidator.parse([new ActionRowBuilder(), new ActionRowBuilder()])).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid components THEN validator does throw', () => {
|
||||
expect(() => componentsValidator.parse([new ButtonBuilder(), new TextInputBuilder()])).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid required parameters THEN validator does not throw', () => {
|
||||
expect(() =>
|
||||
validateRequiredParameters('123', 'title', [new ActionRowBuilder(), new ActionRowBuilder()]),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid required parameters THEN validator does throw', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error
|
||||
validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]),
|
||||
).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||
expect(() =>
|
||||
modal().setTitle('test').setCustomId('foobar').setComponents([new ActionRowBuilder()]),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN invalid fields THEN builder does throw', () => {
|
||||
expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN valid input THEN valid JSON outputs are given', () => {
|
||||
const modalData: APIModalInteractionResponseCallbackData = {
|
||||
title: 'title',
|
||||
custom_id: 'custom id',
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.TextInput,
|
||||
label: 'label',
|
||||
style: TextInputStyle.Paragraph,
|
||||
custom_id: 'custom id',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(new ModalBuilder(modalData).toJSON()).toEqual(modalData);
|
||||
|
||||
expect(
|
||||
modal()
|
||||
.setTitle(modalData.title)
|
||||
.setCustomId('custom id')
|
||||
.setComponents([
|
||||
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents([
|
||||
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
|
||||
]),
|
||||
])
|
||||
.toJSON(),
|
||||
).toEqual(modalData);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,11 @@
|
||||
import { Embed } from '../../src';
|
||||
import type { APIEmbed } from 'discord-api-types/v9';
|
||||
|
||||
const emptyEmbed: APIEmbed = {
|
||||
author: undefined,
|
||||
color: undefined,
|
||||
description: undefined,
|
||||
fields: [],
|
||||
footer: undefined,
|
||||
image: undefined,
|
||||
provider: undefined,
|
||||
thumbnail: undefined,
|
||||
title: undefined,
|
||||
url: undefined,
|
||||
video: undefined,
|
||||
};
|
||||
import { EmbedBuilder, embedLength } from '../../src';
|
||||
|
||||
const alpha = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
describe('Embed', () => {
|
||||
describe('Embed getters', () => {
|
||||
test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => {
|
||||
const embed = new Embed({
|
||||
const embed = new EmbedBuilder({
|
||||
title: alpha,
|
||||
description: alpha,
|
||||
fields: [{ name: alpha, value: alpha }],
|
||||
@@ -28,38 +13,38 @@ describe('Embed', () => {
|
||||
footer: { text: alpha },
|
||||
});
|
||||
|
||||
expect(embed.length).toBe(alpha.length * 6);
|
||||
expect(embedLength(embed.data)).toEqual(alpha.length * 6);
|
||||
});
|
||||
|
||||
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(embed.length).toBe(0);
|
||||
expect(embedLength(embed.data)).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed title', () => {
|
||||
test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ title: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: 'foo' });
|
||||
const embed = new EmbedBuilder({ title: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTitle('foo');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ title: 'foo' });
|
||||
const embed = new EmbedBuilder({ title: 'foo' });
|
||||
embed.setTitle(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ title: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid title THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setTitle('a'.repeat(257))).toThrowError();
|
||||
});
|
||||
@@ -67,26 +52,26 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed description', () => {
|
||||
test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ ...emptyEmbed, description: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: 'foo' });
|
||||
const embed = new EmbedBuilder({ description: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setDescription('foo');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: 'foo' });
|
||||
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ description: 'foo' });
|
||||
const embed = new EmbedBuilder({ description: 'foo' });
|
||||
embed.setDescription(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ description: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid description THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setDescription('a'.repeat(4097))).toThrowError();
|
||||
});
|
||||
@@ -94,62 +79,61 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed URL', () => {
|
||||
test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ url: 'https://discord.js.org/' });
|
||||
const embed = new EmbedBuilder({ url: 'https://discord.js.org/' });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
url: 'https://discord.js.org/',
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setURL('https://discord.js.org/');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
url: 'https://discord.js.org/',
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ url: 'https://discord.js.org' });
|
||||
const embed = new EmbedBuilder({ url: 'https://discord.js.org' });
|
||||
embed.setURL(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ url: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid URL THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setURL('owo')).toThrowError();
|
||||
expect(() => embed.setURL(input)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Color', () => {
|
||||
test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ color: 0xff0000 });
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: 0xff0000 });
|
||||
const embed = new EmbedBuilder({ color: 0xff0000 });
|
||||
expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
embed.setColor(0xff0000);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: 0xff0000 });
|
||||
expect(new EmbedBuilder().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
|
||||
expect(new EmbedBuilder().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ color: 0xff0000 });
|
||||
const embed = new EmbedBuilder({ color: 0xff0000 });
|
||||
embed.setColor(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ color: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid color THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
// @ts-expect-error
|
||||
expect(() => embed.setColor('RED')).toThrowError();
|
||||
// @ts-expect-error
|
||||
expect(() => embed.setColor([42, 36])).toThrowError();
|
||||
expect(() => embed.setColor([42, 36, 1000])).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,67 +141,65 @@ describe('Embed', () => {
|
||||
const now = new Date();
|
||||
|
||||
test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ timestamp: now.toISOString() });
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
|
||||
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
||||
});
|
||||
|
||||
test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTimestamp(now);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTimestamp(now.getTime());
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTimestamp();
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: embed.timestamp });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: embed.data.timestamp });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ timestamp: now.toISOString() });
|
||||
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
|
||||
embed.setTimestamp(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: undefined });
|
||||
expect(embed.toJSON()).toStrictEqual({ timestamp: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Embed Thumbnail', () => {
|
||||
test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setThumbnail('https://discord.js.org/static/logo.svg');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
embed.setThumbnail(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setThumbnail('owo')).toThrowError();
|
||||
});
|
||||
@@ -225,32 +207,30 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed Image', () => {
|
||||
test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({ image: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
image: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setImage('https://discord.js.org/static/logo.svg');
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
image: { url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ image: { url: 'https://discord.js/org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } });
|
||||
embed.setImage(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ image: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid image THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setImage('owo')).toThrowError();
|
||||
});
|
||||
@@ -258,17 +238,16 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed Author', () => {
|
||||
test('GIVEN an embed with a pre-defined author THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({
|
||||
const embed = new EmbedBuilder({
|
||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||
});
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setAuthor({
|
||||
name: 'Wumpus',
|
||||
iconURL: 'https://discord.js.org/static/logo.svg',
|
||||
@@ -276,22 +255,21 @@ describe('Embed', () => {
|
||||
});
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({
|
||||
const embed = new EmbedBuilder({
|
||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||
});
|
||||
embed.setAuthor(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ author: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with an invalid author name THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError();
|
||||
});
|
||||
@@ -299,34 +277,34 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed Footer', () => {
|
||||
test('GIVEN an embed with a pre-defined footer THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({
|
||||
const embed = new EmbedBuilder({
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' });
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => {
|
||||
const embed = new Embed({ footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' } });
|
||||
const embed = new EmbedBuilder({
|
||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||
});
|
||||
embed.setFooter(null);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed });
|
||||
expect(embed.toJSON()).toStrictEqual({ footer: undefined });
|
||||
});
|
||||
|
||||
test('GIVEN an embed with invalid footer text THEN throws error', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setFooter({ text: 'a'.repeat(2049) })).toThrowError();
|
||||
});
|
||||
@@ -334,48 +312,38 @@ describe('Embed', () => {
|
||||
|
||||
describe('Embed Fields', () => {
|
||||
test('GIVEN an embed with a pre-defined field THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed({
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
const embed = new EmbedBuilder({
|
||||
fields: [{ name: 'foo', value: 'bar' }],
|
||||
});
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#addField THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
embed.addField({ name: 'foo', value: 'bar' });
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
fields: [{ name: 'foo', value: 'bar' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
embed.addFields({ name: 'foo', value: 'bar' });
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields([{ name: 'foo', value: 'bar' }]);
|
||||
|
||||
expect(embed.toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
|
||||
fields: [{ name: 'foo', value: 'bar' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
|
||||
const embed = new Embed();
|
||||
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields([
|
||||
{ name: 'foo', value: 'bar' },
|
||||
{ name: 'foo', value: 'baz' },
|
||||
]);
|
||||
|
||||
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
|
||||
...emptyEmbed,
|
||||
fields: [{ name: 'foo', value: 'baz', inline: undefined }],
|
||||
fields: [{ name: 'foo', value: 'baz' }],
|
||||
});
|
||||
});
|
||||
|
||||
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' })));
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields(Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
||||
|
||||
expect(() =>
|
||||
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
@@ -383,45 +351,57 @@ describe('Embed', () => {
|
||||
});
|
||||
|
||||
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' })));
|
||||
const embed = new EmbedBuilder();
|
||||
embed.addFields(Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
||||
|
||||
expect(() =>
|
||||
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() =>
|
||||
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => {
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError();
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field amount THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() =>
|
||||
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
|
||||
).toThrowError();
|
||||
expect(() => embed.addFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field name THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
|
||||
expect(() => embed.addFields([{ name: '', value: 'bar' }])).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field name length THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
|
||||
expect(() => embed.addFields([{ name: 'a'.repeat(257), value: 'bar' }])).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIVEN invalid field value length THEN throws error', () => {
|
||||
test('', () => {
|
||||
const embed = new Embed();
|
||||
const embed = new EmbedBuilder();
|
||||
|
||||
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError();
|
||||
expect(() => embed.addFields([{ name: '', value: 'a'.repeat(1025) }])).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
hyperlink,
|
||||
inlineCode,
|
||||
italic,
|
||||
memberNicknameMention,
|
||||
quote,
|
||||
roleMention,
|
||||
spoiler,
|
||||
@@ -23,75 +22,75 @@ import {
|
||||
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```');
|
||||
expect<'```\ndiscord.js```'>(codeBlock('discord.js')).toEqual('```\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```');
|
||||
expect<'```js\ndiscord.js```'>(codeBlock('js', 'discord.js')).toEqual('```js\ndiscord.js```');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inlineCode', () => {
|
||||
test('GIVEN "discord.js" THEN returns "`discord.js`"', () => {
|
||||
expect<'`discord.js`'>(inlineCode('discord.js')).toBe('`discord.js`');
|
||||
expect<'`discord.js`'>(inlineCode('discord.js')).toEqual('`discord.js`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('italic', () => {
|
||||
test('GIVEN "discord.js" THEN returns "_discord.js_"', () => {
|
||||
expect<'_discord.js_'>(italic('discord.js')).toBe('_discord.js_');
|
||||
expect<'_discord.js_'>(italic('discord.js')).toEqual('_discord.js_');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bold', () => {
|
||||
test('GIVEN "discord.js" THEN returns "**discord.js**"', () => {
|
||||
expect<'**discord.js**'>(bold('discord.js')).toBe('**discord.js**');
|
||||
expect<'**discord.js**'>(bold('discord.js')).toEqual('**discord.js**');
|
||||
});
|
||||
});
|
||||
|
||||
describe('underscore', () => {
|
||||
test('GIVEN "discord.js" THEN returns "__discord.js__"', () => {
|
||||
expect<'__discord.js__'>(underscore('discord.js')).toBe('__discord.js__');
|
||||
expect<'__discord.js__'>(underscore('discord.js')).toEqual('__discord.js__');
|
||||
});
|
||||
});
|
||||
|
||||
describe('strikethrough', () => {
|
||||
test('GIVEN "discord.js" THEN returns "~~discord.js~~"', () => {
|
||||
expect<'~~discord.js~~'>(strikethrough('discord.js')).toBe('~~discord.js~~');
|
||||
expect<'~~discord.js~~'>(strikethrough('discord.js')).toEqual('~~discord.js~~');
|
||||
});
|
||||
});
|
||||
|
||||
describe('quote', () => {
|
||||
test('GIVEN "discord.js" THEN returns "> discord.js"', () => {
|
||||
expect<'> discord.js'>(quote('discord.js')).toBe('> discord.js');
|
||||
expect<'> discord.js'>(quote('discord.js')).toEqual('> discord.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('blockQuote', () => {
|
||||
test('GIVEN "discord.js" THEN returns ">>> discord.js"', () => {
|
||||
expect<'>>> discord.js'>(blockQuote('discord.js')).toBe('>>> discord.js');
|
||||
expect<'>>> discord.js'>(blockQuote('discord.js')).toEqual('>>> 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>');
|
||||
expect<'<https://discord.js.org>'>(hideLinkEmbed('https://discord.js.org')).toEqual('<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/>');
|
||||
expect<`<${string}>`>(hideLinkEmbed(new URL('https://discord.js.org/'))).toEqual('<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(
|
||||
expect<'[discord.js](https://discord.js.org)'>(hyperlink('discord.js', 'https://discord.js.org')).toEqual(
|
||||
'[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(
|
||||
expect<`[discord.js](${string})`>(hyperlink('discord.js', new URL('https://discord.js.org'))).toEqual(
|
||||
'[discord.js](https://discord.js.org/)',
|
||||
);
|
||||
});
|
||||
@@ -99,59 +98,53 @@ describe('Message formatters', () => {
|
||||
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")');
|
||||
).toEqual('[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")');
|
||||
).toEqual('[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||');
|
||||
expect<'||discord.js||'>(spoiler('discord.js')).toEqual('||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>');
|
||||
expect(userMention('139836912335716352')).toEqual('<@139836912335716352>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('channelMention', () => {
|
||||
test('GIVEN channelId THEN returns "<#[channelId]>"', () => {
|
||||
expect(channelMention('829924760309334087')).toBe('<#829924760309334087>');
|
||||
expect(channelMention('829924760309334087')).toEqual('<#829924760309334087>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('roleMention', () => {
|
||||
test('GIVEN roleId THEN returns "<&[roleId]>"', () => {
|
||||
expect(roleMention('815434166602170409')).toBe('<@&815434166602170409>');
|
||||
expect(roleMention('815434166602170409')).toEqual('<@&815434166602170409>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatEmoji', () => {
|
||||
test('GIVEN static emojiId THEN returns "<:_:${emojiId}>"', () => {
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji('851461487498493952')).toBe('<:_:851461487498493952>');
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji('851461487498493952')).toEqual('<:_:851461487498493952>');
|
||||
});
|
||||
|
||||
test('GIVEN static emojiId WITH animated explicitly false THEN returns "<:_:[emojiId]>"', () => {
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji('851461487498493952', false)).toBe('<:_:851461487498493952>');
|
||||
expect<`<:_:851461487498493952>`>(formatEmoji('851461487498493952', false)).toEqual('<:_:851461487498493952>');
|
||||
});
|
||||
|
||||
test('GIVEN animated emojiId THEN returns "<a:_:${emojiId}>"', () => {
|
||||
expect<`<a:_:827220205352255549>`>(formatEmoji('827220205352255549', true)).toBe('<a:_:827220205352255549>');
|
||||
expect<`<a:_:827220205352255549>`>(formatEmoji('827220205352255549', true)).toEqual('<a:_:827220205352255549>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,47 +153,49 @@ describe('Message formatters', () => {
|
||||
jest.useFakeTimers('modern');
|
||||
jest.setSystemTime(1566424897579);
|
||||
|
||||
expect<`<t:${bigint}>`>(time()).toBe('<t:1566424897>');
|
||||
expect<`<t:${bigint}>`>(time()).toEqual('<t:1566424897>');
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('GIVEN a date THEN returns "<t:${bigint}>"', () => {
|
||||
expect<`<t:${bigint}>`>(time(new Date(1867424897579))).toBe('<t:1867424897>');
|
||||
expect<`<t:${bigint}>`>(time(new Date(1867424897579))).toEqual('<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>');
|
||||
expect<`<t:${bigint}:d>`>(time(new Date(1867424897579), 'd')).toEqual('<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>');
|
||||
expect<`<t:${bigint}:R>`>(time(new Date(1867424897579), TimestampStyles.RelativeTime)).toEqual(
|
||||
'<t:1867424897:R>',
|
||||
);
|
||||
});
|
||||
|
||||
test('GIVEN a date THEN returns "<t:${time}>"', () => {
|
||||
expect<'<t:1867424897>'>(time(1867424897)).toBe('<t:1867424897>');
|
||||
expect<'<t:1867424897>'>(time(1867424897)).toEqual('<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>');
|
||||
expect<'<t:1867424897:d>'>(time(1867424897, 'd')).toEqual('<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>');
|
||||
expect<'<t:1867424897:R>'>(time(1867424897, TimestampStyles.RelativeTime)).toEqual('<t:1867424897:R>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Faces', () => {
|
||||
test('GIVEN Faces.Shrug THEN returns "¯\\_(ツ)\\_/¯"', () => {
|
||||
expect<'¯\\_(ツ)\\_/¯'>(Faces.Shrug).toBe('¯\\_(ツ)\\_/¯');
|
||||
expect<'¯\\_(ツ)\\_/¯'>(Faces.Shrug).toEqual('¯\\_(ツ)\\_/¯');
|
||||
});
|
||||
|
||||
test('GIVEN Faces.Tableflip THEN returns "(╯°□°)╯︵ ┻━┻"', () => {
|
||||
expect<'(╯°□°)╯︵ ┻━┻'>(Faces.Tableflip).toBe('(╯°□°)╯︵ ┻━┻');
|
||||
expect<'(╯°□°)╯︵ ┻━┻'>(Faces.Tableflip).toEqual('(╯°□°)╯︵ ┻━┻');
|
||||
});
|
||||
|
||||
test('GIVEN Faces.Unflip THEN returns "┬─┬ ノ( ゜-゜ノ)"', () => {
|
||||
expect<'┬─┬ ノ( ゜-゜ノ)'>(Faces.Unflip).toBe('┬─┬ ノ( ゜-゜ノ)');
|
||||
expect<'┬─┬ ノ( ゜-゜ノ)'>(Faces.Unflip).toEqual('┬─┬ ノ( ゜-゜ノ)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,29 +6,32 @@ 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") }})
|
||||
# [{{ 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]
|
||||
# [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 %}
|
||||
## {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% 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 }}))\
|
||||
{% if commit.breaking %}\
|
||||
\n\n {% raw %} {% endraw %} ### Breaking Changes:\n \
|
||||
{% for breakingChange in commit.footers %}\
|
||||
{% raw %} {% endraw %} - {{ breakingChange }}\n\
|
||||
{% endfor %}\
|
||||
{% endif %}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
trim = true
|
||||
@@ -38,25 +41,24 @@ footer = ""
|
||||
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"},
|
||||
{ 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"
|
||||
tag_pattern = "@discordjs/builders@[0-9]*"
|
||||
ignore_tags = ""
|
||||
topo_order = false
|
||||
sort_commits = "newest"
|
||||
|
||||
99
packages/builders/docs/examples/Slash Command Builders.md
Normal file
99
packages/builders/docs/examples/Slash Command Builders.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Slash Command Builders
|
||||
|
||||
## Ping command
|
||||
|
||||
```ts
|
||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||
|
||||
// Create a slash command builder
|
||||
const pingCommand = new SlashCommandBuilder().setName('ping').setDescription('Check if this interaction is responsive');
|
||||
|
||||
// Get the raw data that can be sent to Discord
|
||||
const rawData = pingCommand.toJSON();
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
```ts
|
||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||
|
||||
// Creates a boop command
|
||||
const boopCommand = new SlashCommandBuilder()
|
||||
.setName('boop')
|
||||
.setDescription('Boops the specified user, as many times as you want')
|
||||
.addUserOption((option) => option.setName('user').setDescription('The user to boop').setRequired(true))
|
||||
|
||||
// Adds an integer option
|
||||
.addIntegerOption((option) =>
|
||||
option.setName('boop_amount').setDescription('How many times should the user be booped (defaults to 1)'),
|
||||
)
|
||||
|
||||
// Supports choices too!
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('boop_reminder')
|
||||
.setDescription('How often should we remind you to boop the user')
|
||||
.addChoices({ name: 'Every day', value: 1 }, { name: 'Weekly', value: 7 }),
|
||||
);
|
||||
|
||||
// Get the final raw data that can be sent to Discord
|
||||
const rawData = boopCommand.toJSON();
|
||||
```
|
||||
|
||||
## Subcommands and subcommand groups
|
||||
|
||||
```ts
|
||||
import { SlashCommandBuilder } from '@discordjs/builders';
|
||||
|
||||
const pointsCommand = new SlashCommandBuilder()
|
||||
.setName('points')
|
||||
.setDescription('Lists or manages user points')
|
||||
|
||||
// Add a manage group
|
||||
.addSubcommandGroup((group) =>
|
||||
group
|
||||
.setName('manage')
|
||||
.setDescription('Shows or manages points in the server')
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName('user_points')
|
||||
.setDescription("Alters a user's points")
|
||||
.addUserOption((option) =>
|
||||
option.setName('user').setDescription('The user whose points to alter').setRequired(true),
|
||||
)
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName('action')
|
||||
.setDescription('What action should be taken with the users points?')
|
||||
.addChoices(
|
||||
{ name: 'Add points', value: 'add' },
|
||||
{ name: 'Remove points', value: 'remove' },
|
||||
{ name: 'Reset points', value: 'reset' },
|
||||
)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addIntegerOption((option) => option.setName('points').setDescription('Points to add or remove')),
|
||||
),
|
||||
)
|
||||
|
||||
// Add an information group
|
||||
.addSubcommandGroup((group) =>
|
||||
group
|
||||
.setName('info')
|
||||
.setDescription('Shows information about points in the guild')
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand.setName('total').setDescription('Tells you the total amount of points given in the guild'),
|
||||
)
|
||||
.addSubcommand((subcommand) =>
|
||||
subcommand
|
||||
.setName('user')
|
||||
.setDescription("Lists a user's points")
|
||||
.addUserOption((option) =>
|
||||
option.setName('user').setDescription('The user whose points to list').setRequired(true),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Get the final raw data that can be sent to Discord
|
||||
const rawData = pointsCommand.toJSON();
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@discordjs/builders",
|
||||
"version": "0.12.0",
|
||||
"version": "0.14.0",
|
||||
"description": "A set of builders that you can use when creating your bot",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
@@ -9,14 +9,15 @@
|
||||
"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/*'"
|
||||
"changelog": "git cliff --prepend ./CHANGELOG.md -l -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"
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "src",
|
||||
@@ -51,33 +52,33 @@
|
||||
},
|
||||
"homepage": "https://discord.js.org",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.3.0",
|
||||
"discord-api-types": "^0.26.1",
|
||||
"ts-mixer": "^6.0.0",
|
||||
"tslib": "^2.3.1",
|
||||
"zod": "^3.11.6"
|
||||
"@sapphire/shapeshift": "^3.1.0",
|
||||
"@sindresorhus/is": "^4.6.0",
|
||||
"discord-api-types": "^0.33.3",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"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/core": "^7.18.2",
|
||||
"@babel/plugin-proposal-decorators": "^7.18.2",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@discordjs/ts-docgen": "^0.4.1",
|
||||
"@types/jest": "^28.1.0",
|
||||
"@types/node": "^16.11.38",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"babel-plugin-transform-typescript-metadata": "^0.3.2",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-marine": "^9.3.2",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.4.7",
|
||||
"prettier": "^2.5.1",
|
||||
"standard-version": "^9.3.2",
|
||||
"tsup": "^5.11.11",
|
||||
"typedoc": "^0.22.11",
|
||||
"typescript": "^4.5.5"
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-marine": "^9.4.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"jest": "^28.1.0",
|
||||
"prettier": "^2.6.2",
|
||||
"tsup": "^6.0.1",
|
||||
"typedoc": "^0.22.17",
|
||||
"typescript": "^4.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { APIActionRowComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import type { ButtonComponent, SelectMenuComponent } from '..';
|
||||
import type { Component } from './Component';
|
||||
import { createComponent } from './Components';
|
||||
import {
|
||||
type APIActionRowComponent,
|
||||
ComponentType,
|
||||
APIMessageActionRowComponent,
|
||||
APIModalActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from './Component';
|
||||
import { createComponentBuilder } from './Components';
|
||||
import type { ButtonBuilder, SelectMenuBuilder, TextInputBuilder } from '..';
|
||||
|
||||
export type ActionRowComponent = ButtonComponent | SelectMenuComponent;
|
||||
|
||||
// TODO: Add valid form component types
|
||||
export type MessageComponentBuilder =
|
||||
| MessageActionRowComponentBuilder
|
||||
| ActionRowBuilder<MessageActionRowComponentBuilder>;
|
||||
export type ModalComponentBuilder = ModalActionRowComponentBuilder | ActionRowBuilder<ModalActionRowComponentBuilder>;
|
||||
export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder;
|
||||
export type ModalActionRowComponentBuilder = TextInputBuilder;
|
||||
export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
|
||||
|
||||
/**
|
||||
* Represents an action row component
|
||||
*/
|
||||
export class ActionRow<T extends ActionRowComponent> implements Component {
|
||||
public readonly components: T[] = [];
|
||||
public readonly type = ComponentType.ActionRow;
|
||||
export class ActionRowBuilder<T extends AnyComponentBuilder> extends ComponentBuilder<
|
||||
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
||||
> {
|
||||
/**
|
||||
* The components within this action row
|
||||
*/
|
||||
public readonly components: T[];
|
||||
|
||||
public constructor(data?: APIActionRowComponent) {
|
||||
this.components = (data?.components.map(createComponent) ?? []) as T[];
|
||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
||||
super({ type: ComponentType.ActionRow, ...data });
|
||||
this.components = (components?.map((c) => createComponentBuilder(c)) ?? []) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,7 +38,7 @@ export class ActionRow<T extends ActionRowComponent> implements Component {
|
||||
* @param components The components to add to this action row.
|
||||
* @returns
|
||||
*/
|
||||
public addComponents(...components: T[]) {
|
||||
public addComponents(components: T[]) {
|
||||
this.components.push(...components);
|
||||
return this;
|
||||
}
|
||||
@@ -33,14 +48,15 @@ export class ActionRow<T extends ActionRowComponent> implements Component {
|
||||
* @param components The components to set this row to
|
||||
*/
|
||||
public setComponents(components: T[]) {
|
||||
Reflect.set(this, 'components', [...components]);
|
||||
this.components.splice(0, this.components.length, ...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIActionRowComponent {
|
||||
public toJSON(): APIActionRowComponent<ReturnType<T['toJSON']>> {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this,
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
};
|
||||
} as APIActionRowComponent<ReturnType<T['toJSON']>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,58 @@
|
||||
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import type { SelectMenuOption } from './selectMenu/SelectMenuOption';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v10';
|
||||
import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './selectMenu/UnsafeSelectMenuOption';
|
||||
|
||||
export const customIdValidator = z.string().min(1).max(100);
|
||||
export const customIdValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
|
||||
|
||||
export const emojiValidator = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
animated: z.boolean(),
|
||||
})
|
||||
.partial()
|
||||
.strict();
|
||||
export const emojiValidator = s.object({
|
||||
id: s.string,
|
||||
name: s.string,
|
||||
animated: s.boolean,
|
||||
}).partial.strict;
|
||||
|
||||
export const disabledValidator = z.boolean();
|
||||
export const disabledValidator = s.boolean;
|
||||
|
||||
export const buttonLabelValidator = z.string().nonempty().max(80);
|
||||
export const buttonLabelValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(80);
|
||||
|
||||
export const buttonStyleValidator = z.number().int().min(ButtonStyle.Primary).max(ButtonStyle.Link);
|
||||
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
|
||||
|
||||
export const placeholderValidator = z.string().max(100);
|
||||
export const minMaxValidator = z.number().int().min(0).max(25);
|
||||
export const placeholderValidator = s.string.lengthLessThanOrEqual(150);
|
||||
export const minMaxValidator = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(25);
|
||||
|
||||
export const optionsValidator = z.object({}).array().nonempty();
|
||||
export const labelValueDescriptionValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
|
||||
export const optionValidator = s.union(
|
||||
s.object({
|
||||
label: labelValueDescriptionValidator,
|
||||
value: labelValueDescriptionValidator,
|
||||
description: labelValueDescriptionValidator.optional,
|
||||
emoji: emojiValidator.optional,
|
||||
default: s.boolean.optional,
|
||||
}),
|
||||
s.instance(UnsafeSelectMenuOptionBuilder),
|
||||
);
|
||||
export const optionsValidator = optionValidator.array.lengthGreaterThanOrEqual(0);
|
||||
export const optionsLengthValidator = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(25);
|
||||
|
||||
export function validateRequiredSelectMenuParameters(options: SelectMenuOption[], customId?: string) {
|
||||
export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
optionsValidator.parse(options);
|
||||
}
|
||||
|
||||
export const labelValueValidator = z.string().min(1).max(100);
|
||||
export const defaultValidator = z.boolean();
|
||||
export const labelValueValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
|
||||
export const defaultValidator = s.boolean;
|
||||
|
||||
export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {
|
||||
labelValueValidator.parse(label);
|
||||
labelValueValidator.parse(value);
|
||||
}
|
||||
|
||||
export const urlValidator = z.string().url();
|
||||
export const urlValidator = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:', 'discord:'],
|
||||
});
|
||||
|
||||
export function validateRequiredButtonParameters(
|
||||
style: ButtonStyle,
|
||||
style?: ButtonStyle,
|
||||
label?: string,
|
||||
emoji?: APIMessageComponentEmoji,
|
||||
customId?: string,
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { APIButtonComponent, APIMessageComponentEmoji, ButtonStyle, ComponentType } from 'discord-api-types/v9';
|
||||
import {
|
||||
buttonLabelValidator,
|
||||
buttonStyleValidator,
|
||||
customIdValidator,
|
||||
disabledValidator,
|
||||
emojiValidator,
|
||||
urlValidator,
|
||||
validateRequiredButtonParameters,
|
||||
} from './Assertions';
|
||||
import type { Component } from './Component';
|
||||
|
||||
export class ButtonComponent implements Component {
|
||||
public readonly type = ComponentType.Button as const;
|
||||
public readonly style!: ButtonStyle;
|
||||
public readonly label?: string;
|
||||
public readonly emoji?: APIMessageComponentEmoji;
|
||||
public readonly disabled?: boolean;
|
||||
public readonly custom_id!: string;
|
||||
public readonly url!: string;
|
||||
|
||||
public constructor(data?: APIButtonComponent) {
|
||||
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.style = data?.style as ButtonStyle;
|
||||
this.label = data?.label;
|
||||
this.emoji = data?.emoji;
|
||||
this.disabled = data?.disabled;
|
||||
|
||||
// This if/else makes typescript happy
|
||||
if (data?.style === ButtonStyle.Link) {
|
||||
this.url = data.url;
|
||||
} else {
|
||||
this.custom_id = data?.custom_id as string;
|
||||
}
|
||||
|
||||
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style of this button
|
||||
* @param style The style of the button
|
||||
*/
|
||||
public setStyle(style: ButtonStyle) {
|
||||
buttonStyleValidator.parse(style);
|
||||
Reflect.set(this, 'style', style);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL for this button
|
||||
* @param url The URL to open when this button is clicked
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
urlValidator.parse(url);
|
||||
Reflect.set(this, 'url', url);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom Id for this button
|
||||
* @param customId The custom ID to use for this button
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
customIdValidator.parse(customId);
|
||||
Reflect.set(this, 'custom_id', customId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this button
|
||||
* @param emoji The emoji to display on this button
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
emojiValidator.parse(emoji);
|
||||
Reflect.set(this, 'emoji', emoji);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this button is disable or not
|
||||
* @param disabled Whether or not to disable this button or not
|
||||
*/
|
||||
public setDisabled(disabled: boolean) {
|
||||
disabledValidator.parse(disabled);
|
||||
Reflect.set(this, 'disabled', disabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this button
|
||||
* @param label The label to display on this button
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
buttonLabelValidator.parse(label);
|
||||
Reflect.set(this, 'label', label);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIButtonComponent {
|
||||
validateRequiredButtonParameters(this.style, this.label, this.emoji, this.custom_id, this.url);
|
||||
return {
|
||||
...this,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,28 @@
|
||||
import type { APIMessageComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIActionRowComponentTypes,
|
||||
APIBaseComponent,
|
||||
ComponentType,
|
||||
} from 'discord-api-types/v10';
|
||||
import type { JSONEncodable } from '../util/jsonEncodable';
|
||||
|
||||
export type AnyAPIActionRowComponent = APIActionRowComponentTypes | APIActionRowComponent<APIActionRowComponentTypes>;
|
||||
|
||||
/**
|
||||
* Represents a discord component
|
||||
*/
|
||||
export interface Component {
|
||||
export abstract class ComponentBuilder<
|
||||
DataType extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>,
|
||||
> implements JSONEncodable<AnyAPIActionRowComponent>
|
||||
{
|
||||
/**
|
||||
* The type of this component
|
||||
* The API data associated with this component
|
||||
*/
|
||||
readonly type: ComponentType;
|
||||
/**
|
||||
* Converts this component to an API-compatible JSON object
|
||||
*/
|
||||
toJSON: () => APIMessageComponent;
|
||||
public readonly data: Partial<DataType>;
|
||||
|
||||
public abstract toJSON(): AnyAPIActionRowComponent;
|
||||
|
||||
public constructor(data: Partial<DataType>) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
import { APIMessageComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import { ActionRow, ButtonComponent, Component, SelectMenuComponent } from '../index';
|
||||
import type { ActionRowComponent } from './ActionRow';
|
||||
import { APIMessageComponent, APIModalComponent, ComponentType } from 'discord-api-types/v10';
|
||||
import type { AnyComponentBuilder, MessageComponentBuilder, ModalComponentBuilder } from './ActionRow';
|
||||
import { ActionRowBuilder, ButtonBuilder, ComponentBuilder, SelectMenuBuilder, TextInputBuilder } from '../index';
|
||||
|
||||
export interface MappedComponentTypes {
|
||||
[ComponentType.ActionRow]: ActionRow<ActionRowComponent>;
|
||||
[ComponentType.Button]: ButtonComponent;
|
||||
[ComponentType.SelectMenu]: SelectMenuComponent;
|
||||
[ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>;
|
||||
[ComponentType.Button]: ButtonBuilder;
|
||||
[ComponentType.SelectMenu]: SelectMenuBuilder;
|
||||
[ComponentType.TextInput]: TextInputBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating components from API data
|
||||
* @param data The api data to transform to a component class
|
||||
*/
|
||||
export function createComponent<T extends keyof MappedComponentTypes>(
|
||||
data: APIMessageComponent & { type: T },
|
||||
export function createComponentBuilder<T extends keyof MappedComponentTypes>(
|
||||
data: (APIMessageComponent | APIModalComponent) & { type: T },
|
||||
): MappedComponentTypes[T];
|
||||
export function createComponent(data: APIMessageComponent): Component {
|
||||
export function createComponentBuilder<C extends MessageComponentBuilder | ModalComponentBuilder>(data: C): C;
|
||||
export function createComponentBuilder(
|
||||
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
|
||||
): ComponentBuilder {
|
||||
if (data instanceof ComponentBuilder) {
|
||||
return data;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case ComponentType.ActionRow:
|
||||
return new ActionRow(data);
|
||||
return new ActionRowBuilder(data);
|
||||
case ComponentType.Button:
|
||||
return new ButtonComponent(data);
|
||||
return new ButtonBuilder(data);
|
||||
case ComponentType.SelectMenu:
|
||||
return new SelectMenuComponent(data);
|
||||
return new SelectMenuBuilder(data);
|
||||
case ComponentType.TextInput:
|
||||
return new TextInputBuilder(data);
|
||||
default:
|
||||
// @ts-expect-error
|
||||
throw new Error(`Cannot serialize component type: ${data.type as number}`);
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Cannot properly serialize component type: ${data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
57
packages/builders/src/components/button/Button.ts
Normal file
57
packages/builders/src/components/button/Button.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type {
|
||||
ButtonStyle,
|
||||
APIMessageComponentEmoji,
|
||||
APIButtonComponent,
|
||||
APIButtonComponentWithCustomId,
|
||||
APIButtonComponentWithURL,
|
||||
} from 'discord-api-types/v10';
|
||||
import { UnsafeButtonBuilder } from './UnsafeButton';
|
||||
import {
|
||||
buttonLabelValidator,
|
||||
buttonStyleValidator,
|
||||
customIdValidator,
|
||||
disabledValidator,
|
||||
emojiValidator,
|
||||
urlValidator,
|
||||
validateRequiredButtonParameters,
|
||||
} from '../Assertions';
|
||||
|
||||
/**
|
||||
* Represents a validated button component
|
||||
*/
|
||||
export class ButtonBuilder extends UnsafeButtonBuilder {
|
||||
public override setStyle(style: ButtonStyle) {
|
||||
return super.setStyle(buttonStyleValidator.parse(style));
|
||||
}
|
||||
|
||||
public override setURL(url: string) {
|
||||
return super.setURL(urlValidator.parse(url));
|
||||
}
|
||||
|
||||
public override setCustomId(customId: string) {
|
||||
return super.setCustomId(customIdValidator.parse(customId));
|
||||
}
|
||||
|
||||
public override setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
return super.setEmoji(emojiValidator.parse(emoji));
|
||||
}
|
||||
|
||||
public override setDisabled(disabled = true) {
|
||||
return super.setDisabled(disabledValidator.parse(disabled));
|
||||
}
|
||||
|
||||
public override setLabel(label: string) {
|
||||
return super.setLabel(buttonLabelValidator.parse(label));
|
||||
}
|
||||
|
||||
public override toJSON(): APIButtonComponent {
|
||||
validateRequiredButtonParameters(
|
||||
this.data.style,
|
||||
this.data.label,
|
||||
this.data.emoji,
|
||||
(this.data as APIButtonComponentWithCustomId).custom_id,
|
||||
(this.data as APIButtonComponentWithURL).url,
|
||||
);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
79
packages/builders/src/components/button/UnsafeButton.ts
Normal file
79
packages/builders/src/components/button/UnsafeButton.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ComponentType,
|
||||
ButtonStyle,
|
||||
type APIMessageComponentEmoji,
|
||||
type APIButtonComponent,
|
||||
type APIButtonComponentWithURL,
|
||||
type APIButtonComponentWithCustomId,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
|
||||
/**
|
||||
* Represents a non-validated button component
|
||||
*/
|
||||
export class UnsafeButtonBuilder extends ComponentBuilder<APIButtonComponent> {
|
||||
public constructor(data?: Partial<APIButtonComponent>) {
|
||||
super({ type: ComponentType.Button, ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style of this button
|
||||
* @param style The style of the button
|
||||
*/
|
||||
public setStyle(style: ButtonStyle) {
|
||||
this.data.style = style;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL for this button
|
||||
* @param url The URL to open when this button is clicked
|
||||
*/
|
||||
public setURL(url: string) {
|
||||
(this.data as APIButtonComponentWithURL).url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom Id for this button
|
||||
* @param customId The custom id to use for this button
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
(this.data as APIButtonComponentWithCustomId).custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this button
|
||||
* @param emoji The emoji to display on this button
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
this.data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this button is disable or not
|
||||
* @param disabled Whether or not to disable this button or not
|
||||
*/
|
||||
public setDisabled(disabled = true) {
|
||||
this.data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this button
|
||||
* @param label The label to display on this button
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIButtonComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
} as APIButtonComponent;
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,68 @@
|
||||
import { APISelectMenuComponent, ComponentType } from 'discord-api-types/v9';
|
||||
import type { APISelectMenuComponent, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
import {
|
||||
customIdValidator,
|
||||
disabledValidator,
|
||||
minMaxValidator,
|
||||
optionsLengthValidator,
|
||||
optionValidator,
|
||||
placeholderValidator,
|
||||
validateRequiredSelectMenuParameters,
|
||||
} from '../Assertions';
|
||||
import type { Component } from '../Component';
|
||||
import { SelectMenuOption } from './SelectMenuOption';
|
||||
|
||||
/**
|
||||
* Represents a select menu component
|
||||
* Represents a validated select menu component
|
||||
*/
|
||||
export class SelectMenuComponent implements Component {
|
||||
public readonly type = ComponentType.SelectMenu as const;
|
||||
public readonly options: SelectMenuOption[];
|
||||
public readonly placeholder?: string;
|
||||
public readonly min_values?: number;
|
||||
public readonly max_values?: number;
|
||||
public readonly custom_id!: string;
|
||||
public readonly disabled?: boolean;
|
||||
|
||||
public constructor(data?: APISelectMenuComponent) {
|
||||
this.options = data?.options.map((option) => new SelectMenuOption(option)) ?? [];
|
||||
this.placeholder = data?.placeholder;
|
||||
this.min_values = data?.min_values;
|
||||
this.max_values = data?.max_values;
|
||||
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.custom_id = data?.custom_id as string;
|
||||
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.disabled = data?.disabled;
|
||||
export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
|
||||
public override setPlaceholder(placeholder: string) {
|
||||
return super.setPlaceholder(placeholderValidator.parse(placeholder));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the placeholder for this select menu
|
||||
* @param placeholder The placeholder to use for this select menu
|
||||
*/
|
||||
public setPlaceholder(placeholder: string) {
|
||||
placeholderValidator.parse(placeholder);
|
||||
Reflect.set(this, 'placeholder', placeholder);
|
||||
public override setMinValues(minValues: number) {
|
||||
return super.setMinValues(minMaxValidator.parse(minValues));
|
||||
}
|
||||
|
||||
public override setMaxValues(maxValues: number) {
|
||||
return super.setMaxValues(minMaxValidator.parse(maxValues));
|
||||
}
|
||||
|
||||
public override setCustomId(customId: string) {
|
||||
return super.setCustomId(customIdValidator.parse(customId));
|
||||
}
|
||||
|
||||
public override setDisabled(disabled = true) {
|
||||
return super.setDisabled(disabledValidator.parse(disabled));
|
||||
}
|
||||
|
||||
public override addOptions(options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
optionsLengthValidator.parse(this.options.length + options.length);
|
||||
this.options.push(
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOptionBuilder
|
||||
? option
|
||||
: new UnsafeSelectMenuOptionBuilder(optionValidator.parse(option) as APISelectMenuOption),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets thes minimum values that must be selected in the select menu
|
||||
* @param minValues The minimum values that must be selected
|
||||
*/
|
||||
public setMinValues(minValues: number) {
|
||||
minMaxValidator.parse(minValues);
|
||||
Reflect.set(this, 'min_values', minValues);
|
||||
public override setOptions(options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
optionsLengthValidator.parse(options.length);
|
||||
this.options.splice(
|
||||
0,
|
||||
this.options.length,
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOptionBuilder
|
||||
? option
|
||||
: new UnsafeSelectMenuOptionBuilder(optionValidator.parse(option) as APISelectMenuOption),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets thes maximum values that must be selected in the select menu
|
||||
* @param minValues The maximum values that must be selected
|
||||
*/
|
||||
public setMaxValues(maxValues: number) {
|
||||
minMaxValidator.parse(maxValues);
|
||||
Reflect.set(this, 'max_values', maxValues);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom Id for this select menu
|
||||
* @param customId The custom ID to use for this select menu
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
customIdValidator.parse(customId);
|
||||
Reflect.set(this, 'custom_id', customId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not this select menu is disabled
|
||||
* @param disabled Whether or not this select menu is disabled
|
||||
*/
|
||||
public setDisabled(disabled: boolean) {
|
||||
disabledValidator.parse(disabled);
|
||||
Reflect.set(this, 'disabled', disabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds options to this select menu
|
||||
* @param options The options to add to this select menu
|
||||
* @returns
|
||||
*/
|
||||
public addOptions(...options: SelectMenuOption[]) {
|
||||
this.options.push(...options);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the options on this select menu
|
||||
* @param options The options to set on this select menu
|
||||
*/
|
||||
public setOptions(options: SelectMenuOption[]) {
|
||||
Reflect.set(this, 'options', [...options]);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APISelectMenuComponent {
|
||||
validateRequiredSelectMenuParameters(this.options, this.custom_id);
|
||||
return {
|
||||
...this,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
};
|
||||
public override toJSON(): APISelectMenuComponent {
|
||||
validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v9';
|
||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
import {
|
||||
defaultValidator,
|
||||
emojiValidator,
|
||||
@@ -7,77 +8,23 @@ import {
|
||||
} from '../Assertions';
|
||||
|
||||
/**
|
||||
* Represents an option within a select menu component
|
||||
* Represents a validated option within a select menu component
|
||||
*/
|
||||
export class SelectMenuOption {
|
||||
public readonly label!: string;
|
||||
public readonly value!: string;
|
||||
public readonly description?: string;
|
||||
public readonly emoji?: APIMessageComponentEmoji;
|
||||
public readonly default?: boolean;
|
||||
|
||||
public constructor(data?: APISelectMenuOption) {
|
||||
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.label = data?.label as string;
|
||||
this.value = data?.value as string;
|
||||
/* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */
|
||||
this.description = data?.description;
|
||||
this.emoji = data?.emoji;
|
||||
this.default = data?.default;
|
||||
export class SelectMenuOptionBuilder extends UnsafeSelectMenuOptionBuilder {
|
||||
public override setDescription(description: string) {
|
||||
return super.setDescription(labelValueValidator.parse(description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label of this option
|
||||
* @param label The label to show on this option
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
Reflect.set(this, 'label', label);
|
||||
return this;
|
||||
public override setDefault(isDefault = true) {
|
||||
return super.setDefault(defaultValidator.parse(isDefault));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of this option
|
||||
* @param value The value of this option
|
||||
*/
|
||||
public setValue(value: string) {
|
||||
Reflect.set(this, 'value', value);
|
||||
return this;
|
||||
public override setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
return super.setEmoji(emojiValidator.parse(emoji));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this option.
|
||||
* @param description The description of this option
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
labelValueValidator.parse(description);
|
||||
Reflect.set(this, 'description', description);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this option is selected by default
|
||||
* @param isDefault Whether or not this option is selected by default
|
||||
*/
|
||||
public setDefault(isDefault: boolean) {
|
||||
defaultValidator.parse(isDefault);
|
||||
Reflect.set(this, 'default', isDefault);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this button
|
||||
* @param emoji The emoji to display on this button
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
emojiValidator.parse(emoji);
|
||||
Reflect.set(this, 'emoji', emoji);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APISelectMenuOption {
|
||||
validateRequiredSelectMenuOptionParameters(this.label, this.value);
|
||||
return {
|
||||
...this,
|
||||
};
|
||||
public override toJSON(): APISelectMenuOption {
|
||||
validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
101
packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts
Normal file
101
packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v10';
|
||||
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
|
||||
import { ComponentBuilder } from '../Component';
|
||||
|
||||
/**
|
||||
* Represents a non-validated select menu component
|
||||
*/
|
||||
export class UnsafeSelectMenuBuilder extends ComponentBuilder<APISelectMenuComponent> {
|
||||
/**
|
||||
* The options within this select menu
|
||||
*/
|
||||
public readonly options: UnsafeSelectMenuOptionBuilder[];
|
||||
|
||||
public constructor(data?: Partial<APISelectMenuComponent>) {
|
||||
const { options, ...initData } = data ?? {};
|
||||
super({ type: ComponentType.SelectMenu, ...initData });
|
||||
this.options = options?.map((o) => new UnsafeSelectMenuOptionBuilder(o)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the placeholder for this select menu
|
||||
* @param placeholder The placeholder to use for this select menu
|
||||
*/
|
||||
public setPlaceholder(placeholder: string) {
|
||||
this.data.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum values that must be selected in the select menu
|
||||
* @param minValues The minimum values that must be selected
|
||||
*/
|
||||
public setMinValues(minValues: number) {
|
||||
this.data.min_values = minValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum values that must be selected in the select menu
|
||||
* @param minValues The maximum values that must be selected
|
||||
*/
|
||||
public setMaxValues(maxValues: number) {
|
||||
this.data.max_values = maxValues;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom Id for this select menu
|
||||
* @param customId The custom id to use for this select menu
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not this select menu is disabled
|
||||
* @param disabled Whether or not this select menu is disabled
|
||||
*/
|
||||
public setDisabled(disabled = true) {
|
||||
this.data.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds options to this select menu
|
||||
* @param options The options to add to this select menu
|
||||
* @returns
|
||||
*/
|
||||
public addOptions(options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
this.options.push(
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the options on this select menu
|
||||
* @param options The options to set on this select menu
|
||||
*/
|
||||
public setOptions(options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
|
||||
this.options.splice(
|
||||
0,
|
||||
this.options.length,
|
||||
...options.map((option) =>
|
||||
option instanceof UnsafeSelectMenuOptionBuilder ? option : new UnsafeSelectMenuOptionBuilder(option),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APISelectMenuComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
options: this.options.map((o) => o.toJSON()),
|
||||
} as APISelectMenuComponent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
|
||||
|
||||
/**
|
||||
* Represents a non-validated option within a select menu component
|
||||
*/
|
||||
export class UnsafeSelectMenuOptionBuilder {
|
||||
public constructor(public data: Partial<APISelectMenuOption> = {}) {}
|
||||
|
||||
/**
|
||||
* Sets the label of this option
|
||||
* @param label The label to show on this option
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of this option
|
||||
* @param value The value of this option
|
||||
*/
|
||||
public setValue(value: string) {
|
||||
this.data.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this option.
|
||||
* @param description The description of this option
|
||||
*/
|
||||
public setDescription(description: string) {
|
||||
this.data.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this option is selected by default
|
||||
* @param isDefault Whether this option is selected by default
|
||||
*/
|
||||
public setDefault(isDefault = true) {
|
||||
this.data.default = isDefault;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the emoji to display on this option
|
||||
* @param emoji The emoji to display on this option
|
||||
*/
|
||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||
this.data.emoji = emoji;
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APISelectMenuOption {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
} as APISelectMenuOption;
|
||||
}
|
||||
}
|
||||
17
packages/builders/src/components/textInput/Assertions.ts
Normal file
17
packages/builders/src/components/textInput/Assertions.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { TextInputStyle } from 'discord-api-types/v10';
|
||||
import { customIdValidator } from '../Assertions';
|
||||
|
||||
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
|
||||
export const minLengthValidator = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(4000);
|
||||
export const maxLengthValidator = s.number.int.greaterThanOrEqual(1).lessThanOrEqual(4000);
|
||||
export const requiredValidator = s.boolean;
|
||||
export const valueValidator = s.string.lengthLessThanOrEqual(4000);
|
||||
export const placeholderValidator = s.string.lengthLessThanOrEqual(100);
|
||||
export const labelValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(45);
|
||||
|
||||
export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
|
||||
customIdValidator.parse(customId);
|
||||
textInputStyleValidator.parse(style);
|
||||
labelValidator.parse(label);
|
||||
}
|
||||
37
packages/builders/src/components/textInput/TextInput.ts
Normal file
37
packages/builders/src/components/textInput/TextInput.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { APITextInputComponent } from 'discord-api-types/v10';
|
||||
import {
|
||||
maxLengthValidator,
|
||||
minLengthValidator,
|
||||
placeholderValidator,
|
||||
requiredValidator,
|
||||
valueValidator,
|
||||
validateRequiredParameters,
|
||||
} from './Assertions';
|
||||
import { UnsafeTextInputBuilder } from './UnsafeTextInput';
|
||||
|
||||
export class TextInputBuilder extends UnsafeTextInputBuilder {
|
||||
public override setMinLength(minLength: number) {
|
||||
return super.setMinLength(minLengthValidator.parse(minLength));
|
||||
}
|
||||
|
||||
public override setMaxLength(maxLength: number) {
|
||||
return super.setMaxLength(maxLengthValidator.parse(maxLength));
|
||||
}
|
||||
|
||||
public override setRequired(required = true) {
|
||||
return super.setRequired(requiredValidator.parse(required));
|
||||
}
|
||||
|
||||
public override setValue(value: string) {
|
||||
return super.setValue(valueValidator.parse(value));
|
||||
}
|
||||
|
||||
public override setPlaceholder(placeholder: string) {
|
||||
return super.setPlaceholder(placeholderValidator.parse(placeholder));
|
||||
}
|
||||
|
||||
public override toJSON(): APITextInputComponent {
|
||||
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { ComponentBuilder } from '../../index';
|
||||
|
||||
export class UnsafeTextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
||||
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
|
||||
super({ type: ComponentType.TextInput, ...data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id for this text input
|
||||
* @param customId The custom id of this text input
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the label for this text input
|
||||
* @param label The label for this text input
|
||||
*/
|
||||
public setLabel(label: string) {
|
||||
this.data.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style for this text input
|
||||
* @param style The style for this text input
|
||||
*/
|
||||
public setStyle(style: TextInputStyle) {
|
||||
this.data.style = style;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum length of text for this text input
|
||||
* @param minLength The minimum length of text for this text input
|
||||
*/
|
||||
public setMinLength(minLength: number) {
|
||||
this.data.min_length = minLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum length of text for this text input
|
||||
* @param maxLength The maximum length of text for this text input
|
||||
*/
|
||||
public setMaxLength(maxLength: number) {
|
||||
this.data.max_length = maxLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the placeholder of this text input
|
||||
* @param placeholder The placeholder of this text input
|
||||
*/
|
||||
public setPlaceholder(placeholder: string) {
|
||||
this.data.placeholder = placeholder;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of this text input
|
||||
* @param value The value for this text input
|
||||
*/
|
||||
public setValue(value: string) {
|
||||
this.data.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this text input is required or not
|
||||
* @param required Whether this text input is required or not
|
||||
*/
|
||||
public setRequired(required = true) {
|
||||
this.data.required = required;
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APITextInputComponent {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
} as APITextInputComponent;
|
||||
}
|
||||
|
||||
public equals(other: UnsafeTextInputBuilder | APITextInputComponent): boolean {
|
||||
if (other instanceof UnsafeTextInputBuilder) {
|
||||
return isEqual(other.data, this.data);
|
||||
}
|
||||
|
||||
return isEqual(other, this.data);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
export * as EmbedAssertions from './messages/embed/Assertions';
|
||||
export * from './messages/embed/Embed';
|
||||
export * from './messages/formatters';
|
||||
export * from './messages/embed/UnsafeEmbed';
|
||||
|
||||
export * as ComponentAssertions from './components/Assertions';
|
||||
export * from './components/ActionRow';
|
||||
export * from './components/Button';
|
||||
export * from './components/button/Button';
|
||||
export * from './components/Component';
|
||||
export * from './components/Components';
|
||||
export * from './components/textInput/TextInput';
|
||||
export * as TextInputAssertions from './components/textInput/Assertions';
|
||||
export * from './components/textInput/UnsafeTextInput';
|
||||
export * from './interactions/modals/UnsafeModal';
|
||||
export * from './interactions/modals/Modal';
|
||||
export * as ModalAssertions from './interactions/modals/Assertions';
|
||||
export * from './components/selectMenu/SelectMenu';
|
||||
export * from './components/selectMenu/SelectMenuOption';
|
||||
export * from './components/button/UnsafeButton';
|
||||
export * from './components/selectMenu/UnsafeSelectMenu';
|
||||
export * from './components/selectMenu/UnsafeSelectMenuOption';
|
||||
|
||||
export * as SlashCommandAssertions from './interactions/slashCommands/Assertions';
|
||||
export * from './interactions/slashCommands/SlashCommandBuilder';
|
||||
@@ -19,8 +29,19 @@ export * from './interactions/slashCommands/options/integer';
|
||||
export * from './interactions/slashCommands/options/mentionable';
|
||||
export * from './interactions/slashCommands/options/number';
|
||||
export * from './interactions/slashCommands/options/role';
|
||||
export * from './interactions/slashCommands/options/attachment';
|
||||
export * from './interactions/slashCommands/options/string';
|
||||
export * from './interactions/slashCommands/options/user';
|
||||
export * from './interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
|
||||
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionBase';
|
||||
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin';
|
||||
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
export * from './interactions/slashCommands/mixins/NameAndDescription';
|
||||
export * from './interactions/slashCommands/mixins/SharedSlashCommandOptions';
|
||||
|
||||
export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions';
|
||||
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder';
|
||||
|
||||
export * from './util/jsonEncodable';
|
||||
export * from './util/equatable';
|
||||
export * from './util/componentUtil';
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
import { ApplicationCommandType } from 'discord-api-types/v9';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ApplicationCommandType } from 'discord-api-types/v10';
|
||||
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder';
|
||||
|
||||
const namePredicate = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.regex(/^( *[\p{L}\p{N}_-]+ *)+$/u);
|
||||
const namePredicate = s.string
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(32)
|
||||
.regex(/^( *[\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+ *)+$/u);
|
||||
|
||||
const typePredicate = z.union([z.literal(ApplicationCommandType.User), z.literal(ApplicationCommandType.Message)]);
|
||||
const typePredicate = s.union(s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message));
|
||||
|
||||
const booleanPredicate = z.boolean();
|
||||
const booleanPredicate = s.boolean;
|
||||
|
||||
export function validateDefaultPermission(value: unknown): asserts value is boolean {
|
||||
booleanPredicate.parse(value);
|
||||
@@ -31,3 +30,19 @@ export function validateRequiredParameters(name: string, type: number) {
|
||||
// Assert type is valid
|
||||
validateType(type);
|
||||
}
|
||||
|
||||
const dmPermissionPredicate = s.boolean.nullish;
|
||||
|
||||
export function validateDMPermission(value: unknown): asserts value is boolean | null | undefined {
|
||||
dmPermissionPredicate.parse(value);
|
||||
}
|
||||
|
||||
const memberPermissionPredicate = s.union(
|
||||
s.bigint.transform((value) => value.toString()),
|
||||
s.number.safeInt.transform((value) => value.toString()),
|
||||
s.string.regex(/^\d+$/),
|
||||
).nullish;
|
||||
|
||||
export function validateDefaultMemberPermissions(permissions: unknown) {
|
||||
return memberPermissionPredicate.parse(permissions);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import { validateRequiredParameters, validateName, validateType, validateDefaultPermission } from './Assertions';
|
||||
import type { ApplicationCommandType, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v9';
|
||||
import type {
|
||||
ApplicationCommandType,
|
||||
LocaleString,
|
||||
LocalizationMap,
|
||||
Permissions,
|
||||
RESTPostAPIApplicationCommandsJSONBody,
|
||||
} from 'discord-api-types/v10';
|
||||
import {
|
||||
validateRequiredParameters,
|
||||
validateName,
|
||||
validateType,
|
||||
validateDefaultPermission,
|
||||
validateDefaultMemberPermissions,
|
||||
validateDMPermission,
|
||||
} from './Assertions';
|
||||
import { validateLocale, validateLocalizationMap } from '../slashCommands/Assertions';
|
||||
|
||||
export class ContextMenuCommandBuilder {
|
||||
/**
|
||||
@@ -7,6 +21,11 @@ export class ContextMenuCommandBuilder {
|
||||
*/
|
||||
public readonly name: string = undefined!;
|
||||
|
||||
/**
|
||||
* The localized names for this command
|
||||
*/
|
||||
public readonly name_localizations?: LocalizationMap;
|
||||
|
||||
/**
|
||||
* The type of this context menu command
|
||||
*/
|
||||
@@ -15,9 +34,21 @@ export class ContextMenuCommandBuilder {
|
||||
/**
|
||||
* Whether the command is enabled by default when the app is added to a guild
|
||||
*
|
||||
* @default true
|
||||
* @deprecated This property is deprecated and will be removed in the future.
|
||||
* You should use `setDefaultMemberPermissions` or `setDMPermission` instead.
|
||||
*/
|
||||
public readonly defaultPermission: boolean | undefined = undefined;
|
||||
public readonly default_permission: boolean | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Set of permissions represented as a bit set for the command
|
||||
*/
|
||||
public readonly default_member_permissions: Permissions | null | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Indicates whether the command is available in DMs with the application, only for globally-scoped commands.
|
||||
* By default, commands are visible.
|
||||
*/
|
||||
public readonly dm_permission: boolean | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Sets the name
|
||||
@@ -55,16 +86,95 @@ export class ContextMenuCommandBuilder {
|
||||
* @param value Whether or not to enable this command by default
|
||||
*
|
||||
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
|
||||
* @deprecated Use `setDefaultMemberPermissions` or `setDMPermission` instead.
|
||||
*/
|
||||
public setDefaultPermission(value: boolean) {
|
||||
// Assert the value matches the conditions
|
||||
validateDefaultPermission(value);
|
||||
|
||||
Reflect.set(this, 'defaultPermission', value);
|
||||
Reflect.set(this, 'default_permission', value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default permissions a member should have in order to run the command.
|
||||
*
|
||||
* **Note:** You can set this to `'0'` to disable the command by default.
|
||||
*
|
||||
* @param permissions The permissions bit field to set
|
||||
*
|
||||
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
|
||||
*/
|
||||
public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) {
|
||||
// Assert the value and parse it
|
||||
const permissionValue = validateDefaultMemberPermissions(permissions);
|
||||
|
||||
Reflect.set(this, 'default_member_permissions', permissionValue);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if the command is available in DMs with the application, only for globally-scoped commands.
|
||||
* By default, commands are visible.
|
||||
*
|
||||
* @param enabled If the command should be enabled in DMs
|
||||
*
|
||||
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
|
||||
*/
|
||||
public setDMPermission(enabled: boolean | null | undefined) {
|
||||
// Assert the value matches the conditions
|
||||
validateDMPermission(enabled);
|
||||
|
||||
Reflect.set(this, 'dm_permission', enabled);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a name localization
|
||||
*
|
||||
* @param locale The locale to set a description for
|
||||
* @param localizedName The localized description for the given locale
|
||||
*/
|
||||
public setNameLocalization(locale: LocaleString, localizedName: string | null) {
|
||||
if (!this.name_localizations) {
|
||||
Reflect.set(this, 'name_localizations', {});
|
||||
}
|
||||
|
||||
const parsedLocale = validateLocale(locale);
|
||||
|
||||
if (localizedName === null) {
|
||||
this.name_localizations![parsedLocale] = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
validateName(localizedName);
|
||||
|
||||
this.name_localizations![parsedLocale] = localizedName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name localizations
|
||||
*
|
||||
* @param localizedNames The dictionary of localized descriptions to set
|
||||
*/
|
||||
public setNameLocalizations(localizedNames: LocalizationMap | null) {
|
||||
if (localizedNames === null) {
|
||||
Reflect.set(this, 'name_localizations', null);
|
||||
return this;
|
||||
}
|
||||
|
||||
Reflect.set(this, 'name_localizations', {});
|
||||
|
||||
Object.entries(localizedNames).forEach((args) =>
|
||||
this.setNameLocalization(...(args as [LocaleString, string | null])),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final data that should be sent to Discord.
|
||||
*
|
||||
@@ -72,11 +182,10 @@ export class ContextMenuCommandBuilder {
|
||||
*/
|
||||
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
|
||||
validateRequiredParameters(this.name, this.type);
|
||||
return {
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
default_permission: this.defaultPermission,
|
||||
};
|
||||
|
||||
validateLocalizationMap(this.name_localizations);
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
packages/builders/src/interactions/modals/Assertions.ts
Normal file
16
packages/builders/src/interactions/modals/Assertions.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../..';
|
||||
import { customIdValidator } from '../../components/Assertions';
|
||||
|
||||
export const titleValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(45);
|
||||
export const componentsValidator = s.instance(ActionRowBuilder).array.lengthGreaterThanOrEqual(1);
|
||||
|
||||
export function validateRequiredParameters(
|
||||
customId?: string,
|
||||
title?: string,
|
||||
components?: ActionRowBuilder<ModalActionRowComponentBuilder>[],
|
||||
) {
|
||||
customIdValidator.parse(customId);
|
||||
titleValidator.parse(title);
|
||||
componentsValidator.parse(components);
|
||||
}
|
||||
19
packages/builders/src/interactions/modals/Modal.ts
Normal file
19
packages/builders/src/interactions/modals/Modal.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
|
||||
import { titleValidator, validateRequiredParameters } from './Assertions';
|
||||
import { UnsafeModalBuilder } from './UnsafeModal';
|
||||
import { customIdValidator } from '../../components/Assertions';
|
||||
|
||||
export class ModalBuilder extends UnsafeModalBuilder {
|
||||
public override setCustomId(customId: string): this {
|
||||
return super.setCustomId(customIdValidator.parse(customId));
|
||||
}
|
||||
|
||||
public override setTitle(title: string) {
|
||||
return super.setTitle(titleValidator.parse(title));
|
||||
}
|
||||
|
||||
public override toJSON(): APIModalInteractionResponseCallbackData {
|
||||
validateRequiredParameters(this.data.custom_id, this.data.title, this.components);
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
72
packages/builders/src/interactions/modals/UnsafeModal.ts
Normal file
72
packages/builders/src/interactions/modals/UnsafeModal.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type {
|
||||
APIActionRowComponent,
|
||||
APIModalActionRowComponent,
|
||||
APIModalInteractionResponseCallbackData,
|
||||
} from 'discord-api-types/v10';
|
||||
import { ActionRowBuilder, createComponentBuilder, JSONEncodable, ModalActionRowComponentBuilder } from '../../index';
|
||||
|
||||
export class UnsafeModalBuilder implements JSONEncodable<APIModalInteractionResponseCallbackData> {
|
||||
public readonly data: Partial<APIModalInteractionResponseCallbackData>;
|
||||
public readonly components: ActionRowBuilder<ModalActionRowComponentBuilder>[] = [];
|
||||
|
||||
public constructor({ components, ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
|
||||
this.data = { ...data };
|
||||
this.components = (components?.map((c) => createComponentBuilder(c)) ??
|
||||
[]) as ActionRowBuilder<ModalActionRowComponentBuilder>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of the modal
|
||||
* @param title The title of the modal
|
||||
*/
|
||||
public setTitle(title: string) {
|
||||
this.data.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom id of the modal
|
||||
* @param customId The custom id of this modal
|
||||
*/
|
||||
public setCustomId(customId: string) {
|
||||
this.data.custom_id = customId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds components to this modal
|
||||
* @param components The components to add to this modal
|
||||
*/
|
||||
public addComponents(
|
||||
components: (
|
||||
| ActionRowBuilder<ModalActionRowComponentBuilder>
|
||||
| APIActionRowComponent<APIModalActionRowComponent>
|
||||
)[],
|
||||
) {
|
||||
this.components.push(
|
||||
...components.map((component) =>
|
||||
component instanceof ActionRowBuilder
|
||||
? component
|
||||
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
|
||||
),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the components in this modal
|
||||
* @param components The components to set this modal to
|
||||
*/
|
||||
public setComponents(components: ActionRowBuilder<ModalActionRowComponentBuilder>[]) {
|
||||
this.components.splice(0, this.components.length, ...components);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON(): APIModalInteractionResponseCallbackData {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return {
|
||||
...this.data,
|
||||
components: this.components.map((component) => component.toJSON()),
|
||||
} as APIModalInteractionResponseCallbackData;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,30 @@
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import is from '@sindresorhus/is';
|
||||
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
|
||||
import { type APIApplicationCommandOptionChoice, Locale, LocalizationMap } from 'discord-api-types/v10';
|
||||
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
|
||||
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
|
||||
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
|
||||
|
||||
const namePredicate = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.regex(/^[\P{Lu}\p{N}_-]+$/u);
|
||||
const namePredicate = s.string
|
||||
.lengthGreaterThanOrEqual(1)
|
||||
.lengthLessThanOrEqual(32)
|
||||
.regex(/^[\P{Lu}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u);
|
||||
|
||||
export function validateName(name: unknown): asserts name is string {
|
||||
namePredicate.parse(name);
|
||||
}
|
||||
|
||||
const descriptionPredicate = z.string().min(1).max(100);
|
||||
const descriptionPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
|
||||
const localePredicate = s.nativeEnum(Locale);
|
||||
|
||||
export function validateDescription(description: unknown): asserts description is string {
|
||||
descriptionPredicate.parse(description);
|
||||
}
|
||||
|
||||
const maxArrayLengthPredicate = z.unknown().array().max(25);
|
||||
const maxArrayLengthPredicate = s.unknown.array.lengthLessThanOrEqual(25);
|
||||
export function validateLocale(locale: unknown) {
|
||||
return localePredicate.parse(locale);
|
||||
}
|
||||
|
||||
export function validateMaxOptionsLength(options: unknown): asserts options is ToAPIApplicationCommandOptions[] {
|
||||
maxArrayLengthPredicate.parse(options);
|
||||
@@ -42,7 +45,7 @@ export function validateRequiredParameters(
|
||||
validateMaxOptionsLength(options);
|
||||
}
|
||||
|
||||
const booleanPredicate = z.boolean();
|
||||
const booleanPredicate = s.boolean;
|
||||
|
||||
export function validateDefaultPermission(value: unknown): asserts value is boolean {
|
||||
booleanPredicate.parse(value);
|
||||
@@ -52,8 +55,10 @@ export function validateRequired(required: unknown): asserts required is boolean
|
||||
booleanPredicate.parse(required);
|
||||
}
|
||||
|
||||
export function validateMaxChoicesLength(choices: APIApplicationCommandOptionChoice[]) {
|
||||
maxArrayLengthPredicate.parse(choices);
|
||||
const choicesLengthPredicate = s.number.lessThanOrEqual(25);
|
||||
|
||||
export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void {
|
||||
choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding);
|
||||
}
|
||||
|
||||
export function assertReturnOfBuilder<
|
||||
@@ -82,3 +87,27 @@ export function assertReturnOfBuilder<
|
||||
throw new TypeError(`Expected to receive a ${instanceName} builder, got ${fullResultName} instead.`);
|
||||
}
|
||||
}
|
||||
|
||||
export const localizationMapPredicate = s.object<LocalizationMap>(
|
||||
Object.fromEntries(Object.values(Locale).map((locale) => [locale, s.string.nullish])),
|
||||
).strict.nullish;
|
||||
|
||||
export function validateLocalizationMap(value: unknown): asserts value is LocalizationMap {
|
||||
localizationMapPredicate.parse(value);
|
||||
}
|
||||
|
||||
const dmPermissionPredicate = s.boolean.nullish;
|
||||
|
||||
export function validateDMPermission(value: unknown): asserts value is boolean | null | undefined {
|
||||
dmPermissionPredicate.parse(value);
|
||||
}
|
||||
|
||||
const memberPermissionPredicate = s.union(
|
||||
s.bigint.transform((value) => value.toString()),
|
||||
s.number.safeInt.transform((value) => value.toString()),
|
||||
s.string.regex(/^\d+$/),
|
||||
).nullish;
|
||||
|
||||
export function validateDefaultMemberPermissions(permissions: unknown) {
|
||||
return memberPermissionPredicate.parse(permissions);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import type { APIApplicationCommandOption, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v9';
|
||||
import type {
|
||||
APIApplicationCommandOption,
|
||||
LocalizationMap,
|
||||
Permissions,
|
||||
RESTPostAPIApplicationCommandsJSONBody,
|
||||
} from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import {
|
||||
assertReturnOfBuilder,
|
||||
validateDefaultMemberPermissions,
|
||||
validateDefaultPermission,
|
||||
validateLocalizationMap,
|
||||
validateDMPermission,
|
||||
validateMaxOptionsLength,
|
||||
validateRequiredParameters,
|
||||
} from './Assertions';
|
||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
|
||||
import { SharedNameAndDescription } from './mixins/NameAndDescription';
|
||||
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
|
||||
import { SharedNameAndDescription } from './mixins/NameAndDescription';
|
||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
|
||||
|
||||
@mix(SharedSlashCommandOptions, SharedNameAndDescription)
|
||||
export class SlashCommandBuilder {
|
||||
@@ -17,11 +25,21 @@ export class SlashCommandBuilder {
|
||||
*/
|
||||
public readonly name: string = undefined!;
|
||||
|
||||
/**
|
||||
* The localized names for this command
|
||||
*/
|
||||
public readonly name_localizations?: LocalizationMap;
|
||||
|
||||
/**
|
||||
* The description of this slash command
|
||||
*/
|
||||
public readonly description: string = undefined!;
|
||||
|
||||
/**
|
||||
* The localized descriptions for this command
|
||||
*/
|
||||
public readonly description_localizations?: LocalizationMap;
|
||||
|
||||
/**
|
||||
* The options of this slash command
|
||||
*/
|
||||
@@ -30,9 +48,21 @@ export class SlashCommandBuilder {
|
||||
/**
|
||||
* Whether the command is enabled by default when the app is added to a guild
|
||||
*
|
||||
* @default true
|
||||
* @deprecated This property is deprecated and will be removed in the future.
|
||||
* You should use `setDefaultMemberPermissions` or `setDMPermission` instead.
|
||||
*/
|
||||
public readonly defaultPermission: boolean | undefined = undefined;
|
||||
public readonly default_permission: boolean | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Set of permissions represented as a bit set for the command
|
||||
*/
|
||||
public readonly default_member_permissions: Permissions | null | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Indicates whether the command is available in DMs with the application, only for globally-scoped commands.
|
||||
* By default, commands are visible.
|
||||
*/
|
||||
public readonly dm_permission: boolean | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Returns the final data that should be sent to Discord.
|
||||
@@ -42,11 +72,12 @@ export class SlashCommandBuilder {
|
||||
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
|
||||
validateRequiredParameters(this.name, this.description, this.options);
|
||||
|
||||
validateLocalizationMap(this.name_localizations);
|
||||
validateLocalizationMap(this.description_localizations);
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
...this,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
default_permission: this.defaultPermission,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,12 +89,48 @@ export class SlashCommandBuilder {
|
||||
* @param value Whether or not to enable this command by default
|
||||
*
|
||||
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
|
||||
* @deprecated Use `setDefaultMemberPermissions` or `setDMPermission` instead.
|
||||
*/
|
||||
public setDefaultPermission(value: boolean) {
|
||||
// Assert the value matches the conditions
|
||||
validateDefaultPermission(value);
|
||||
|
||||
Reflect.set(this, 'defaultPermission', value);
|
||||
Reflect.set(this, 'default_permission', value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default permissions a member should have in order to run the command.
|
||||
*
|
||||
* **Note:** You can set this to `'0'` to disable the command by default.
|
||||
*
|
||||
* @param permissions The permissions bit field to set
|
||||
*
|
||||
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
|
||||
*/
|
||||
public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) {
|
||||
// Assert the value and parse it
|
||||
const permissionValue = validateDefaultMemberPermissions(permissions);
|
||||
|
||||
Reflect.set(this, 'default_member_permissions', permissionValue);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if the command is available in DMs with the application, only for globally-scoped commands.
|
||||
* By default, commands are visible.
|
||||
*
|
||||
* @param enabled If the command should be enabled in DMs
|
||||
*
|
||||
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
|
||||
*/
|
||||
public setDMPermission(enabled: boolean | null | undefined) {
|
||||
// Assert the value matches the conditions
|
||||
validateDMPermission(enabled);
|
||||
|
||||
Reflect.set(this, 'dm_permission', enabled);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import {
|
||||
APIApplicationCommandSubcommandGroupOption,
|
||||
APIApplicationCommandSubcommandOption,
|
||||
ApplicationCommandOptionType,
|
||||
} from 'discord-api-types/v9';
|
||||
} from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions';
|
||||
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
|
||||
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
|
||||
import { SharedNameAndDescription } from './mixins/NameAndDescription';
|
||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
|
||||
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
|
||||
|
||||
/**
|
||||
* Represents a folder for subcommands
|
||||
@@ -66,7 +66,9 @@ export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationComma
|
||||
return {
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
name: this.name,
|
||||
name_localizations: this.name_localizations,
|
||||
description: this.description,
|
||||
description_localizations: this.description_localizations,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
};
|
||||
}
|
||||
@@ -102,7 +104,9 @@ export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOpt
|
||||
return {
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
name: this.name,
|
||||
name_localizations: this.name_localizations,
|
||||
description: this.description,
|
||||
description_localizations: this.description_localizations,
|
||||
options: this.options.map((option) => option.toJSON()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { validateRequiredParameters, validateRequired } from '../Assertions';
|
||||
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { SharedNameAndDescription } from './NameAndDescription';
|
||||
import { validateRequiredParameters, validateRequired, validateLocalizationMap } from '../Assertions';
|
||||
|
||||
export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription {
|
||||
public abstract readonly type: ApplicationCommandOptionType;
|
||||
@@ -26,6 +26,10 @@ export abstract class ApplicationCommandOptionBase extends SharedNameAndDescript
|
||||
protected runRequiredValidations() {
|
||||
validateRequiredParameters(this.name, this.description, []);
|
||||
|
||||
// Validate localizations
|
||||
validateLocalizationMap(this.name_localizations);
|
||||
validateLocalizationMap(this.description_localizations);
|
||||
|
||||
// Assert that you actually passed a boolean
|
||||
validateRequired(this.required);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChannelType } from 'discord-api-types/v9';
|
||||
import { z, ZodLiteral } from 'zod';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { ChannelType } from 'discord-api-types/v10';
|
||||
|
||||
// Only allow valid channel types to be used. (This can't be dynamic because const enums are erased at runtime)
|
||||
const allowedChannelTypes = [
|
||||
@@ -7,7 +7,6 @@ const allowedChannelTypes = [
|
||||
ChannelType.GuildVoice,
|
||||
ChannelType.GuildCategory,
|
||||
ChannelType.GuildNews,
|
||||
ChannelType.GuildStore,
|
||||
ChannelType.GuildNewsThread,
|
||||
ChannelType.GuildPublicThread,
|
||||
ChannelType.GuildPrivateThread,
|
||||
@@ -16,40 +15,23 @@ const allowedChannelTypes = [
|
||||
|
||||
export type ApplicationCommandOptionAllowedChannelTypes = typeof allowedChannelTypes[number];
|
||||
|
||||
const channelTypePredicate = z.union(
|
||||
allowedChannelTypes.map((type) => z.literal(type)) as [
|
||||
ZodLiteral<ChannelType>,
|
||||
ZodLiteral<ChannelType>,
|
||||
...ZodLiteral<ChannelType>[]
|
||||
],
|
||||
);
|
||||
const channelTypesPredicate = s.array(s.union(...allowedChannelTypes.map((type) => s.literal(type))));
|
||||
|
||||
export class ApplicationCommandOptionChannelTypesMixin {
|
||||
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
|
||||
|
||||
/**
|
||||
* Adds a channel type to this option
|
||||
*
|
||||
* @param channelType The type of channel to allow
|
||||
*/
|
||||
public addChannelType(channelType: ApplicationCommandOptionAllowedChannelTypes) {
|
||||
if (this.channel_types === undefined) {
|
||||
Reflect.set(this, 'channel_types', []);
|
||||
}
|
||||
|
||||
channelTypePredicate.parse(channelType);
|
||||
this.channel_types!.push(channelType);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds channel types to this option
|
||||
*
|
||||
* @param channelTypes The channel types to add
|
||||
*/
|
||||
public addChannelTypes(channelTypes: ApplicationCommandOptionAllowedChannelTypes[]) {
|
||||
channelTypes.forEach((channelType) => this.addChannelType(channelType));
|
||||
public addChannelTypes(...channelTypes: ApplicationCommandOptionAllowedChannelTypes[]) {
|
||||
if (this.channel_types === undefined) {
|
||||
Reflect.set(this, 'channel_types', []);
|
||||
}
|
||||
|
||||
this.channel_types!.push(...channelTypesPredicate.parse(channelTypes));
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import { validateMaxChoicesLength } from '../Assertions';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { localizationMapPredicate, validateChoicesLength } from '../Assertions';
|
||||
|
||||
const stringPredicate = z.string().min(1).max(100);
|
||||
const numberPredicate = z.number().gt(-Infinity).lt(Infinity);
|
||||
const choicesPredicate = z.tuple([stringPredicate, z.union([stringPredicate, numberPredicate])]).array();
|
||||
const booleanPredicate = z.boolean();
|
||||
const stringPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
|
||||
const numberPredicate = s.number.greaterThan(-Infinity).lessThan(Infinity);
|
||||
const choicesPredicate = s.object({
|
||||
name: stringPredicate,
|
||||
name_localizations: localizationMapPredicate,
|
||||
value: s.union(stringPredicate, numberPredicate),
|
||||
}).array;
|
||||
const booleanPredicate = s.boolean;
|
||||
|
||||
export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends string | number> {
|
||||
public readonly choices?: APIApplicationCommandOptionChoice<T>[];
|
||||
@@ -14,59 +18,39 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
|
||||
// Since this is present and this is a mixin, this is needed
|
||||
public readonly type!: ApplicationCommandOptionType;
|
||||
|
||||
/**
|
||||
* Adds a choice for this option
|
||||
*
|
||||
* @param name The name of the choice
|
||||
* @param value The value of the choice
|
||||
*/
|
||||
public addChoice(name: string, value: T): Omit<this, 'setAutocomplete'> {
|
||||
if (this.autocomplete) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
if (this.choices === undefined) {
|
||||
Reflect.set(this, 'choices', []);
|
||||
}
|
||||
|
||||
validateMaxChoicesLength(this.choices!);
|
||||
|
||||
// Validate name
|
||||
stringPredicate.parse(name);
|
||||
|
||||
// Validate the value
|
||||
if (this.type === ApplicationCommandOptionType.String) {
|
||||
stringPredicate.parse(value);
|
||||
} else {
|
||||
numberPredicate.parse(value);
|
||||
}
|
||||
|
||||
this.choices!.push({ name, value });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple choices for this option
|
||||
*
|
||||
* @param choices The choices to add
|
||||
*/
|
||||
public addChoices(choices: [name: string, value: T][]): Omit<this, 'setAutocomplete'> {
|
||||
if (this.autocomplete) {
|
||||
public addChoices(...choices: APIApplicationCommandOptionChoice<T>[]): this {
|
||||
if (choices.length > 0 && this.autocomplete) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
|
||||
choicesPredicate.parse(choices);
|
||||
|
||||
for (const [label, value] of choices) this.addChoice(label, value);
|
||||
if (this.choices === undefined) {
|
||||
Reflect.set(this, 'choices', []);
|
||||
}
|
||||
|
||||
validateChoicesLength(choices.length, this.choices);
|
||||
|
||||
for (const { name, name_localizations, value } of choices) {
|
||||
// Validate the value
|
||||
if (this.type === ApplicationCommandOptionType.String) {
|
||||
stringPredicate.parse(value);
|
||||
} else {
|
||||
numberPredicate.parse(value);
|
||||
}
|
||||
|
||||
this.choices!.push({ name, name_localizations, value });
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public setChoices<Input extends [name: string, value: T][]>(
|
||||
choices: Input,
|
||||
): Input extends []
|
||||
? this & Pick<ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T>, 'setAutocomplete'>
|
||||
: Omit<this, 'setAutocomplete'> {
|
||||
public setChoices<Input extends APIApplicationCommandOptionChoice<T>[]>(...choices: Input): this {
|
||||
if (choices.length > 0 && this.autocomplete) {
|
||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
||||
}
|
||||
@@ -74,7 +58,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
|
||||
choicesPredicate.parse(choices);
|
||||
|
||||
Reflect.set(this, 'choices', []);
|
||||
for (const [label, value] of choices) this.addChoice(label, value);
|
||||
this.addChoices(...choices);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -83,11 +67,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
|
||||
* Marks the option as autocompletable
|
||||
* @param autocomplete If this option should be autocompletable
|
||||
*/
|
||||
public setAutocomplete<U extends boolean>(
|
||||
autocomplete: U,
|
||||
): U extends true
|
||||
? Omit<this, 'addChoice' | 'addChoices'>
|
||||
: this & Pick<ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T>, 'addChoice' | 'addChoices'> {
|
||||
public setAutocomplete(autocomplete: boolean): this {
|
||||
// Assert that you actually passed a boolean
|
||||
booleanPredicate.parse(autocomplete);
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { validateDescription, validateName } from '../Assertions';
|
||||
import type { LocaleString, LocalizationMap } from 'discord-api-types/v10';
|
||||
import { validateDescription, validateLocale, validateName } from '../Assertions';
|
||||
|
||||
export class SharedNameAndDescription {
|
||||
public readonly name!: string;
|
||||
public readonly name_localizations?: LocalizationMap;
|
||||
public readonly description!: string;
|
||||
public readonly description_localizations?: LocalizationMap;
|
||||
|
||||
/**
|
||||
* Sets the name
|
||||
@@ -31,4 +34,89 @@ export class SharedNameAndDescription {
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a name localization
|
||||
*
|
||||
* @param locale The locale to set a description for
|
||||
* @param localizedName The localized description for the given locale
|
||||
*/
|
||||
public setNameLocalization(locale: LocaleString, localizedName: string | null) {
|
||||
if (!this.name_localizations) {
|
||||
Reflect.set(this, 'name_localizations', {});
|
||||
}
|
||||
|
||||
const parsedLocale = validateLocale(locale);
|
||||
|
||||
if (localizedName === null) {
|
||||
this.name_localizations![parsedLocale] = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
validateName(localizedName);
|
||||
|
||||
this.name_localizations![parsedLocale] = localizedName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name localizations
|
||||
*
|
||||
* @param localizedNames The dictionary of localized descriptions to set
|
||||
*/
|
||||
public setNameLocalizations(localizedNames: LocalizationMap | null) {
|
||||
if (localizedNames === null) {
|
||||
Reflect.set(this, 'name_localizations', null);
|
||||
return this;
|
||||
}
|
||||
|
||||
Reflect.set(this, 'name_localizations', {});
|
||||
|
||||
Object.entries(localizedNames).forEach((args) =>
|
||||
this.setNameLocalization(...(args as [LocaleString, string | null])),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a description localization
|
||||
*
|
||||
* @param locale The locale to set a description for
|
||||
* @param localizedDescription The localized description for the given locale
|
||||
*/
|
||||
public setDescriptionLocalization(locale: LocaleString, localizedDescription: string | null) {
|
||||
if (!this.description_localizations) {
|
||||
Reflect.set(this, 'description_localizations', {});
|
||||
}
|
||||
|
||||
const parsedLocale = validateLocale(locale);
|
||||
|
||||
if (localizedDescription === null) {
|
||||
this.description_localizations![parsedLocale] = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
validateDescription(localizedDescription);
|
||||
|
||||
this.description_localizations![parsedLocale] = localizedDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description localizations
|
||||
*
|
||||
* @param localizedDescriptions The dictionary of localized descriptions to set
|
||||
*/
|
||||
public setDescriptionLocalizations(localizedDescriptions: LocalizationMap | null) {
|
||||
if (localizedDescriptions === null) {
|
||||
Reflect.set(this, 'description_localizations', null);
|
||||
return this;
|
||||
}
|
||||
|
||||
Reflect.set(this, 'description_localizations', {});
|
||||
Object.entries(localizedDescriptions).forEach((args) =>
|
||||
this.setDescriptionLocalization(...(args as [LocaleString, string | null])),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions';
|
||||
import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase';
|
||||
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions';
|
||||
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder';
|
||||
import { SlashCommandAttachmentOption } from '../options/attachment';
|
||||
import { SlashCommandBooleanOption } from '../options/boolean';
|
||||
import { SlashCommandChannelOption } from '../options/channel';
|
||||
import { SlashCommandIntegerOption } from '../options/integer';
|
||||
@@ -8,7 +10,6 @@ import { SlashCommandNumberOption } from '../options/number';
|
||||
import { SlashCommandRoleOption } from '../options/role';
|
||||
import { SlashCommandStringOption } from '../options/string';
|
||||
import { SlashCommandUserOption } from '../options/user';
|
||||
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder';
|
||||
|
||||
export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
public readonly options!: ToAPIApplicationCommandOptions[];
|
||||
@@ -53,6 +54,17 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandRoleOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an attachment option
|
||||
*
|
||||
* @param input A function that returns an option builder, or an already built builder
|
||||
*/
|
||||
public addAttachmentOption(
|
||||
input: SlashCommandAttachmentOption | ((builder: SlashCommandAttachmentOption) => SlashCommandAttachmentOption),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandAttachmentOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mentionable option
|
||||
*
|
||||
@@ -73,13 +85,13 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
input:
|
||||
| SlashCommandStringOption
|
||||
| Omit<SlashCommandStringOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandStringOption, 'addChoice' | 'addChoices'>
|
||||
| Omit<SlashCommandStringOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandStringOption,
|
||||
) =>
|
||||
| SlashCommandStringOption
|
||||
| Omit<SlashCommandStringOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandStringOption, 'addChoice' | 'addChoices'>),
|
||||
| Omit<SlashCommandStringOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandStringOption);
|
||||
}
|
||||
@@ -93,13 +105,13 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
input:
|
||||
| SlashCommandIntegerOption
|
||||
| Omit<SlashCommandIntegerOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandIntegerOption, 'addChoice' | 'addChoices'>
|
||||
| Omit<SlashCommandIntegerOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandIntegerOption,
|
||||
) =>
|
||||
| SlashCommandIntegerOption
|
||||
| Omit<SlashCommandIntegerOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandIntegerOption, 'addChoice' | 'addChoices'>),
|
||||
| Omit<SlashCommandIntegerOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandIntegerOption);
|
||||
}
|
||||
@@ -113,13 +125,13 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
input:
|
||||
| SlashCommandNumberOption
|
||||
| Omit<SlashCommandNumberOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandNumberOption, 'addChoice' | 'addChoices'>
|
||||
| Omit<SlashCommandNumberOption, 'addChoices'>
|
||||
| ((
|
||||
builder: SlashCommandNumberOption,
|
||||
) =>
|
||||
| SlashCommandNumberOption
|
||||
| Omit<SlashCommandNumberOption, 'setAutocomplete'>
|
||||
| Omit<SlashCommandNumberOption, 'addChoice' | 'addChoices'>),
|
||||
| Omit<SlashCommandNumberOption, 'addChoices'>),
|
||||
) {
|
||||
return this._sharedAddOptionMethod(input, SlashCommandNumberOption);
|
||||
}
|
||||
@@ -128,8 +140,8 @@ export class SharedSlashCommandOptions<ShouldOmitSubcommandFunctions = true> {
|
||||
input:
|
||||
| T
|
||||
| Omit<T, 'setAutocomplete'>
|
||||
| Omit<T, 'addChoice' | 'addChoices'>
|
||||
| ((builder: T) => T | Omit<T, 'setAutocomplete'> | Omit<T, 'addChoice' | 'addChoices'>),
|
||||
| Omit<T, 'addChoices'>
|
||||
| ((builder: T) => T | Omit<T, 'setAutocomplete'> | Omit<T, 'addChoices'>),
|
||||
Instance: new () => T,
|
||||
): ShouldOmitSubcommandFunctions extends true ? Omit<this, 'addSubcommand' | 'addSubcommandGroup'> : this {
|
||||
const { options } = this;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { APIApplicationCommandAttachmentOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandAttachmentOption extends ApplicationCommandOptionBase {
|
||||
public override readonly type = ApplicationCommandOptionType.Attachment as const;
|
||||
|
||||
public toJSON(): APIApplicationCommandAttachmentOption {
|
||||
this.runRequiredValidations();
|
||||
|
||||
return { ...this };
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandBooleanOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandBooleanOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandBooleanOption extends ApplicationCommandOptionBase {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandChannelOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandChannelOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { z } from 'zod';
|
||||
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
const numberValidator = z.number().int().nonnegative();
|
||||
const numberValidator = s.number.int;
|
||||
|
||||
@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
|
||||
export class SlashCommandIntegerOption
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandMentionableOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandMentionableOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandMentionableOption extends ApplicationCommandOptionBase {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { z } from 'zod';
|
||||
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
const numberValidator = z.number().nonnegative();
|
||||
const numberValidator = s.number;
|
||||
|
||||
@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
|
||||
export class SlashCommandNumberOption
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandRoleOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandRoleOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandRoleOption extends ApplicationCommandOptionBase {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandStringOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandStringOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { mix } from 'ts-mixer';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIApplicationCommandUserOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
|
||||
import { APIApplicationCommandUserOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
|
||||
|
||||
export class SlashCommandUserOption extends ApplicationCommandOptionBase {
|
||||
|
||||
@@ -1,36 +1,57 @@
|
||||
import type { APIEmbedField } from 'discord-api-types/v9';
|
||||
import { z } from 'zod';
|
||||
import { s } from '@sapphire/shapeshift';
|
||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||
|
||||
export const fieldNamePredicate = z.string().min(1).max(256);
|
||||
export const fieldNamePredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(256);
|
||||
|
||||
export const fieldValuePredicate = z.string().min(1).max(1024);
|
||||
export const fieldValuePredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(1024);
|
||||
|
||||
export const fieldInlinePredicate = z.boolean().optional();
|
||||
export const fieldInlinePredicate = s.boolean.optional;
|
||||
|
||||
export const embedFieldPredicate = z.object({
|
||||
export const embedFieldPredicate = s.object({
|
||||
name: fieldNamePredicate,
|
||||
value: fieldValuePredicate,
|
||||
inline: fieldInlinePredicate,
|
||||
});
|
||||
|
||||
export const embedFieldsArrayPredicate = embedFieldPredicate.array();
|
||||
export const embedFieldsArrayPredicate = embedFieldPredicate.array;
|
||||
|
||||
export const fieldLengthPredicate = z.number().lte(25);
|
||||
export const fieldLengthPredicate = s.number.lessThanOrEqual(25);
|
||||
|
||||
export function validateFieldLength(fields: APIEmbedField[], amountAdding: number): void {
|
||||
fieldLengthPredicate.parse(fields.length + amountAdding);
|
||||
export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void {
|
||||
fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding);
|
||||
}
|
||||
|
||||
export const authorNamePredicate = fieldNamePredicate.nullable();
|
||||
export const authorNamePredicate = fieldNamePredicate.nullable;
|
||||
|
||||
export const urlPredicate = z.string().url().nullish();
|
||||
export const imageURLPredicate = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:', 'attachment:'],
|
||||
}).nullish;
|
||||
|
||||
export const colorPredicate = z.number().gte(0).lte(0xffffff).nullable();
|
||||
export const urlPredicate = s.string.url({
|
||||
allowedProtocols: ['http:', 'https:'],
|
||||
}).nullish;
|
||||
|
||||
export const descriptionPredicate = z.string().min(1).max(4096).nullable();
|
||||
export const embedAuthorPredicate = s.object({
|
||||
name: authorNamePredicate,
|
||||
iconURL: imageURLPredicate,
|
||||
url: urlPredicate,
|
||||
});
|
||||
|
||||
export const footerTextPredicate = z.string().min(1).max(2048).nullable();
|
||||
export const RGBPredicate = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(255);
|
||||
export const colorPredicate = s.number.int
|
||||
.greaterThanOrEqual(0)
|
||||
.lessThanOrEqual(0xffffff)
|
||||
.or(s.tuple([RGBPredicate, RGBPredicate, RGBPredicate])).nullable;
|
||||
|
||||
export const timestampPredicate = z.union([z.number(), z.date()]).nullable();
|
||||
export const descriptionPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(4096).nullable;
|
||||
|
||||
export const titlePredicate = fieldNamePredicate.nullable();
|
||||
export const footerTextPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(2048).nullable;
|
||||
|
||||
export const embedFooterPredicate = s.object({
|
||||
text: footerTextPredicate,
|
||||
iconURL: imageURLPredicate,
|
||||
});
|
||||
|
||||
export const timestampPredicate = s.union(s.number, s.date).nullable;
|
||||
|
||||
export const titlePredicate = fieldNamePredicate.nullable;
|
||||
|
||||
@@ -1,326 +1,92 @@
|
||||
import type {
|
||||
APIEmbed,
|
||||
APIEmbedAuthor,
|
||||
APIEmbedField,
|
||||
APIEmbedFooter,
|
||||
APIEmbedImage,
|
||||
APIEmbedProvider,
|
||||
APIEmbedThumbnail,
|
||||
APIEmbedVideo,
|
||||
} from 'discord-api-types/v9';
|
||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||
import {
|
||||
authorNamePredicate,
|
||||
colorPredicate,
|
||||
descriptionPredicate,
|
||||
embedAuthorPredicate,
|
||||
embedFieldsArrayPredicate,
|
||||
fieldInlinePredicate,
|
||||
fieldNamePredicate,
|
||||
fieldValuePredicate,
|
||||
footerTextPredicate,
|
||||
embedFooterPredicate,
|
||||
imageURLPredicate,
|
||||
timestampPredicate,
|
||||
titlePredicate,
|
||||
urlPredicate,
|
||||
validateFieldLength,
|
||||
} from './Assertions';
|
||||
|
||||
export interface AuthorOptions {
|
||||
name: string;
|
||||
url?: string;
|
||||
iconURL?: string;
|
||||
}
|
||||
|
||||
export interface FooterOptions {
|
||||
text: string;
|
||||
iconURL?: string;
|
||||
}
|
||||
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbedBuilder } from './UnsafeEmbed';
|
||||
|
||||
/**
|
||||
* Represents an embed in a message (image/video preview, rich embed, etc.)
|
||||
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
|
||||
*/
|
||||
export class Embed implements APIEmbed {
|
||||
/**
|
||||
* An array of fields of this embed
|
||||
*/
|
||||
public fields: APIEmbedField[];
|
||||
|
||||
/**
|
||||
* The embed title
|
||||
*/
|
||||
public title?: string;
|
||||
|
||||
/**
|
||||
* The embed description
|
||||
*/
|
||||
public description?: string;
|
||||
|
||||
/**
|
||||
* The embed url
|
||||
*/
|
||||
public url?: string;
|
||||
|
||||
/**
|
||||
* The embed color
|
||||
*/
|
||||
public color?: number;
|
||||
|
||||
/**
|
||||
* The timestamp of the embed in the ISO format
|
||||
*/
|
||||
public timestamp?: string;
|
||||
|
||||
/**
|
||||
* The embed thumbnail data
|
||||
*/
|
||||
public thumbnail?: APIEmbedThumbnail;
|
||||
|
||||
/**
|
||||
* The embed image data
|
||||
*/
|
||||
public image?: APIEmbedImage;
|
||||
|
||||
/**
|
||||
* Received video data
|
||||
*/
|
||||
public video?: APIEmbedVideo;
|
||||
|
||||
/**
|
||||
* The embed author data
|
||||
*/
|
||||
public author?: APIEmbedAuthor;
|
||||
|
||||
/**
|
||||
* Received data about the embed provider
|
||||
*/
|
||||
public provider?: APIEmbedProvider;
|
||||
|
||||
/**
|
||||
* The embed footer data
|
||||
*/
|
||||
public footer?: APIEmbedFooter;
|
||||
|
||||
public constructor(data: APIEmbed = {}) {
|
||||
this.title = data.title;
|
||||
this.description = data.description;
|
||||
this.url = data.url;
|
||||
this.color = data.color;
|
||||
this.thumbnail = data.thumbnail;
|
||||
this.image = data.image;
|
||||
this.video = data.video;
|
||||
this.author = data.author;
|
||||
this.provider = data.provider;
|
||||
this.footer = data.footer;
|
||||
this.fields = data.fields ?? [];
|
||||
|
||||
if (data.timestamp) this.timestamp = new Date(data.timestamp).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The accumulated length for the embed title, description, fields, footer text, and author name
|
||||
*/
|
||||
public get length(): number {
|
||||
return (
|
||||
(this.title?.length ?? 0) +
|
||||
(this.description?.length ?? 0) +
|
||||
this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) +
|
||||
(this.footer?.text.length ?? 0) +
|
||||
(this.author?.name.length ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a field to the embed (max 25)
|
||||
*
|
||||
* @param field The field to add.
|
||||
*/
|
||||
public addField(field: APIEmbedField): this {
|
||||
return this.addFields(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields to the embed (max 25)
|
||||
*
|
||||
* @param fields The fields to add
|
||||
*/
|
||||
public addFields(...fields: APIEmbedField[]): this {
|
||||
// Data assertions
|
||||
embedFieldsArrayPredicate.parse(fields);
|
||||
|
||||
export class EmbedBuilder extends UnsafeEmbedBuilder {
|
||||
public override addFields(fields: APIEmbedField[]): this {
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(this.fields, fields.length);
|
||||
validateFieldLength(fields.length, this.data.fields);
|
||||
|
||||
this.fields.push(...Embed.normalizeFields(...fields));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts fields in the embed (max 25)
|
||||
*
|
||||
* @param index The index to start at
|
||||
* @param deleteCount The number of fields to remove
|
||||
* @param fields The replacing field objects
|
||||
*/
|
||||
public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
|
||||
// Data assertions
|
||||
embedFieldsArrayPredicate.parse(fields);
|
||||
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(this.fields, fields.length - deleteCount);
|
||||
|
||||
this.fields.splice(index, deleteCount, ...Embed.normalizeFields(...fields));
|
||||
return this;
|
||||
return super.addFields(embedFieldsArrayPredicate.parse(fields));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the author of this embed
|
||||
*
|
||||
* @param options The options for the author
|
||||
*/
|
||||
public setAuthor(options: AuthorOptions | null): this {
|
||||
public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
|
||||
// Ensure adding these fields won't exceed the 25 field limit
|
||||
validateFieldLength(fields.length - deleteCount, this.data.fields);
|
||||
|
||||
// Data assertions
|
||||
return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields));
|
||||
}
|
||||
|
||||
public override setAuthor(options: EmbedAuthorOptions | null): this {
|
||||
if (options === null) {
|
||||
this.author = undefined;
|
||||
return this;
|
||||
return super.setAuthor(null);
|
||||
}
|
||||
|
||||
const { name, iconURL, url } = options;
|
||||
// Data assertions
|
||||
authorNamePredicate.parse(name);
|
||||
urlPredicate.parse(iconURL);
|
||||
urlPredicate.parse(url);
|
||||
embedAuthorPredicate.parse(options);
|
||||
|
||||
this.author = { name, url, icon_url: iconURL };
|
||||
return this;
|
||||
return super.setAuthor(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of this embed
|
||||
*
|
||||
* @param color The color of the embed
|
||||
*/
|
||||
public setColor(color: number | null): this {
|
||||
public override setColor(color: number | RGBTuple | null): this {
|
||||
// Data assertions
|
||||
colorPredicate.parse(color);
|
||||
|
||||
this.color = color ?? undefined;
|
||||
return this;
|
||||
return super.setColor(colorPredicate.parse(color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this embed
|
||||
*
|
||||
* @param description The description
|
||||
*/
|
||||
public setDescription(description: string | null): this {
|
||||
public override setDescription(description: string | null): this {
|
||||
// Data assertions
|
||||
descriptionPredicate.parse(description);
|
||||
|
||||
this.description = description ?? undefined;
|
||||
return this;
|
||||
return super.setDescription(descriptionPredicate.parse(description));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the footer of this embed
|
||||
*
|
||||
* @param options The options for the footer
|
||||
*/
|
||||
public setFooter(options: FooterOptions | null): this {
|
||||
public override setFooter(options: EmbedFooterOptions | null): this {
|
||||
if (options === null) {
|
||||
this.footer = undefined;
|
||||
return this;
|
||||
return super.setFooter(null);
|
||||
}
|
||||
|
||||
const { text, iconURL } = options;
|
||||
// Data assertions
|
||||
footerTextPredicate.parse(text);
|
||||
urlPredicate.parse(iconURL);
|
||||
embedFooterPredicate.parse(options);
|
||||
|
||||
this.footer = { text, icon_url: iconURL };
|
||||
return this;
|
||||
return super.setFooter(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image of this embed
|
||||
*
|
||||
* @param url The URL of the image
|
||||
*/
|
||||
public setImage(url: string | null): this {
|
||||
public override setImage(url: string | null): this {
|
||||
// Data assertions
|
||||
urlPredicate.parse(url);
|
||||
|
||||
this.image = url ? { url } : undefined;
|
||||
return this;
|
||||
return super.setImage(imageURLPredicate.parse(url)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the thumbnail of this embed
|
||||
*
|
||||
* @param url The URL of the thumbnail
|
||||
*/
|
||||
public setThumbnail(url: string | null): this {
|
||||
public override setThumbnail(url: string | null): this {
|
||||
// Data assertions
|
||||
urlPredicate.parse(url);
|
||||
|
||||
this.thumbnail = url ? { url } : undefined;
|
||||
return this;
|
||||
return super.setThumbnail(imageURLPredicate.parse(url)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timestamp of this embed
|
||||
*
|
||||
* @param timestamp The timestamp or date
|
||||
*/
|
||||
public setTimestamp(timestamp: number | Date | null = Date.now()): this {
|
||||
public override setTimestamp(timestamp: number | Date | null = Date.now()): this {
|
||||
// Data assertions
|
||||
timestampPredicate.parse(timestamp);
|
||||
|
||||
this.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined;
|
||||
return this;
|
||||
return super.setTimestamp(timestampPredicate.parse(timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of this embed
|
||||
*
|
||||
* @param title The title
|
||||
*/
|
||||
public setTitle(title: string | null): this {
|
||||
public override setTitle(title: string | null): this {
|
||||
// Data assertions
|
||||
titlePredicate.parse(title);
|
||||
|
||||
this.title = title ?? undefined;
|
||||
return this;
|
||||
return super.setTitle(titlePredicate.parse(title));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL of this embed
|
||||
*
|
||||
* @param url The URL
|
||||
*/
|
||||
public setURL(url: string | null): this {
|
||||
public override setURL(url: string | null): this {
|
||||
// Data assertions
|
||||
urlPredicate.parse(url);
|
||||
|
||||
this.url = url ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the embed to a plain object
|
||||
*/
|
||||
public toJSON(): APIEmbed {
|
||||
return { ...this };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes field input and resolves strings
|
||||
*
|
||||
* @param fields Fields to normalize
|
||||
*/
|
||||
public static normalizeFields(...fields: APIEmbedField[]): APIEmbedField[] {
|
||||
return fields.flat(Infinity).map((field) => {
|
||||
fieldNamePredicate.parse(field.name);
|
||||
fieldValuePredicate.parse(field.value);
|
||||
fieldInlinePredicate.parse(field.inline);
|
||||
|
||||
return { name: field.name, value: field.value, inline: field.inline ?? undefined };
|
||||
});
|
||||
return super.setURL(urlPredicate.parse(url)!);
|
||||
}
|
||||
}
|
||||
|
||||
186
packages/builders/src/messages/embed/UnsafeEmbed.ts
Normal file
186
packages/builders/src/messages/embed/UnsafeEmbed.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v10';
|
||||
|
||||
export type RGBTuple = [red: number, green: number, blue: number];
|
||||
|
||||
export interface IconData {
|
||||
/**
|
||||
* The URL of the icon
|
||||
*/
|
||||
iconURL?: string;
|
||||
/**
|
||||
* The proxy URL of the icon
|
||||
*/
|
||||
proxyIconURL?: string;
|
||||
}
|
||||
|
||||
export type EmbedAuthorData = Omit<APIEmbedAuthor, 'icon_url' | 'proxy_icon_url'> & IconData;
|
||||
|
||||
export type EmbedAuthorOptions = Omit<EmbedAuthorData, 'proxyIconURL'>;
|
||||
|
||||
export type EmbedFooterData = Omit<APIEmbedFooter, 'icon_url' | 'proxy_icon_url'> & IconData;
|
||||
|
||||
export type EmbedFooterOptions = Omit<EmbedFooterData, 'proxyIconURL'>;
|
||||
|
||||
export interface EmbedImageData extends Omit<APIEmbedImage, 'proxy_url'> {
|
||||
/**
|
||||
* The proxy URL for the image
|
||||
*/
|
||||
proxyURL?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a non-validated embed in a message (image/video preview, rich embed, etc.)
|
||||
*/
|
||||
export class UnsafeEmbedBuilder {
|
||||
public readonly data: APIEmbed;
|
||||
|
||||
public constructor(data: APIEmbed = {}) {
|
||||
this.data = { ...data };
|
||||
if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields to the embed (max 25)
|
||||
*
|
||||
* @param fields The fields to add
|
||||
*/
|
||||
public addFields(fields: APIEmbedField[]): this {
|
||||
if (this.data.fields) this.data.fields.push(...fields);
|
||||
else this.data.fields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes, replaces, or inserts fields in the embed (max 25)
|
||||
*
|
||||
* @param index The index to start at
|
||||
* @param deleteCount The number of fields to remove
|
||||
* @param fields The replacing field objects
|
||||
*/
|
||||
public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
|
||||
if (this.data.fields) this.data.fields.splice(index, deleteCount, ...fields);
|
||||
else this.data.fields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the embed's fields (max 25).
|
||||
* @param fields The fields to set
|
||||
*/
|
||||
public setFields(fields: APIEmbedField[]) {
|
||||
this.spliceFields(0, this.data.fields?.length ?? 0, ...fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the author of this embed
|
||||
*
|
||||
* @param options The options for the author
|
||||
*/
|
||||
public setAuthor(options: EmbedAuthorOptions | null): this {
|
||||
if (options === null) {
|
||||
this.data.author = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.author = { name: options.name, url: options.url, icon_url: options.iconURL };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color of this embed
|
||||
*
|
||||
* @param color The color of the embed
|
||||
*/
|
||||
public setColor(color: number | RGBTuple | null): this {
|
||||
if (Array.isArray(color)) {
|
||||
const [red, green, blue] = color;
|
||||
this.data.color = (red << 16) + (green << 8) + blue;
|
||||
return this;
|
||||
}
|
||||
this.data.color = color ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this embed
|
||||
*
|
||||
* @param description The description
|
||||
*/
|
||||
public setDescription(description: string | null): this {
|
||||
this.data.description = description ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the footer of this embed
|
||||
*
|
||||
* @param options The options for the footer
|
||||
*/
|
||||
public setFooter(options: EmbedFooterOptions | null): this {
|
||||
if (options === null) {
|
||||
this.data.footer = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
this.data.footer = { text: options.text, icon_url: options.iconURL };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image of this embed
|
||||
*
|
||||
* @param url The URL of the image
|
||||
*/
|
||||
public setImage(url: string | null): this {
|
||||
this.data.image = url ? { url } : undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the thumbnail of this embed
|
||||
*
|
||||
* @param url The URL of the thumbnail
|
||||
*/
|
||||
public setThumbnail(url: string | null): this {
|
||||
this.data.thumbnail = url ? { url } : undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timestamp of this embed
|
||||
*
|
||||
* @param timestamp The timestamp or date
|
||||
*/
|
||||
public setTimestamp(timestamp: number | Date | null = Date.now()): this {
|
||||
this.data.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of this embed
|
||||
*
|
||||
* @param title The title
|
||||
*/
|
||||
public setTitle(title: string | null): this {
|
||||
this.data.title = title ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URL of this embed
|
||||
*
|
||||
* @param url The URL
|
||||
*/
|
||||
public setURL(url: string | null): this {
|
||||
this.data.url = url ?? undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the embed to a plain object
|
||||
*/
|
||||
public toJSON(): APIEmbed {
|
||||
return { ...this.data };
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Snowflake } from 'discord-api-types/globals';
|
||||
import type { URL } from 'url';
|
||||
import type { Snowflake } from 'discord-api-types/globals';
|
||||
|
||||
/**
|
||||
* Wraps the content inside a codeblock with no language
|
||||
@@ -164,15 +164,6 @@ export function userMention<C extends Snowflake>(userId: C): `<@${C}>` {
|
||||
return `<@${userId}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a user ID into a member-nickname mention
|
||||
*
|
||||
* @param memberId The user ID to format
|
||||
*/
|
||||
export function memberNicknameMention<C extends Snowflake>(memberId: C): `<@!${C}>` {
|
||||
return `<@!${memberId}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a channel ID into a channel mention
|
||||
*
|
||||
|
||||
11
packages/builders/src/util/componentUtil.ts
Normal file
11
packages/builders/src/util/componentUtil.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { APIEmbed } from 'discord-api-types/v10';
|
||||
|
||||
export function embedLength(data: APIEmbed) {
|
||||
return (
|
||||
(data.title?.length ?? 0) +
|
||||
(data.description?.length ?? 0) +
|
||||
(data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) +
|
||||
(data.footer?.text.length ?? 0) +
|
||||
(data.author?.name.length ?? 0)
|
||||
);
|
||||
}
|
||||
14
packages/builders/src/util/equatable.ts
Normal file
14
packages/builders/src/util/equatable.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface Equatable<T> {
|
||||
/**
|
||||
* Whether or not this is equal to another structure
|
||||
*/
|
||||
equals: (other: T) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if an object is equatable or not.
|
||||
* @param maybeEquatable The object to check against
|
||||
*/
|
||||
export function isEquatable(maybeEquatable: unknown): maybeEquatable is Equatable<unknown> {
|
||||
return maybeEquatable !== null && typeof maybeEquatable === 'object' && 'equals' in maybeEquatable;
|
||||
}
|
||||
14
packages/builders/src/util/jsonEncodable.ts
Normal file
14
packages/builders/src/util/jsonEncodable.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface JSONEncodable<T> {
|
||||
/**
|
||||
* Transforms this object to its JSON format
|
||||
*/
|
||||
toJSON: () => T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if an object is encodable or not.
|
||||
* @param maybeEncodable The object to check against
|
||||
*/
|
||||
export function isJSONEncodable(maybeEncodable: unknown): maybeEncodable is JSONEncodable<unknown> {
|
||||
return maybeEncodable !== null && typeof maybeEncodable === 'object' && 'toJSON' in maybeEncodable;
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
import type { Options } from 'tsup';
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export const tsup: Options = {
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
dts: true,
|
||||
entryPoints: ['src/index.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
minify: true,
|
||||
minify: false,
|
||||
keepNames: true,
|
||||
skipNodeModulesBundle: true,
|
||||
sourcemap: true,
|
||||
target: 'es2021',
|
||||
};
|
||||
esbuildOptions: (options, context) => {
|
||||
if (context.format === 'cjs') {
|
||||
options.banner = {
|
||||
js: '"use strict";',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user