Compare commits

...

106 Commits

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

* fix: enums

* fix: prettier

* feat: add zombie connection event to shard events

* Apply suggestions from code review

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

* fix: prettier

* fix: handleZombieConnection

* feat: backport new logic of handling zombie connection

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

* types(MessageOptions): add AttachmentPayload
2022-06-06 10:07:15 +02:00
markisha64
b9df37a89f refact: Sticker's tags property (#8010) 2022-06-05 23:29:31 +02:00
Rodry
ad75be9a9c feat: allow builders to accept rest params and arrays (#7874)
Co-authored-by: Parbez <imranbarbhuiya.fsd@gmail.com>
Co-authored-by: Khafra <42794878+KhafraDev@users.noreply.github.com>
2022-06-05 23:29:16 +02:00
Suneet Tipirneni
70c733bb9a refactor(channel): remove redundant channel type guards (#8012) 2022-06-05 23:29:05 +02:00
Rodry
a3287782b5 feat(MessageReaction): add react method (#7810) 2022-06-05 23:28:44 +02:00
Suneet Tipirneni
0ccc243c8f types(modal): fix showModal() typings (#8014) 2022-06-05 23:28:28 +02:00
Almeida
0a7953e463 docs(Attachment): remove constructor doc (#8009) 2022-06-05 23:28:18 +02:00
Parbez
6aa623240e types: fix some attachment related typings (#8013) 2022-06-05 22:37:56 +02:00
n1ck_pro
6266b0c1e3 types(AttachmentBuilder): remove name parameter from constructor (#8008) 2022-06-05 22:37:01 +02:00
Suneet Tipirneni
9720e55534 refactor: always return Message instances in interactions (#7917)
Co-authored-by: Almeida <almeidx@pm.me>
2022-06-05 19:00:35 +02:00
Parbez
ad36c0be77 fix: add static method from in builders (#7990) 2022-06-05 18:33:19 +02:00
SpaceEEC
5987dbe5cf docs(VoiceChannel): annotate that it is implementing TextBasedChannel (#8007) 2022-06-05 18:30:14 +02:00
Rodry
5244fe3c1c feat(Collector): add ignore event (#7644)
Co-authored-by: Parbez <imranbarbhuiya.fsd@gmail.com>
2022-06-05 18:29:13 +02:00
Synbulat Biishev
349766dd69 feat(GuildMemberManager): add GuildMemberManager#fetchMe() (#7526) 2022-06-05 18:29:00 +02:00
markisha64
7a1095b66b fix: typings (#7965)
Co-authored-by: Almeida <almeidx@pm.me>
2022-06-05 09:36:59 +02:00
iCrawl
10009a43ee ci: remove cache-dir from test 2022-06-05 01:14:39 +02:00
iCrawl
8d8e6c03de feat: use vitest instead of jest for more speed 2022-06-05 01:07:10 +02:00
Suneet Tipirneni
dfadcbc2fd refactor(attachment): don't return attachment builders from API (#7852)
Co-authored-by: Almeida <almeidx@pm.me>
Co-authored-by: A. Román <kyradiscord@gmail.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2022-06-04 22:33:13 +02:00
n1ck_pro
546d48655f docs: add missing discord-api-types external types (#8001) 2022-06-04 22:27:01 +02:00
iCrawl
f8739bd9c0 fix(voice): re-add accidental removal of postbuild script 2022-06-04 19:15:28 +02:00
iCrawl
f2ae1f9348 feat: add scripts package for locally used scripts 2022-06-04 19:07:50 +02:00
iCrawl
3401fd4eb6 chore: remove leftover files from pre-monorepo era 2022-06-04 18:41:37 +02:00
iCrawl
fdbd229832 ci: equality check on tag and package name 2022-06-04 17:57:07 +02:00
iCrawl
9e8e2411c1 ci: check for the correct prop to be defined 2022-06-04 17:51:31 +02:00
iCrawl
c02ced9a22 ci: fix if checks for branch/tag when using workflow dispatch 2022-06-04 17:37:00 +02:00
iCrawl
8c5a7f80ef ci: fix inputs for workflow dispatches 2022-06-04 17:15:08 +02:00
iCrawl
14018b0118 ci: allow workflow dispatches 2022-06-04 17:13:40 +02:00
iCrawl
8095723604 chore: only build cjs for actions 2022-06-04 17:08:26 +02:00
iCrawl
e17bb54c85 chore: update dev versions 2022-06-04 17:01:47 +02:00
iCrawl
e92d937bcc chore: release packages 2022-06-04 16:59:00 +02:00
iCrawl
091f1bd170 chore: deps 2022-06-04 16:49:47 +02:00
iCrawl
bd2cf20ecb chore: re-generate changelogs 2022-06-04 16:42:00 +02:00
iCrawl
8af916dba0 chore: remove accidental changelog for voice 2022-06-04 16:22:31 +02:00
iCrawl
781a6e013c chore: fix changelogs 2022-06-04 16:20:33 +02:00
iCrawl
2172a00c50 chore: remove unneeded actions dep 2022-06-04 15:45:18 +02:00
iCrawl
271b1c8e5d feat: actions for workflows 2022-06-04 15:34:40 +02:00
iCrawl
6b8ef20cb3 style: cleanup tests and tsup configs 2022-06-04 15:21:57 +02:00
iCrawl
a45bef4cad fix(proxy): add docs script 2022-06-04 13:34:57 +02:00
DD
1ba2d2a898 feat: @discordjs/proxy (#7925)
Co-authored-by: Parbez <imranbarbhuiya.fsd@gmail.com>
2022-06-04 13:26:25 +02:00
iCrawl
e518c8a137 chore: update yarn 2022-06-04 13:13:52 +02:00
iCrawl
80cd4a4a43 ci: run assigning reviewers also on drafts 2022-06-04 12:59:54 +02:00
iCrawl
8a7cd10554 fix(builders): leftover invalid null type 2022-06-04 12:59:26 +02:00
iCrawl
251862ea57 chore: deps 2022-06-04 12:58:09 +02:00
Jiralite
2f03f9ad3f fix(GuildAuditLogs): Cache guild scheduled events (#7951) 2022-06-04 09:04:08 +02:00
Suneet Tipirneni
db81127886 chore: add external links for dapi-types (#7966) 2022-06-04 09:03:05 +02:00
Suneet Tipirneni
cdd2ba036a feat(guildChannelManager): add videoQualityMode option for create() (#7980)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2022-06-04 09:02:21 +02:00
Suneet Tipirneni
97eaab35d7 feat(rest): add guild member banner cdn url (#7973) 2022-06-04 09:00:39 +02:00
Suneet Tipirneni
d60c464e61 types: add types to EventEmitter static methods (#7986) 2022-06-04 08:58:15 +02:00
Suneet Tipirneni
643dab3b1b refactor: clean up modal submissions (#7994)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2022-06-04 08:57:19 +02:00
Azarias B
68d5169f66 feat: Export types from interactions/slashCommands/mixins (#7942) 2022-05-25 10:04:20 +02:00
Suneet Tipirneni
7f4d411e73 chore: reexport all dapi-types (#7909) 2022-05-25 10:03:51 +02:00
Jeroen Claassens
636d4f263b chore: fix git cliff configs (#7948)
Co-authored-by: Vlad <kingdgrizzle@gmail.com>
2022-05-25 10:02:17 +02:00
Jiralite
fdeac9d9fb types: nullify guildScheduledEventUpdate's old parameter (#7955) 2022-05-21 02:02:57 +02:00
Suneet Tipirneni
adf461baf4 fix: make sure action row builders create djs builders (#7945) 2022-05-18 21:56:56 +02:00
DD
191510b7f8 fix(TextBasedChannel#bulkDelete): return deleted message (#7943) 2022-05-18 19:58:39 +02:00
DD
e2f5a4a494 chore: enable noUncheckedIndexAccess (#7931) 2022-05-18 19:56:42 +02:00
A. Román
993eb74475 chore(deps): update discord-api-types and /voice (#7934) 2022-05-18 19:54:35 +02:00
DD
dfe449c253 feat: REST#raw (#7929) 2022-05-17 16:31:19 +02:00
Jiralite
5e9b757a37 fix: Remove trailing invites on channel deletion (#7932) 2022-05-16 12:13:02 +02:00
Sam
28172ca7b5 fix(DataResolver): fix check for readable streams (#7928) 2022-05-16 12:12:34 +02:00
Vlad Frangu
7ce641d33a fix(SlashCommandBuilder): import Permissions correctly (#7921) 2022-05-14 22:47:26 +02:00
Khafra
e92b17d855 fix(REST): remove dom types (#7922) 2022-05-14 18:39:12 +02:00
Khafra
d1504f2ae1 fix: ok statusCode can be 200..299 (#7919) 2022-05-14 18:38:05 +02:00
Jiralite
b1a3aa97ea chore: bump discord-api-types (#7923) 2022-05-14 18:37:01 +02:00
iCrawl
d522320aa4 chore: update v13 changelog 2022-05-13 12:11:36 +02:00
Khafra
76694c1497 feat(EnumResolvers): remove Enumresolvers (#7876) 2022-05-13 11:35:08 +02:00
Vlad Frangu
de3f1573f0 feat(builders): add new command permissions v2 (#7861) 2022-05-13 11:33:40 +02:00
Synbulat Biishev
aed687b09f feat: move me to GuildMemberManager manager (#7669)
Co-authored-by: muchnameless <12682826+muchnameless@users.noreply.github.com>
Co-authored-by: Almeida <almeidx@pm.me>
2022-05-12 22:51:28 +02:00
n1ck_pro
e07b910e68 types: make CacheType generic more accurate for return values (#7868) 2022-05-12 22:50:31 +02:00
Khafra
d1ec8c37ff feat(rest): use undici (#7747)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: ckohen <chaikohen@gmail.com>
2022-05-12 22:49:15 +02:00
Parbez
4515a1ea80 chore: bump shapeshift for faster object validation (#7914) 2022-05-12 12:09:50 +02:00
Vlad Frangu
c1b5e731da fix: add localizations for subcommand builders and option choices (#7862) 2022-05-12 10:32:27 +02:00
Suneet Tipirneni
64bdf53116 types(guildScheduledEvent): Mark entityMetadata as nullable (#7908) 2022-05-12 10:30:26 +02:00
Ben
d308c66eec docs(InteractionResponses): replace outdated Embed example for reply (#7875) 2022-05-12 10:29:44 +02:00
Jiralite
dfd9eb20b2 refactor(ThreadChannel): Remove MAX helper from threads (#7846) 2022-05-12 10:29:21 +02:00
Ryan Munro
19eaed6390 fix(AuditLog): default changes to empty array (#7880) 2022-05-12 10:26:07 +02:00
SpaceEEC
4ba0f56b6a feat(VoiceChannel): add support for text in voice (#6921) 2022-05-02 09:39:14 +02:00
advaith
5eeef3f708 types(discord.js): export missing enums (#7864) 2022-05-02 09:37:46 +02:00
with-heart
679dcda970 feat(REST): enable setting default authPrefix (#7853) 2022-04-28 13:08:02 +02:00
newracket
df64d3ea38 fix(Util): flatten ignoring certain fields (#7773) 2022-04-26 00:45:52 +02:00
Jiralite
6239d83c4d fix: possibly missing (#7829) 2022-04-26 00:31:21 +02:00
Khafra
0c18dab128 fix: endReason not being properly set in base Collector (#7833)
* fix: `endReason` not being properly set in base Collector

* types: add _endReason type
2022-04-26 00:31:12 +02:00
Almeida
ece628986c types: fix return type of toString() on channels (#7836)
* types(DMChannel): `toString()` returns a `UserMention`

* test: add more tests
2022-04-26 00:31:05 +02:00
Jiralite
f4ccc6772c docs: require parameter (#7838) 2022-04-26 00:30:58 +02:00
Almeida
5ba7740fcf refactor(Activity): remove undocumented properties (#7844) 2022-04-26 00:30:45 +02:00
Almeida
bfeaf856f7 types(Message#activity): make partyId optional and use enum for type (#7845) 2022-04-26 00:30:13 +02:00
FIRE
361709332b fix(SelectMenuBuilder): options array (#7826) 2022-04-22 20:50:36 +02:00
Rodry
61a44c509c docs(ApplicationCommand): fix and improve localization docs (#7804) 2022-04-21 19:07:49 +02:00
Hyro
4ac91c61d0 fix(Activity): platform type (#7805) 2022-04-21 19:07:18 +02:00
Hyro
4972bd87c1 fix(ApplicationCommand): equal nameLocalizations and descriptionLocalizations (#7802) 2022-04-21 19:06:42 +02:00
Euan Caskie
a1b145e0ce makeContent should accept empty strings (#7764) 2022-04-21 18:57:00 +02:00
Suneet Tipirneni
29293d7bbb refactor: use arrays instead of rest parameters for builders (#7759) 2022-04-21 18:56:10 +02:00
Rodry
f22245e9d0 feat(SelectMenu): allow emojis in options and option constructors (#7797) 2022-04-21 18:55:51 +02:00
Suneet Tipirneni
585169f2f0 types: cleanup *Data type definitions (#7716) 2022-04-19 16:00:55 +02:00
Ryan Munro
ec8d87f932 feat(builders): improve embed errors and predicates (#7795)
* feat(builders): improve embed errors

* fix: use imageURLPredicates

Co-authored-by: Synbulat Biishev <arziksin@gmail.com>

Co-authored-by: Synbulat Biishev <arziksin@gmail.com>
2022-04-19 15:58:47 +02:00
n1ck_pro
440ac243ca types: fix BooleanCache never resolving to true (#7809)
* types: fix BooleanCache never resolving to true

* fix: broke other types by accident
2022-04-19 15:57:44 +02:00
Ryan Munro
c5fb548529 fix(InteractionResponses): use optional chaining on nullable property (#7812)
* fix(InteractionResponses): use optional chaining on nullable property

* fix: one more spot
2022-04-19 15:56:45 +02:00
Almeida
a6d9ce57c6 types(CommandInteraction): add awaitModalSubmit (#7811) 2022-04-19 15:56:22 +02:00
Rodry
08574763eb types(ThreadChannel): fix autoArchiveDuration types (#7816) 2022-04-19 15:54:54 +02:00
iCrawl
9ef7ffdc6d ci: use proper regex escapes 2022-04-17 20:07:19 +02:00
256 changed files with 8449 additions and 6551 deletions

View File

@@ -15,6 +15,7 @@ body:
- builders
- collection
- rest
- proxy
- voice
validations:
required: true

View File

@@ -17,6 +17,7 @@ body:
- builders
- collection
- rest
- proxy
- voice
validations:
required: true

View File

@@ -5,3 +5,4 @@ reviewers:
- kyranet
- vladfrangu
numberOfReviewers: 0
runOnDraft: true

4
.github/labeler.yml vendored
View File

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

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

View File

@@ -7,6 +7,11 @@ on:
- '!docs'
tags:
- '**'
workflow_dispatch:
inputs:
ref:
description: 'The branch, tag or SHA to checkout'
required: true
jobs:
build:
name: Build documentation
@@ -19,6 +24,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.ref || '' }}
- name: Install node.js v16
uses: actions/setup-node@v2
@@ -62,13 +69,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_NAME: ${{ github.event.inputs.ref || needs.build.outputs.BRANCH_NAME }}
BRANCH_OR_TAG: ${{ needs.build.outputs.BRANCH_OR_TAG }}
SHA: ${{ needs.build.outputs.SHA }}
steps:
- 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,35 +114,24 @@ jobs:
token: ${{ secrets.DJS_DOCS }}
path: 'out'
- name: 'Extract package from tag'
if: env.BRANCH_OR_TAG == 'tag'
id: package-name
uses: frabert/replace-string-action@v2.0
- name: Extract package and semver from tag
if: ${{ github.event.inputs.ref || env.BRANCH_OR_TAG == 'tag' }}
id: extract-tag
uses: ./packages/actions/src/formatTag
with:
pattern: '(^@.*\\/(?<package>.*)@v?)?(?<semver>\d+.\d+.\d+)-?.*'
string: ${{ env.BRANCH_NAME }}
replace-with: '$<package>'
- name: 'Extract semver from tag'
if: env.BRANCH_OR_TAG == 'tag'
id: semver
uses: frabert/replace-string-action@v2.0
with:
pattern: '(^@.*\\/(?<package>.*)@v?)?(?<semver>\d+.\d+.\d+)-?.*'
string: ${{ env.BRANCH_NAME }}
replace-with: '$<semver>'
tag: ${{ env.BRANCH_NAME }}
- name: Move docs to correct directory
if: env.BRANCH_OR_TAG == 'tag'
if: ${{ (github.event.inputs.ref || env.BRANCH_OR_TAG == 'tag') && matrix.package == steps.extract-tag.outputs.package }}
env:
PACKAGE: ${{ steps.package-name.outputs.replaced }}
SEMVER: ${{ steps.semver.outputs.replaced }}
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'
if: ${{ !github.event.inputs.ref && env.BRANCH_OR_TAG == 'branch' }}
env:
PACKAGE: ${{ matrix.package }}
run: |

View File

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

View File

@@ -16,6 +16,8 @@ jobs:
folder: 'collection'
- package: 'discord.js'
folder: 'discord.js'
- package: '@discordjs/proxy'
folder: 'proxy'
- package: '@discordjs/rest'
folder: 'rest'
- package: '@discordjs/voice'

View File

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

1
.gitignore vendored
View File

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

View File

@@ -5,5 +5,8 @@
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": false
},
"files.exclude": {
"**/node_modules": true
}
}

File diff suppressed because one or more lines are too long

786
.yarn/releases/yarn-3.2.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: "@yarnpkg/plugin-version"
yarnPath: .yarn/releases/yarn-3.1.1.cjs
yarnPath: .yarn/releases/yarn-3.2.1.cjs

View File

@@ -5,7 +5,7 @@
"private": true,
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"test": "turbo run test && vitest run",
"lint": "turbo run lint",
"format": "turbo run format",
"fmt": "turbo run format",
@@ -38,13 +38,16 @@
},
"homepage": "https://discord.js.org",
"devDependencies": {
"@commitlint/cli": "^16.2.3",
"@commitlint/config-angular": "^16.2.3",
"@commitlint/cli": "^17.0.2",
"@commitlint/config-angular": "^17.0.0",
"@favware/npm-deprecate": "^1.0.4",
"c8": "^7.11.3",
"conventional-changelog-cli": "^2.2.2",
"husky": "^7.0.4",
"husky": "^8.0.1",
"is-ci": "^3.0.1",
"prettier": "^2.6.2",
"turbo": "^1.2.4"
"turbo": "^1.2.16",
"vitest": "^0.13.1"
},
"engines": {
"node": ">=16.9.0"
@@ -52,5 +55,5 @@
"workspaces": [
"packages/*"
],
"packageManager": "yarn@3.1.1"
"packageManager": "yarn@3.2.1"
}

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

View File

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

View File

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

191
packages/actions/LICENSE Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2021 Noel Buechler
Copyright 2021 Vlad Frangu
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,31 @@
<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://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/test.yml/badge.svg" alt="Build status" /></a>
</p>
</div>
## Links
- [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
- [Documentation](https://discord.js.org/#/docs)
- [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v13.html), including updated and removed items in the library.
- [discord.js Discord server](https://discord.gg/djs)
- [Discord API Discord server](https://discord.gg/discord-api)
- [GitHub](https://github.com/discordjs/discord.js/tree/main/packages/scripts)
- [Related libraries](https://discord.com/developers/docs/topics/community-resources#libraries)
## Contributing
See [the contribution guide](https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
nudge in the right direction, please don't hesitate to join our official [discord.js Server](https://discord.gg/djs).

View File

@@ -0,0 +1,22 @@
import { describe, test, expect } from 'vitest';
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);
});
});

View File

@@ -0,0 +1,64 @@
{
"name": "@discordjs/actions",
"version": "0.1.0-dev",
"description": "A set of actions that we use for our workflows",
"private": true,
"scripts": {
"build": "tsup",
"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": {
"@types/node": "^16.11.38",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"eslint": "^8.17.0",
"eslint-config-marine": "^9.4.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"prettier": "^2.6.2",
"tsup": "^6.0.1",
"typescript": "^4.7.3"
},
"engines": {
"node": ">=16.9.0"
}
}

View 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.js

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

View 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) {
setOutput('package', parsed.package);
setOutput('semver', parsed.semver);
}

View File

@@ -0,0 +1 @@
export * from './formatTag/formatTag';

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'tsup';
export default defineConfig({
clean: true,
dts: true,
entryPoints: ['src/index.ts', 'src/formatTag/index.ts'],
format: ['cjs'],
minify: true,
skipNodeModulesBundle: false,
noExternal: ['@actions/core'],
sourcemap: true,
target: 'es2021',
});

View File

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

View File

@@ -2,7 +2,37 @@
All notable changes to this project will be documented in this file.
# [0.13.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.12.0...@discordjs/builders@0.13.0) (2022-04-17)
# [@discordjs/builders@0.15.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.14.0...@discordjs/builders@0.15.0) - (2022-06-05)
## Features
- Allow builders to accept rest params and arrays (#7874) ([ad75be9](https://github.com/discordjs/discord.js/commit/ad75be9a9cf90c8624495df99b75177e6c24022f))
- Use vitest instead of jest for more speed ([8d8e6c0](https://github.com/discordjs/discord.js/commit/8d8e6c03decd7352a2aa180f6e5bc1a13602539b))
- Add scripts package for locally used scripts ([f2ae1f9](https://github.com/discordjs/discord.js/commit/f2ae1f9348bfd893332a9060f71a8a5f272a1b8b))
# [@discordjs/builders@0.14.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.13.0...@discordjs/builders@0.14.0) - (2022-06-04)
## Bug Fixes
- **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
@@ -15,13 +45,11 @@ All notable changes to this project will be documented in this file.
- Fix some typos (#7393) ([92a04f4](https://github.com/discordjs/discord.js/commit/92a04f4d98f6c6760214034cc8f5a1eaa78893c7))
- **builders:** Make type optional in constructor (#7391) ([4abb28c](https://github.com/discordjs/discord.js/commit/4abb28c0a1256c57a60369a6b8ec9e98c265b489))
- Don't create new instances of builders classes (#7343) ([d6b56d0](https://github.com/discordjs/discord.js/commit/d6b56d0080c4c5f8ace731f1e8bcae0c9d3fb5a5))
- **builders:** Dont export `Button` component stuff twice (#7289) ([86d9d06](https://github.com/discordjs/discord.js/commit/86d9d0674347c08d056cd054cb4ce4253195bf94))
## Documentation
- Completely fix builders example link (#7543) ([1a14c0c](https://github.com/discordjs/discord.js/commit/1a14c0ca562ea173d363a770a0437209f461fd23))
- Add slash command builders example, fixes #7338 (#7339) ([3ae6f3c](https://github.com/discordjs/discord.js/commit/3ae6f3c313091151245d6e6b52337b459ecfc765))
- **SlashCommandSubcommands:** Updating old links from Discord developer portal (#7224) ([bd7a6f2](https://github.com/discordjs/discord.js/commit/bd7a6f265212624199fb0b2ddc8ece39759c63de))
## Features
@@ -33,7 +61,6 @@ All notable changes to this project will be documented in this file.
- **builders:** Add attachment command option type (#7203) ([ae0f35f](https://github.com/discordjs/discord.js/commit/ae0f35f51d68dfa5a7dc43d161ef9365171debdb))
- **components:** Add unsafe message component builders (#7387) ([6b6222b](https://github.com/discordjs/discord.js/commit/6b6222bf513d1ee8cd98fba0ad313def560b864f))
- **embed:** Add setFields (#7322) ([bcc5cda](https://github.com/discordjs/discord.js/commit/bcc5cda8a902ddb28c7e3578e0f29b4272832624))
- Add components to /builders (#7195) ([2bb40fd](https://github.com/discordjs/discord.js/commit/2bb40fd767cf5918e3ba422ff73082734bfa05b0))
## Refactor
@@ -55,9 +82,8 @@ All notable changes to this project will be documented in this file.
## Typings
- Fix regressions (#7649) ([5748dbe](https://github.com/discordjs/discord.js/commit/5748dbe08783beb80c526de38ccd105eb0e82664))
- Make `required` a boolean (#7307) ([c10afea](https://github.com/discordjs/discord.js/commit/c10afeadc702ab98bec5e077b3b92494a9596f9c))
# [0.12.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@0.11.0...@discordjs/builders@0.12.0) (2021-12-08)
# [@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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { APISelectMenuComponent, APISelectMenuOption, ComponentType } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index';
const selectMenu = () => new SelectMenuBuilder();
@@ -46,27 +47,49 @@ describe('Select Menu Components', () => {
expect(() => selectMenu().addOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError();
expect(() => selectMenu().addOptions([option])).not.toThrowError();
expect(() => selectMenu().setOptions([option])).not.toThrowError();
expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError();
expect(() =>
selectMenu().addOptions({
label: 'test',
value: 'test',
emoji: {
id: '123',
name: 'test',
animated: true,
},
}),
selectMenu()
.addOptions({
label: 'test',
value: 'test',
emoji: {
id: '123',
name: 'test',
animated: true,
},
})
.addOptions([
{
label: 'test',
value: 'test',
emoji: {
id: '123',
name: 'test',
animated: true,
},
},
]),
).not.toThrowError();
const options = new Array<APISelectMenuOption>(25).fill({ label: 'test', value: 'test' });
expect(() => selectMenu().addOptions(...options)).not.toThrowError();
expect(() => selectMenu().setOptions(...options)).not.toThrowError();
expect(() => selectMenu().addOptions(options)).not.toThrowError();
expect(() => selectMenu().setOptions(options)).not.toThrowError();
expect(() =>
selectMenu()
.addOptions({ label: 'test', value: 'test' })
.addOptions(...new Array<APISelectMenuOption>(24).fill({ label: 'test', value: 'test' })),
).not.toThrowError();
expect(() =>
selectMenu()
.addOptions([{ label: 'test', value: 'test' }])
.addOptions(new Array<APISelectMenuOption>(24).fill({ label: 'test', value: 'test' })),
).not.toThrowError();
});
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
@@ -87,15 +110,32 @@ describe('Select Menu Components', () => {
expect(() => selectMenu().addOptions({ value: 'test' })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ default: true })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions([{ label: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ label: longStr, value: 'test' }])).toThrowError();
expect(() => selectMenu().addOptions([{ value: longStr, label: 'test' }])).toThrowError();
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().setOptions(tooManyOptions)).toThrowError();
expect(() =>
selectMenu()
.addOptions({ label: 'test', value: 'test' })
.addOptions(...tooManyOptions),
).toThrowError();
expect(() =>
selectMenu()
.addOptions([{ label: 'test', value: 'test' }])
.addOptions(tooManyOptions),
).toThrowError();
expect(() => {
selectMenuOption()
@@ -115,6 +155,11 @@ describe('Select Menu Components', () => {
.addOptions(new SelectMenuOptionBuilder(selectMenuOptionData))
.toJSON(),
).toEqual(selectMenuData);
expect(
new SelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions([new SelectMenuOptionBuilder(selectMenuOptionData)])
.toJSON(),
).toEqual(selectMenuData);
expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
});
});

View File

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

View File

@@ -1,3 +1,5 @@
import { PermissionFlagsBits } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index';
const getBuilder = () => new ContextMenuCommandBuilder();
@@ -85,5 +87,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();
});
});
});
});

View File

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

View File

@@ -1,4 +1,5 @@
import { APIApplicationCommandOptionChoice, ChannelType } from 'discord-api-types/v10';
import { APIApplicationCommandOptionChoice, ChannelType, PermissionFlagsBits } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
SlashCommandAssertions,
SlashCommandBooleanOption,
@@ -313,8 +314,10 @@ describe('Slash Commands', () => {
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(true)).toThrowError();
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(null)).toThrowError();
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
expect(() => getBuilder().addBooleanOption(undefined)).toThrowError();
// @ts-expect-error Checking if not providing anything, or an invalid return type causes an error
@@ -433,7 +436,7 @@ describe('Slash Commands', () => {
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN valid name localizations THEN does not throw error', () => {
test('GIVEN invalid name localizations THEN does throw error', () => {
// @ts-expect-error
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error
@@ -456,7 +459,7 @@ describe('Slash Commands', () => {
expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
});
test('GIVEN valid description localizations THEN does not throw error', () => {
test('GIVEN invalid description localizations THEN does throw error', () => {
// @ts-expect-error
expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError();
// @ts-expect-error
@@ -476,5 +479,27 @@ describe('Slash Commands', () => {
});
});
});
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();
});
});
});
});

View File

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

View File

@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest';
import { EmbedBuilder, embedLength } from '../../src';
const alpha = 'abcdefghijklmnopqrstuvwxyz';
@@ -13,13 +14,13 @@ describe('Embed', () => {
footer: { text: alpha },
});
expect(embedLength(embed.data)).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 EmbedBuilder();
expect(embedLength(embed.data)).toBe(0);
expect(embedLength(embed.data)).toEqual(0);
});
});
@@ -323,9 +324,13 @@ describe('Embed', () => {
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
const embed = new EmbedBuilder();
embed.addFields({ name: 'foo', value: 'bar' });
embed.addFields([{ name: 'foo', value: 'bar' }]);
expect(embed.toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
fields: [
{ name: 'foo', value: 'bar' },
{ name: 'foo', value: 'bar' },
],
});
});
@@ -334,11 +339,11 @@ describe('Embed', () => {
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'baz', inline: undefined }],
fields: [{ name: 'foo', value: 'baz' }],
});
});
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data', () => {
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data 2', () => {
const embed = new EmbedBuilder();
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
@@ -362,6 +367,9 @@ describe('Embed', () => {
expect(() =>
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
expect(() =>
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
).not.toThrowError();
});
test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => {
@@ -370,10 +378,11 @@ describe('Embed', () => {
expect(() =>
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
).toThrowError();
expect(() => embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError();
});
describe('GIVEN invalid field amount THEN throws error', () => {
test('', () => {
test('1', () => {
const embed = new EmbedBuilder();
expect(() =>
@@ -383,7 +392,7 @@ describe('Embed', () => {
});
describe('GIVEN invalid field name THEN throws error', () => {
test('', () => {
test('2', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
@@ -391,7 +400,7 @@ describe('Embed', () => {
});
describe('GIVEN invalid field name length THEN throws error', () => {
test('', () => {
test('3', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
@@ -399,7 +408,7 @@ describe('Embed', () => {
});
describe('GIVEN invalid field value length THEN throws error', () => {
test('', () => {
test('4', () => {
const embed = new EmbedBuilder();
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1025) })).toThrowError();

View File

@@ -1,3 +1,4 @@
import { describe, test, expect, vitest } from 'vitest';
import {
blockQuote,
bold,
@@ -22,75 +23,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/)',
);
});
@@ -98,102 +99,104 @@ 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>');
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>');
});
});
describe('time', () => {
test('GIVEN no arguments THEN returns "<t:${bigint}>"', () => {
jest.useFakeTimers('modern');
jest.setSystemTime(1566424897579);
vitest.useFakeTimers();
vitest.setSystemTime(1566424897579);
expect<`<t:${bigint}>`>(time()).toBe('<t:1566424897>');
expect<`<t:${bigint}>`>(time()).toEqual('<t:1566424897>');
jest.useRealTimers();
vitest.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('┬─┬ ( ゜-゜ノ)');
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
{
"name": "@discordjs/builders",
"version": "0.14.0-dev",
"version": "0.15.0",
"description": "A set of builders that you can use when creating your bot",
"scripts": {
"build": "tsup",
"test": "jest --pass-with-no-tests",
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && node scripts/docs.mjs",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && ts-docgen -i docs/typedoc-out.json -c docs/index.yml -o docs/docs.json",
"prepublishOnly": "yarn build && yarn lint && yarn test",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/builders/*'"
},
@@ -52,33 +51,26 @@
},
"homepage": "https://discord.js.org",
"dependencies": {
"@sapphire/shapeshift": "^2.0.0",
"@sapphire/shapeshift": "^3.1.0",
"@sindresorhus/is": "^4.6.0",
"discord-api-types": "^0.31.1",
"discord-api-types": "^0.33.3",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.1",
"tslib": "^2.3.1"
"tslib": "^2.4.0"
},
"devDependencies": {
"@babel/core": "^7.17.9",
"@babel/plugin-proposal-decorators": "^7.17.9",
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.7",
"@discordjs/ts-docgen": "^0.4.1",
"@types/jest": "^27.4.1",
"@types/node": "^16.11.27",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"babel-plugin-transform-typescript-metadata": "^0.3.2",
"eslint": "^8.13.0",
"@discordjs/scripts": "workspace:^",
"@types/node": "^16.11.38",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"eslint": "^8.17.0",
"eslint-config-marine": "^9.4.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"jest": "^27.5.1",
"prettier": "^2.6.2",
"tsup": "^5.12.5",
"typedoc": "^0.22.15",
"typescript": "^4.6.3"
"tsup": "^6.0.1",
"typedoc": "^0.22.17",
"typescript": "^4.7.3"
},
"engines": {
"node": ">=16.9.0"

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v10';
import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption';
import { UnsafeSelectMenuOptionBuilder } from './selectMenu/UnsafeSelectMenuOption';
export const customIdValidator = s.string.lengthGe(1).lengthLe(100);
export const customIdValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
export const emojiValidator = s.object({
id: s.string,
@@ -13,14 +13,14 @@ export const emojiValidator = s.object({
export const disabledValidator = s.boolean;
export const buttonLabelValidator = s.string.lengthGe(1).lengthLe(80);
export const buttonLabelValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(80);
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
export const placeholderValidator = s.string.lengthLe(150);
export const minMaxValidator = s.number.int.ge(0).le(25);
export const placeholderValidator = s.string.lengthLessThanOrEqual(150);
export const minMaxValidator = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(25);
export const labelValueDescriptionValidator = s.string.lengthGe(1).lengthLe(100);
export const labelValueDescriptionValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
export const optionValidator = s.union(
s.object({
label: labelValueDescriptionValidator,
@@ -31,15 +31,15 @@ export const optionValidator = s.union(
}),
s.instance(UnsafeSelectMenuOptionBuilder),
);
export const optionsValidator = optionValidator.array.lengthGe(0);
export const optionsLengthValidator = s.number.int.ge(0).le(25);
export const optionsValidator = optionValidator.array.lengthGreaterThanOrEqual(0);
export const optionsLengthValidator = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(25);
export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
customIdValidator.parse(customId);
optionsValidator.parse(options);
}
export const labelValueValidator = s.string.lengthGe(1).lengthLe(100);
export const labelValueValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
export const defaultValidator = s.boolean;
export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {

View File

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

View File

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

View File

@@ -3,12 +3,12 @@ import { TextInputStyle } from 'discord-api-types/v10';
import { customIdValidator } from '../Assertions';
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
export const minLengthValidator = s.number.int.ge(0).le(4000);
export const maxLengthValidator = s.number.int.ge(1).le(4000);
export const 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.lengthLe(4000);
export const placeholderValidator = s.string.lengthLe(100);
export const labelValidator = s.string.lengthGe(1).lengthLe(45);
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);

View File

@@ -9,7 +9,7 @@ export class UnsafeTextInputBuilder extends ComponentBuilder<APITextInputCompone
/**
* Sets the custom id for this text input
* @param customId The custom id of this text inputå
* @param customId The custom id of this text input
*/
public setCustomId(customId: string) {
this.data.custom_id = customId;

View File

@@ -32,6 +32,12 @@ 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';
@@ -39,3 +45,4 @@ export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder';
export * from './util/jsonEncodable';
export * from './util/equatable';
export * from './util/componentUtil';
export * from './util/normalizeArray';

View File

@@ -3,8 +3,8 @@ import { ApplicationCommandType } from 'discord-api-types/v10';
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder';
const namePredicate = s.string
.lengthGe(1)
.lengthLe(32)
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(32)
.regex(/^( *[\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+ *)+$/u);
const typePredicate = s.union(s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message));
@@ -30,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);
}

View File

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

View File

@@ -2,8 +2,8 @@ import { s } from '@sapphire/shapeshift';
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../..';
import { customIdValidator } from '../../components/Assertions';
export const titleValidator = s.string.lengthGe(1).lengthLe(45);
export const componentsValidator = s.instance(ActionRowBuilder).array.lengthGe(1);
export const titleValidator = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(45);
export const componentsValidator = s.instance(ActionRowBuilder).array.lengthGreaterThanOrEqual(1);
export function validateRequiredParameters(
customId?: string,

View File

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

View File

@@ -1,27 +1,27 @@
import { s } from '@sapphire/shapeshift';
import is from '@sindresorhus/is';
import { type APIApplicationCommandOptionChoice, Locale } from 'discord-api-types/v10';
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 = s.string
.lengthGe(1)
.lengthLe(32)
.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 = s.string.lengthGe(1).lengthLe(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 = s.unknown.array.lengthLe(25);
const maxArrayLengthPredicate = s.unknown.array.lengthLessThanOrEqual(25);
export function validateLocale(locale: unknown) {
return localePredicate.parse(locale);
}
@@ -55,7 +55,7 @@ export function validateRequired(required: unknown): asserts required is boolean
booleanPredicate.parse(required);
}
const choicesLengthPredicate = s.number.le(25);
const choicesLengthPredicate = s.number.lessThanOrEqual(25);
export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void {
choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding);
@@ -87,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);
}

View File

@@ -1,12 +1,16 @@
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';
@@ -44,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.
@@ -56,13 +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,
name_localizations: this.name_localizations,
description: this.description,
description_localizations: this.description_localizations,
...this,
options: this.options.map((option) => option.toJSON()),
default_permission: this.defaultPermission,
};
}
@@ -74,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;
}

View File

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

View File

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

View File

@@ -1,10 +1,14 @@
import { s } from '@sapphire/shapeshift';
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v10';
import { validateChoicesLength } from '../Assertions';
import { localizationMapPredicate, validateChoicesLength } from '../Assertions';
const stringPredicate = s.string.lengthGe(1).lengthLe(100);
const numberPredicate = s.number.gt(-Infinity).lt(Infinity);
const choicesPredicate = s.object({ name: stringPredicate, value: s.union(stringPredicate, numberPredicate) }).array;
const 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> {
@@ -32,7 +36,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
validateChoicesLength(choices.length, this.choices);
for (const { name, value } of choices) {
for (const { name, name_localizations, value } of choices) {
// Validate the value
if (this.type === ApplicationCommandOptionType.String) {
stringPredicate.parse(value);
@@ -40,7 +44,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends s
numberPredicate.parse(value);
}
this.choices!.push({ name, value });
this.choices!.push({ name, name_localizations, value });
}
return this;

View File

@@ -46,14 +46,16 @@ export class SharedNameAndDescription {
Reflect.set(this, 'name_localizations', {});
}
const parsedLocale = validateLocale(locale);
if (localizedName === null) {
this.name_localizations![locale] = null;
this.name_localizations![parsedLocale] = null;
return this;
}
validateName(localizedName);
this.name_localizations![validateLocale(locale)] = localizedName;
this.name_localizations![parsedLocale] = localizedName;
return this;
}
@@ -87,14 +89,16 @@ export class SharedNameAndDescription {
Reflect.set(this, 'description_localizations', {});
}
const parsedLocale = validateLocale(locale);
if (localizedDescription === null) {
this.description_localizations![locale] = null;
this.description_localizations![parsedLocale] = null;
return this;
}
validateDescription(localizedDescription);
this.description_localizations![validateLocale(locale)] = localizedDescription;
this.description_localizations![parsedLocale] = localizedDescription;
return this;
}

View File

@@ -1,9 +1,9 @@
import { s } from '@sapphire/shapeshift';
import type { APIEmbedField } from 'discord-api-types/v10';
export const fieldNamePredicate = s.string.lengthGe(1).lengthLe(256);
export const fieldNamePredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(256);
export const fieldValuePredicate = s.string.lengthGe(1).lengthLe(1024);
export const fieldValuePredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(1024);
export const fieldInlinePredicate = s.boolean.optional;
@@ -15,7 +15,7 @@ export const embedFieldPredicate = s.object({
export const embedFieldsArrayPredicate = embedFieldPredicate.array;
export const fieldLengthPredicate = s.number.le(25);
export const fieldLengthPredicate = s.number.lessThanOrEqual(25);
export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void {
fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding);
@@ -31,15 +31,26 @@ export const urlPredicate = s.string.url({
allowedProtocols: ['http:', 'https:'],
}).nullish;
export const RGBPredicate = s.number.int.ge(0).le(255);
export const embedAuthorPredicate = s.object({
name: authorNamePredicate,
iconURL: imageURLPredicate,
url: urlPredicate,
});
export const RGBPredicate = s.number.int.greaterThanOrEqual(0).lessThanOrEqual(255);
export const colorPredicate = s.number.int
.ge(0)
.le(0xffffff)
.greaterThanOrEqual(0)
.lessThanOrEqual(0xffffff)
.or(s.tuple([RGBPredicate, RGBPredicate, RGBPredicate])).nullable;
export const descriptionPredicate = s.string.lengthGe(1).lengthLe(4096).nullable;
export const descriptionPredicate = s.string.lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(4096).nullable;
export const footerTextPredicate = s.string.lengthGe(1).lengthLe(2048).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;

View File

@@ -1,10 +1,10 @@
import type { APIEmbedField } from 'discord-api-types/v10';
import {
authorNamePredicate,
colorPredicate,
descriptionPredicate,
embedAuthorPredicate,
embedFieldsArrayPredicate,
footerTextPredicate,
embedFooterPredicate,
imageURLPredicate,
timestampPredicate,
titlePredicate,
@@ -12,12 +12,14 @@ import {
validateFieldLength,
} from './Assertions';
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbedBuilder } from './UnsafeEmbed';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray';
/**
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
*/
export class EmbedBuilder extends UnsafeEmbedBuilder {
public override addFields(...fields: APIEmbedField[]): this {
public override addFields(...fields: RestOrArray<APIEmbedField>): this {
fields = normalizeArray(fields);
// Ensure adding these fields won't exceed the 25 field limit
validateFieldLength(fields.length, this.data.fields);
@@ -39,9 +41,7 @@ export class EmbedBuilder extends UnsafeEmbedBuilder {
}
// Data assertions
authorNamePredicate.parse(options.name);
urlPredicate.parse(options.iconURL);
urlPredicate.parse(options.url);
embedAuthorPredicate.parse(options);
return super.setAuthor(options);
}
@@ -62,8 +62,7 @@ export class EmbedBuilder extends UnsafeEmbedBuilder {
}
// Data assertions
footerTextPredicate.parse(options.text);
urlPredicate.parse(options.iconURL);
embedFooterPredicate.parse(options);
return super.setFooter(options);
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import type { Options } from 'tsup';
import { defineConfig } from 'tsup';
export const tsup: Options = {
export default defineConfig({
clean: true,
dts: true,
entryPoints: ['src/index.ts'],
@@ -17,4 +17,4 @@ export const tsup: Options = {
};
}
},
};
});

View File

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

View File

@@ -2,7 +2,13 @@
All notable changes to this project will be documented in this file.
# [0.6.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@0.5.0...@discordjs/collection@0.6.0) (2022-04-17)
# [@discordjs/collection@0.7.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@0.6.0...@discordjs/collection@0.7.0) - (2022-06-04)
## Styling
- Cleanup tests and tsup configs ([6b8ef20](https://github.com/discordjs/discord.js/commit/6b8ef20cb3af5b5cfd176dd0aa0a1a1e98551629))
# [@discordjs/collection@0.6.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@0.5.0...@discordjs/collection@0.6.0) - (2022-04-17)
## Features
@@ -10,16 +16,7 @@ All notable changes to this project will be documented in this file.
- **builders:** Add attachment command option type (#7203) ([ae0f35f](https://github.com/discordjs/discord.js/commit/ae0f35f51d68dfa5a7dc43d161ef9365171debdb))
- **Collection:** Add merging functions (#7299) ([e4bd07b](https://github.com/discordjs/discord.js/commit/e4bd07b2394f227ea06b72eb6999de9ab3127b25))
## Refactor
- Make `intersect` perform a true intersection (#7211) ([d8efba2](https://github.com/discordjs/discord.js/commit/d8efba24e09aa2a8dbf028fc57a561a56e7833fd))
## Typings
- Add `ReadonlyCollection` (#7245) ([db25f52](https://github.com/discordjs/discord.js/commit/db25f529b26d7c819c1c42ad3e26c2263ea2da0e))
- **Collection:** Union types on `intersect` and `difference` (#7196) ([1f9b922](https://github.com/discordjs/discord.js/commit/1f9b9225f2066e9cc66c3355417139fd25cc403c))
# [0.5.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@0.4.0...@discordjs/collection@0.5.0) (2021-12-08)
# [@discordjs/collection@0.5.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@0.4.0...@discordjs/collection@0.5.0) - (2022-01-24)
## Refactor

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
{
"name": "@discordjs/collection",
"version": "0.7.0-dev",
"version": "0.8.0-dev",
"description": "Utility data structure used in discord.js",
"scripts": {
"test": "jest --pass-with-no-tests",
"build": "tsup",
"lint": "prettier --check . && eslint src __tests__ --ext mjs,js,ts",
"format": "prettier --write . && eslint src __tests__ --ext mjs,js,ts --fix",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && node scripts/docs.mjs",
"docs": "typedoc --json docs/typedoc-out.json src/index.ts && ts-docgen -i docs/typedoc-out.json -c docs/index.yml -o docs/docs.json",
"prepublishOnly": "yarn build && yarn lint && yarn test",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/collection/*'"
},
@@ -48,23 +47,18 @@
},
"homepage": "https://discord.js.org",
"devDependencies": {
"@babel/core": "^7.17.9",
"@babel/preset-env": "^7.16.11",
"@babel/preset-typescript": "^7.16.7",
"@discordjs/ts-docgen": "^0.4.1",
"@types/jest": "^27.4.1",
"@types/node": "^16.11.27",
"@typescript-eslint/eslint-plugin": "^5.19.0",
"@typescript-eslint/parser": "^5.19.0",
"eslint": "^8.13.0",
"@discordjs/scripts": "workspace:^",
"@types/node": "^16.11.38",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"@typescript-eslint/parser": "^5.27.0",
"eslint": "^8.17.0",
"eslint-config-marine": "^9.4.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"jest": "^27.5.1",
"prettier": "^2.6.2",
"tsup": "^5.12.5",
"typedoc": "^0.22.15",
"typescript": "^4.6.3"
"tsup": "^6.0.1",
"typedoc": "^0.22.17",
"typescript": "^4.7.3"
},
"engines": {
"node": ">=16.9.0"

View File

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

View File

@@ -185,7 +185,7 @@ export class Collection<K, V> extends Map<K, V> {
if (!arr.length || !amount) return [];
return Array.from(
{ length: Math.min(amount, arr.length) },
(): V => arr.splice(Math.floor(Math.random() * arr.length), 1)[0],
(): V => arr.splice(Math.floor(Math.random() * arr.length), 1)[0]!,
);
}
@@ -204,7 +204,7 @@ export class Collection<K, V> extends Map<K, V> {
if (!arr.length || !amount) return [];
return Array.from(
{ length: Math.min(amount, arr.length) },
(): K => arr.splice(Math.floor(Math.random() * arr.length), 1)[0],
(): K => arr.splice(Math.floor(Math.random() * arr.length), 1)[0]!,
);
}

View File

@@ -1,6 +1,6 @@
import type { Options } from 'tsup';
import { defineConfig } from 'tsup';
export const tsup: Options = {
export default defineConfig({
clean: true,
dts: true,
entryPoints: ['src/index.ts'],
@@ -18,4 +18,4 @@ export const tsup: Options = {
};
}
},
};
});

View File

@@ -2,6 +2,74 @@
All notable changes to this project will be documented in this file.
# [13.7.0](https://github.com/discordjs/discord.js/compare/13.6.0...13.7.0) - (2022-05-13)
## Bug Fixes
- **MessageEmbed:** Fix a typo (#7906) ([ea28638](https://github.com/discordjs/discord.js/commit/ea28638a0c7cebb40aab36c01ed29fed02935540))
- **GuildEditData:** Some fields can be null for v13 (#7633) ([816936e](https://github.com/discordjs/discord.js/commit/816936eafbe4b8caee10a15709a771919754f5ad))
- Apply v14 fix (#7756) ([ab6c2ba](https://github.com/discordjs/discord.js/commit/ab6c2bad845d44b5d822b131e5e458a3637431c6))
- **GuildChannelManager:** `delete` method accessing wrong id (#7771) ([c9e4562](https://github.com/discordjs/discord.js/commit/c9e4562fd58999452829501a3410da2b1709342f))
- **GuildScheduledEvent:** Handle missing `image` for v13 (#7627) ([dfea9c2](https://github.com/discordjs/discord.js/commit/dfea9c27cef4ec7472c39675c93de985b43fee87))
- **messagementions:** Fix `has` method for v13 (#7591) ([7a52785](https://github.com/discordjs/discord.js/commit/7a52785f7d3a0d2211b2ceff68f7152161a0b1f5))
- Check if member has admininistrator on `moderatable` (v13) (#7578) ([13dd82d](https://github.com/discordjs/discord.js/commit/13dd82d7fa6bc5906103cf8c1f4bb7de5d3e8270))
- **ThreadChannel:** Require `sendable` for `unarchivable` (#7555) ([49397c0](https://github.com/discordjs/discord.js/commit/49397c0ca4ec468a2046167afa64b7a82aa5e7fa))
- Backport `MessageReaction#me` being incorrectly `false` (#7553) ([5f621c1](https://github.com/discordjs/discord.js/commit/5f621c19959e892066b23f06f0b6485b6cecdb4c))
- **typings:** SweepStageInstances typo (#7521) ([f096069](https://github.com/discordjs/discord.js/commit/f0960698d2ef51d4586f3c89d8b7c94faf5d921b))
- **MessagePayload:** V13 don't set reply flags to target flags (#7515) ([30baff7](https://github.com/discordjs/discord.js/commit/30baff7ecbe5fdb5bea9ed5b14a28a76922d507c))
- **Shard:** V13 EventEmitter listener warning (#7479) ([77b8e01](https://github.com/discordjs/discord.js/commit/77b8e0191123b0e76832c642454adceab9b8ad13))
- **MessageEmbed:** Set footer to undefined (#7358) ([bc5ddc3](https://github.com/discordjs/discord.js/commit/bc5ddc36fa2e1935ffc8e871c333f6613a5f2396))
## Documentation
- **shardingmanager:** Fix type of `execArgv` option (v13) (#7863) ([43a7870](https://github.com/discordjs/discord.js/commit/43a7870b2337ebdc362640a799fe5e81cfbaf739))
- Fix and improve localization docs (v13 backport) (#7807) ([6dcf0bd](https://github.com/discordjs/discord.js/commit/6dcf0bda05ee79baa173dce02a4f8985f6f654df))
- **ApplicationCommand:** Fix ApplicationCommandOptionChoice (#7798) ([1040ce0](https://github.com/discordjs/discord.js/commit/1040ce0e710e0e805ad75302b9078a0230309ae8))
- Backport version 13 fixes (#7552) ([69ba067](https://github.com/discordjs/discord.js/commit/69ba067a6512fffd2be99ad0b5db2cbfcbb19c88))
## Features
- Backport (#7776) ([5165b18](https://github.com/discordjs/discord.js/commit/5165b18b85379411645ca377389b09a911fd3fc8))
- Backport (#7787) ([3eb45e3](https://github.com/discordjs/discord.js/commit/3eb45e30b3a3b1b5624d36698b7f1af6bff3cb6d))
- Backport (#7786) ([ab324ea](https://github.com/discordjs/discord.js/commit/ab324ea6ae042e8312e28dee1665729e6db29193))
- Add support for localized slash commands (v13 backport) (#7766) ([022e138](https://github.com/discordjs/discord.js/commit/022e138b9a5a9038e61b5bdabade7606c9341982))
- App authorization links and tags for v13 (#7731) ([9e4a900](https://github.com/discordjs/discord.js/commit/9e4a900e6d540ad611675ec54ba35e2b9da984dd))
- Backport (#7777) ([6c56132](https://github.com/discordjs/discord.js/commit/6c5613255ade937384525b17f2179f4086a501f1))
- Backport (#7778) ([ff49b82](https://github.com/discordjs/discord.js/commit/ff49b82db773f0407ac0e890897156fb52843b11))
- Backport (#7779) ([ae7f991](https://github.com/discordjs/discord.js/commit/ae7f991e8d08222483a1e92d6740fedadc479bd5))
- Backport (#7783) ([cedc333](https://github.com/discordjs/discord.js/commit/cedc3339401349dfa00990be204b203ef46a3545))
- **VoiceChannel:** Support `video_quality_mode` (v13) (#7785) ([6daee1b](https://github.com/discordjs/discord.js/commit/6daee1b235fc29eb09d9dd97cbbea619225ee2e1))
- **StageInstance:** Add support for associated guild event (#7713) ([68498a8](https://github.com/discordjs/discord.js/commit/68498a87be9436be95456768f50db638c06a6ca8))
- **modals:** Modals, input text components and modal submits, v13 style (#7431) ([e1cdcfa](https://github.com/discordjs/discord.js/commit/e1cdcfa9a6baed1d373cc5474630d32ce38db31e))
- Backport `Interaction#isRepliable` (#7563) ([5e8162a](https://github.com/discordjs/discord.js/commit/5e8162a1379cb3b8c02cc5b29e70911eb1389218))
- Add methods to managers for v13 (#7611) ([9f09702](https://github.com/discordjs/discord.js/commit/9f09702854d21fd11ab3f4e2f0eec445f294130e))
- Add `premiumSubscriptionCount` to `InviteGuild` (#7629) ([8e7d15e](https://github.com/discordjs/discord.js/commit/8e7d15e49d0b75687d4ae813d8274b7086959004))
- **scheduledevents:** Event cover images for v13 (#7613) ([a7535a2](https://github.com/discordjs/discord.js/commit/a7535a2232c4de4553d0d2a2cee315124e1bdfaa))
- Backport `MessageMentions` channel type fixes (#7562) ([93cdb2f](https://github.com/discordjs/discord.js/commit/93cdb2f2fa3ebde8f06cefe9de4a351b99f3b5e6))
- Backport cache types resolving to `never` (#7561) ([611d3a7](https://github.com/discordjs/discord.js/commit/611d3a7b2f76c8be2655d8f27ec4667e6c2054cf))
- Backport sending message flags (#7560) ([29d42ed](https://github.com/discordjs/discord.js/commit/29d42ed31959a0b5e518b46e45029b99cb15aa59))
- **ThreadChannel:** Backport creation timestamp (#7559) ([1d97dcf](https://github.com/discordjs/discord.js/commit/1d97dcff087b75e36d8d4b20e79ec3b820868024))
- Add custom image support to version 13 (#7557) ([679b87c](https://github.com/discordjs/discord.js/commit/679b87c4f88a7bd56bf81a9ade0fc2bf7e54495a))
- Backport `reason` on `pin` and `unpin` (#7556) ([b231bec](https://github.com/discordjs/discord.js/commit/b231bece0e0a1600bd4e2337a2ec83c9a8df11fd))
- **GuildPreview:** Add stickers to version 13 (#7554) ([215dfe0](https://github.com/discordjs/discord.js/commit/215dfe02d5482fcabccbc2373386eae15bdd866a))
- Backport `sweepStickers` method (#7558) ([ee1698d](https://github.com/discordjs/discord.js/commit/ee1698d92869280dcfdbfb353712ef97dff90b56))
- **scheduledevents:** Add image option (v13) (#7549) ([2fcf8af](https://github.com/discordjs/discord.js/commit/2fcf8af421b34b4908bb01b59cf748a1376e5535))
- **thread:** V13 add `newlyCreated` to `threadCreate` event (#7481) ([2b3db73](https://github.com/discordjs/discord.js/commit/2b3db734dfd0e9a3cbfdd45fd3aa490b82ddaa08))
- **commands:** Attachment options (#7441) ([5bcca8b](https://github.com/discordjs/discord.js/commit/5bcca8b97fc09d9f3149325e2b8d8bccdd61e354))
## Refactor
- Deprecate v13 properties and methods (#7782) ([b9802f4](https://github.com/discordjs/discord.js/commit/b9802f4b6f25da62a0bff049ccc165cce8c9d856))
- Remove non-breaking stuff (#7636) ([b9c5676](https://github.com/discordjs/discord.js/commit/b9c5676006a702f704e970b3027829663b9b0a65))
## Typings
- Fix ModalSubmitInteraction (#7768) ([1d09ad4](https://github.com/discordjs/discord.js/commit/1d09ad4652796ecf39a3146631120d5600f36b6a))
- **threadchannel:** Fix autoArchiveDuration types (#7817) ([7afcd95](https://github.com/discordjs/discord.js/commit/7afcd9594a5706526be20cb26f0de388b094c1b4))
- **InteractionCollector:** Fix guild and channel types (#7624) ([7814074](https://github.com/discordjs/discord.js/commit/78140748ce4a64977426a93fd72c9e2783e5919d))
- V13 channel create overloads fix (#7480) ([0b54089](https://github.com/discordjs/discord.js/commit/0b54089c43b60a325e02b78dd0126771ac71f746))
# [13.6.0](https://github.com/discordjs/discord.js/compare/13.5.1...13.6.0) - (2022-01-13)
## Documentation

View File

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

View File

@@ -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,21 +41,21 @@ 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 = "[0-9]*"

View File

@@ -10,7 +10,7 @@
"docs": "docgen --source ./src --custom ./docs/index.yml --root ../../ --output ./docs/docs.json",
"docs:test": "docgen --source ./src --custom ./docs/index.yml --root ../../",
"prepublishOnly": "yarn lint && yarn test",
"changelog": "git cliff --prepend ./CHANGELOG.md -l -c ./cliff.toml -r ../../ --include-path 'packages/discord.js/*'"
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/discord.js/*'"
},
"main": "./src/index.js",
"types": "./typings/index.d.ts",
@@ -50,30 +50,30 @@
"@discordjs/builders": "workspace:^",
"@discordjs/collection": "workspace:^",
"@discordjs/rest": "workspace:^",
"@sapphire/snowflake": "^3.2.1",
"@sapphire/snowflake": "^3.2.2",
"@types/ws": "^8.5.3",
"discord-api-types": "^0.31.1",
"discord-api-types": "^0.33.3",
"fast-deep-equal": "^3.1.3",
"lodash.snakecase": "^4.1.1",
"tslib": "^2.3.1",
"undici": "^4.16.0",
"ws": "^8.5.0"
"tslib": "^2.4.0",
"undici": "^5.4.0",
"ws": "^8.7.0"
},
"devDependencies": {
"@discordjs/docgen": "^0.11.1",
"@types/node": "^16.11.27",
"@types/node": "^16.11.38",
"dtslint": "^4.2.1",
"eslint": "^8.13.0",
"eslint": "^8.17.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4",
"husky": "^8.0.1",
"is-ci": "^3.0.1",
"jest": "^27.5.1",
"jest": "^28.1.0",
"prettier": "^2.6.2",
"tsd": "^0.20.0",
"tslint": "^6.1.3",
"typescript": "^4.6.3"
"typescript": "^4.7.3"
},
"engines": {
"node": ">=16.9.0"

View File

@@ -10,7 +10,7 @@ class WebhooksUpdate extends Action {
/**
* Emitted whenever a channel has its webhooks changed.
* @event Client#webhookUpdate
* @param {TextChannel|NewsChannel} channel The channel that had a webhook update
* @param {TextChannel|NewsChannel|VoiceChannel} channel The channel that had a webhook update
*/
if (channel) client.emit(Events.WebhooksUpdate, channel);
}

View File

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

View File

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

View File

@@ -71,7 +71,7 @@ const Messages = {
MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.',
MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.',
MESSAGE_CONTENT_TYPE: 'Message content must be a non-empty string.',
MESSAGE_CONTENT_TYPE: 'Message content must be a string.',
SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.',

View File

@@ -19,7 +19,6 @@ exports.Collection = require('@discordjs/collection').Collection;
exports.Constants = require('./util/Constants');
exports.Colors = require('./util/Colors');
exports.DataResolver = require('./util/DataResolver');
exports.EnumResolvers = require('./util/EnumResolvers');
exports.Events = require('./util/Events');
exports.Formatters = require('./util/Formatters');
exports.IntentsBitField = require('./util/IntentsBitField');
@@ -124,6 +123,7 @@ exports.InviteStageInstance = require('./structures/InviteStageInstance');
exports.InviteGuild = require('./structures/InviteGuild');
exports.Message = require('./structures/Message').Message;
exports.Attachment = require('./structures/Attachment');
exports.AttachmentBuilder = require('./structures/AttachmentBuilder');
exports.ModalBuilder = require('./structures/ModalBuilder');
exports.MessageCollector = require('./structures/MessageCollector');
exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction');
@@ -132,7 +132,7 @@ exports.MessageMentions = require('./structures/MessageMentions');
exports.MessagePayload = require('./structures/MessagePayload');
exports.MessageReaction = require('./structures/MessageReaction');
exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction');
exports.ModalSubmitFieldsResolver = require('./structures/ModalSubmitFieldsResolver');
exports.ModalSubmitFields = require('./structures/ModalSubmitFields');
exports.NewsChannel = require('./structures/NewsChannel');
exports.OAuth2Guild = require('./structures/OAuth2Guild');
exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
@@ -172,44 +172,9 @@ exports.WelcomeScreen = require('./structures/WelcomeScreen');
exports.WebSocket = require('./WebSocket');
// External
exports.ActivityType = require('discord-api-types/v10').ActivityType;
exports.ApplicationCommandType = require('discord-api-types/v10').ApplicationCommandType;
exports.ApplicationCommandOptionType = require('discord-api-types/v10').ApplicationCommandOptionType;
exports.ApplicationCommandPermissionType = require('discord-api-types/v10').ApplicationCommandPermissionType;
exports.AuditLogEvent = require('discord-api-types/v10').AuditLogEvent;
exports.ButtonStyle = require('discord-api-types/v10').ButtonStyle;
exports.ChannelType = require('discord-api-types/v10').ChannelType;
exports.ComponentType = require('discord-api-types/v10').ComponentType;
exports.GatewayCloseCodes = require('discord-api-types/v10').GatewayCloseCodes;
exports.GatewayDispatchEvents = require('discord-api-types/v10').GatewayDispatchEvents;
exports.GatewayIntentBits = require('discord-api-types/v10').GatewayIntentBits;
exports.GatewayOpcodes = require('discord-api-types/v10').GatewayOpcodes;
exports.GuildFeature = require('discord-api-types/v10').GuildFeature;
exports.GuildMFALevel = require('discord-api-types/v10').GuildMFALevel;
exports.GuildNSFWLevel = require('discord-api-types/v10').GuildNSFWLevel;
exports.GuildPremiumTier = require('discord-api-types/v10').GuildPremiumTier;
exports.GuildScheduledEventEntityType = require('discord-api-types/v10').GuildScheduledEventEntityType;
exports.GuildScheduledEventPrivacyLevel = require('discord-api-types/v10').GuildScheduledEventPrivacyLevel;
exports.GuildScheduledEventStatus = require('discord-api-types/v10').GuildScheduledEventStatus;
exports.GuildSystemChannelFlags = require('discord-api-types/v10').GuildSystemChannelFlags;
exports.GuildVerificationLevel = require('discord-api-types/v10').GuildVerificationLevel;
exports.InteractionType = require('discord-api-types/v10').InteractionType;
exports.InteractionResponseType = require('discord-api-types/v10').InteractionResponseType;
exports.InviteTargetType = require('discord-api-types/v10').InviteTargetType;
exports.Locale = require('discord-api-types/v10').Locale;
exports.MessageType = require('discord-api-types/v10').MessageType;
exports.MessageFlags = require('discord-api-types/v10').MessageFlags;
exports.OAuth2Scopes = require('discord-api-types/v10').OAuth2Scopes;
exports.PermissionFlagsBits = require('discord-api-types/v10').PermissionFlagsBits;
exports.RESTJSONErrorCodes = require('discord-api-types/v10').RESTJSONErrorCodes;
exports.StageInstancePrivacyLevel = require('discord-api-types/v10').StageInstancePrivacyLevel;
exports.StickerType = require('discord-api-types/v10').StickerType;
exports.StickerFormatType = require('discord-api-types/v10').StickerFormatType;
exports.TextInputStyle = require('discord-api-types/v10').TextInputStyle;
exports.UserFlags = require('discord-api-types/v10').UserFlags;
exports.WebhookType = require('discord-api-types/v10').WebhookType;
exports.DiscordAPIError = require('@discordjs/rest').DiscordAPIError;
exports.HTTPError = require('@discordjs/rest').HTTPError;
exports.RateLimitError = require('@discordjs/rest').RateLimitError;
__exportStar(require('discord-api-types/v10'), exports);
__exportStar(require('@discordjs/builders'), exports);

View File

@@ -48,6 +48,7 @@ class CategoryChannelChildManager extends DataManager {
* @property {number} [position] Position of the new channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds
* @property {string} [rtcRegion] The specific region of the new channel.
* @property {VideoQualityMode} [videoQualityMode] The camera video quality mode of the voice channel
* @property {string} [reason] Reason for creating the new channel
*/

View File

@@ -61,6 +61,11 @@ class ChannelManager extends CachedManager {
_remove(id) {
const channel = this.cache.get(id);
channel?.guild?.channels.cache.delete(id);
for (const [code, invite] of channel?.guild?.invites.cache ?? []) {
if (invite.channelId === id) channel.guild.invites.cache.delete(code);
}
channel?.parent?.threads?.cache.delete(id);
this.cache.delete(id);
}

View File

@@ -13,7 +13,6 @@ const Webhook = require('../structures/Webhook');
const { ThreadChannelTypes } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Util = require('../util/Util');
const { resolveAutoArchiveMaxLimit } = require('../util/Util');
let cacheWarningEmitted = false;
@@ -100,7 +99,7 @@ class GuildChannelManager extends CachedManager {
/**
* Options used to create a new channel in a guild.
* @typedef {CategoryCreateChannelOptions} GuildChannelCreateOptions
* @property {CategoryChannelResolvable} [parent] Parent of the new channel
* @property {?CategoryChannelResolvable} [parent] Parent of the new channel
*/
/**
@@ -138,6 +137,7 @@ class GuildChannelManager extends CachedManager {
position,
rateLimitPerUser,
rtcRegion,
videoQualityMode,
reason,
} = {},
) {
@@ -157,6 +157,7 @@ class GuildChannelManager extends CachedManager {
permission_overwrites: permissionOverwrites,
rate_limit_per_user: rateLimitPerUser,
rtc_region: rtcRegion,
video_quality_mode: videoQualityMode,
},
reason,
});
@@ -200,7 +201,7 @@ class GuildChannelManager extends CachedManager {
* @property {string} [name] The name of the channel
* @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported)
* @property {number} [position] The position of the channel
* @property {string} [topic] The topic of the text channel
* @property {?string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the voice channel
@@ -253,9 +254,6 @@ class GuildChannelManager extends CachedManager {
}
}
let defaultAutoArchiveDuration = data.defaultAutoArchiveDuration;
if (defaultAutoArchiveDuration === 'MAX') defaultAutoArchiveDuration = resolveAutoArchiveMaxLimit(this.guild);
const newData = await this.client.rest.patch(Routes.channel(channel.id), {
body: {
name: (data.name ?? channel.name).trim(),
@@ -269,7 +267,7 @@ class GuildChannelManager extends CachedManager {
parent_id: parent,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: defaultAutoArchiveDuration,
default_auto_archive_duration: data.defaultAutoArchiveDuration,
permission_overwrites,
},
reason,
@@ -433,6 +431,7 @@ class GuildChannelManager extends CachedManager {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
await this.client.rest.delete(Routes.channel(id), { reason });
this.client.actions.ChannelDelete.handle({ id });
}
}

View File

@@ -153,7 +153,7 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
throw new Error('EMOJI_MANAGED');
}
const { me } = this.guild;
const { me } = this.guild.members;
if (!me) throw new Error('GUILD_UNCACHED_ME');
if (!me.permissions.has(PermissionFlagsBits.ManageEmojisAndStickers)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);

View File

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

View File

@@ -12,6 +12,7 @@ const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel');
const { GuildMember } = require('../structures/GuildMember');
const { Role } = require('../structures/Role');
const Events = require('../util/Events');
const Partials = require('../util/Partials');
/**
* Manages API methods for GuildMembers and stores their cache.
@@ -120,6 +121,20 @@ class GuildMemberManager extends CachedManager {
return data instanceof Buffer ? (options.fetchWhenExisting === false ? null : this.fetch(userId)) : this._add(data);
}
/**
* The client user as a GuildMember of this guild
* @type {?GuildMember}
* @readonly
*/
get me() {
return (
this.resolve(this.client.user.id) ??
(this.client.options.partials.includes(Partials.GuildMember)
? this._add({ user: { id: this.client.user.id } }, true)
: null)
);
}
/**
* Options used to fetch a single member from a guild.
* @typedef {BaseFetchOptions} FetchMemberOptions
@@ -191,6 +206,15 @@ class GuildMemberManager extends CachedManager {
return this._fetchMany(options);
}
/**
* Fetches the client user as a GuildMember of the guild.
* @param {BaseFetchOptions} [options] The options for fetching the member
* @returns {Promise<GuildMember>}
*/
fetchMe(options) {
return this.fetch({ ...options, user: this.client.user.id });
}
/**
* Options used for searching guild members.
* @typedef {Object} GuildSearchMembersOptions

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ class MessageManager extends CachedManager {
/**
* Options used to fetch a message.
* @typedef {BaseFetchOptions} FetchMessageOptions
* @property {MessageResolvable} [message] The message to fetch
* @property {MessageResolvable} message The message to fetch
*/
/**

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