Compare commits

...

224 Commits
14.10.0 ... v13

Author SHA1 Message Date
Vlad Frangu
1e06035515 chore(discord.js): release discord.js@13.17.1 (#9955) 2023-11-12 21:38:58 +00:00
Vlad Frangu
f475336b3e fix(ClientPresence): correctly set activity state on CUSTOM activity type (#9954)
* fix(ClientPresence): correctly set activity state on CUSTOM activity type

* Update ClientPresence.js
2023-11-12 21:34:26 +00:00
Vlad Frangu
07caef4cd2 chore(discord.js): release discord.js@13.17.0 (#9944) 2023-11-12 17:40:07 +00:00
Jiralite
efc1536121 fix: Export AttachmentFlags and RoleFlags (#9924)
* fix: export attachment flags

* fix: export `RoleFlags` too
2023-11-05 18:17:44 +00:00
Jiralite
3d5d95775b fix(Action): Conditionally add recipient (#9925)
fix(Action): conditionally add recipient
2023-11-05 18:17:24 +00:00
Jaw0r3k
5dd49339ea chore: run prettier 2023-11-05 19:39:14 +02:00
Jaw0r3k
c08230edc0 types(BaseButtonComponentData): Narrow component type (#9735)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-11-05 19:39:14 +02:00
Jaw0r3k
67dd30a28a feat(Client): add guildAvailable event (#9692)
Co-authored-by: Kyranet <kyradiscord@gmail.com>
2023-11-05 19:39:14 +02:00
Jaw0r3k
2ac8be09a1 feat(Attachment): add flags (#9686)
Co-authored-by: Almeida <almeidx@pm.me>
2023-11-05 19:39:14 +02:00
Jaw0r3k
a222e537c1 feat(Role): add flags (#9694)
Co-authored-by: n1ck_pro <59617443+N1ckPro@users.noreply.github.com>
2023-11-05 19:39:14 +02:00
Jaw0r3k
d0fd79c14a Subject: [PATCH] feat(ClientApplication): Approximate guild count and new
`GET` route (#9713)

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-11-05 19:39:14 +02:00
Jaw0r3k
65bbed8a0f fix(Action): Do not set undefined values (#9755)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-11-05 19:39:14 +02:00
Jaw0r3k
142db0ed8c fix(types): fixed CachedManager constructor arguments in type (#9761) 2023-11-05 19:39:14 +02:00
Jaw0r3k
f217335a9d chore: allow setting activity state (#9743) + (#9742) 2023-11-05 19:39:14 +02:00
brynpttrsn
610bdaa0f1 fix(readme): test badge svg (#9919)
fix test v13 badge
2023-11-04 22:16:38 +00:00
Vlad Frangu
56b481b3e0 fix(Role): calculate position correctly when rawPositions are equal (#9872) 2023-10-09 07:22:55 +00:00
Jaw0r3k
5f6a82d349 feat(StageInstanceManager): add guildScheduledEvent to create() (#9024)
* Update StageInstanceManager.js

* typings

* docs

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-10-03 18:31:19 +00:00
Jaw0r3k
759c0b0d46 feat: support default_thread_rate_limit_per_user in channel creation (#9339)
* feat: support default_thread_rate_limit_per_user in channel creation

* feat: add rawDataTypes

* fix: remove other rawTypes

* chore: missing comma

* types: undo all raw data changes

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-09-20 16:41:34 +00:00
Jaw0r3k
90ca02880a perf: linear speed position getters (#9528)
* perf(Channel): linear speed position getter (#9497)

Co-authored-by: kyra <kyradiscord@gmail.com>

* perf(Role): linear speed position getter

---------

Co-authored-by: kyra <kyradiscord@gmail.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-09-06 10:24:19 +00:00
Elysia
cb11c56a0b feat: user avatar decorations (#9710)
* feat: user avatar decorations

* lint

* Update typings/index.d.ts

Co-authored-by: David Malchin <malchin459@gmail.com>

---------

Co-authored-by: David Malchin <malchin459@gmail.com>
2023-07-15 17:25:14 +00:00
Jiralite
630b9d51ef fix(ChannelUpdate): Check against unknown channels (#9698)
fix(ChannelUpdate): check against unknown channels
2023-07-11 06:32:22 +00:00
Elysia
f9e9843a92 feat: support new username system (#9634)
* feat: support new username system (v13)

* fix(User): check global name in equals

* Update typings/index.d.ts

Co-authored-by: Jaw0r3k <jaworekwiadomosci@gmail.com>

* Update src/util/Util.js

Co-authored-by: Jaw0r3k <jaworekwiadomosci@gmail.com>

* typing

* Update User.js

* Update index.d.ts

* Update User.js

---------

Co-authored-by: Jaw0r3k <jaworekwiadomosci@gmail.com>
2023-07-08 17:01:01 +00:00
Souji
85338ef073 feat(presence): feature parity in image resolve (#9638)
* PR: !9637 for v14
2023-06-14 17:39:22 +00:00
Jiralite
4c072048be chore: Run format script (#9531)
* chore: run Prettier

* ci: ensure this is checked

* chore: requested changes

* style: more changes
2023-05-07 14:39:41 +00:00
Jiralite
24ac27df4d docs: Fix AutoModerationRuleTriggerTypes link (#9530)
docs: fix `AutoModerationRuleTriggerTypes` link
2023-05-07 14:18:11 +00:00
Jaw0r3k
5057f04304 feat: Safety alerts channel and mention raid protection (#9073)
* feat: safety alerts channel and mention raid protection

* feat: add raw types

* Apply suggestions from code review

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* docs: update guild features

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-05-07 12:44:44 +00:00
Jaw0r3k
c5a42ed5ec feat(GuildAuditLogsEntry): add missing channel property to extra (#9527)
feat(GuildAuditLogsEntry#extra): add missing `channel` property (#9518)

Co-authored-by: Synbulat Biishev <syjalo.dev@gmail.com>
2023-05-07 00:54:06 +00:00
iCrawl
ad57e88744 chore(discord.js): release discord.js@13.16.0 2023-05-06 11:09:51 +02:00
Jiralite
53d347734f fix(Constants): Ordering and missing information (#9500)
fix(Constants): ordering and missing information
2023-05-02 10:44:10 +00:00
Jaw0r3k
21dfac90ac fix: Miscellaneous fixes (#9445)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Almeida <almeidx@pm.me>
Co-authored-by: kyranet <kyradiscord@gmail.com>
Fix embeds and components (#9437)
fix links and invalid syntax (#9322)
2023-05-01 21:03:19 +02:00
Hampus Kraft
d867936fce feat: update Discord developer documentation links and add new flags (#9473)
- Update developer documentation links in WebSocketShard.js, ActivityFlags.js, Constants.js, and MessageFlags.js
- Add new flags in ApplicationFlags.js, MessageFlags.js, Permissions.js, and SystemChannelFlags.js
- Update typings in index.d.ts for the added flags
2023-04-29 01:15:19 +00:00
Jaw0r3k
3386bab2c0 feat: voice messages (#9444)
* feat: voice messages

* fix: delete unnesseary error
2023-04-23 19:03:29 +00:00
pkdev08
420f379933 feat: add ACTIVE_DEVELOPER user flag (#9428)
* feat: add `ACTIVE_DEVELOPER` user flag

* add trailing comma + typings
2023-04-20 11:34:59 +00:00
Jaw0r3k
4f26ba7c2a feat v13: Support pagination for fetching thread members (#9045)
* feat: support pagnation

* feat: support pagnation

* feat: fix tests

* fix: better fetch

* fix: tests

* Apply suggestions from code review

Co-authored-by: Aura Román <kyradiscord@gmail.com>

* fix: bad merge

---------

Co-authored-by: Aura Román <kyradiscord@gmail.com>
2023-04-17 13:59:25 +00:00
Ben
71161518ca feat(roleTagData): v13 add guildConnections (#9391)
* feat(roleTagData): add guildConnections

* feat(roleTagData): add guildConnections
2023-04-15 19:44:17 +00:00
Jaw0r3k
28a5c7b125 feat: v13 VIEW_CREATOR_MONETIZATION_ANALYTICS and USE_SOUNDBOARD permissions (#9124)
* feat: view_creator_monetization_analytics perm

* feat: also add USE_SOUNDBOARD
2023-04-14 21:33:56 +00:00
Jaw0r3k
7cf9224c46 feat(StageChannel): v13 support messages (#9145)
Co-authored-by: kyranet <kyradiscord@gmail.com>
2023-04-14 23:32:58 +02:00
Jaw0r3k
add14acc20 feat(AutoModeration): v13 support customMessage (#9173)
* feat: add customMessage

* chore: use main wording
2023-04-13 16:36:46 +00:00
space
5acecf031a chore(discord.js): release discord.js@13.15.1 (#9340) 2023-04-07 13:02:38 +00:00
Jiralite
a8d5325def fix(Message): Fix permissions check in locked threads (#9338)
fix(Message#editable): fix permissions check if channel is thread & locked

Co-authored-by: Erwan <erwan977@gmail.com>
2023-04-07 12:26:16 +00:00
Jiralite
09ca243c2f fix(ClientUser): Fix modifying self (#9318)
fix(ClientUser): fix modifying
2023-04-07 12:25:45 +00:00
Steamed_EGG
8f12054c06 fix: Typo in src/util/MessageFlags.js (#9312) 2023-04-02 12:44:31 +02:00
Jiralite
f79a9b5450 fix(AutocompleteInteraction): Fix responding (#9315) 2023-04-02 12:42:30 +02:00
iCrawl
48f7193ef1 chore: fix changelog 2023-04-02 02:41:39 +02:00
iCrawl
1da4596820 chore: lockfile 2023-04-02 02:38:06 +02:00
iCrawl
3197ad7d1f chore: dont delete gitkeep files 2023-04-02 02:35:41 +02:00
iCrawl
6e11b846bb chore(discord.js): release discord.js@13.15.0 2023-04-02 02:33:26 +02:00
iCrawl
491f268b90 chore: git ignore 2023-04-02 02:25:56 +02:00
Jiralite
a51ddb2b06 chore: Miscellaneous fixes (#9271)
* fix(Message#editable): update editable check in threads locked (#9216)

* fix(Message#editable): update editable check in threads locked

* fix(Message#editable): add check in archived threads

* fix: check manage threads permission only if thread is locked

* fix: adding a full stop at the end of a sentence

Co-authored-by: Jaworek <jaworekwiadomosci@gmail.com>

---------

Co-authored-by: Jaworek <jaworekwiadomosci@gmail.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>

* fix(ThreadManager): Respect `cache` and `force` in fetching (#9239)

* fix(ThreadManager): Respect `cache` and `force` in fetching

* refactor: remove defaults

These are already defaulted down the line.

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* refactor(FetchThreadsOptions): Remove `active` (#9241)

* refactor(FetchThreadsOptions): remove `active`

* docs(FetchThreadsOptions): update description

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* docs: add more examples (#9252)

* docs: add more examples

* fix: fix grammar and syntax

Co-authored-by: Jaw0r3k <jaworekwiadomosci@gmail.com>

* chore: fine-tune examples

* chore: replace double quotes with singles

* fix: remove redundant example tag

* fix: fix timeout logging

* Update packages/discord.js/src/structures/GuildMember.js

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

---------

Co-authored-by: Jaw0r3k <jaworekwiadomosci@gmail.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* docs(Role): Fix example for `comparePositionTo()`

* docs(FetchArchivedThreadOptions): `before` respects `archive_timestamp`, not creation timestamp (#9240)

docs(FetchArchivedThreadOptions): correct `before` description

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* fix(snowflake): snowflakes length (#9144)

* fix(snowflake): fix snowflakes length

* fix(snowflake): fix length

* fix(Message): `bulkDeletable` permissions should be retrieved later for DMs (#9146)

* fix(Message): permissions check should be done later

the getter will error if used on a message originating from a DM

* refactor: remove unessercary chaining

* fix: invalid backport

Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>

* docs(Role): fix comparison example

* fix(ClientUser): no mutation on edit

* refactor: call bans.create directly

* fix(AutocompleteInteraction): Send `name_localizations` correctly (#9238)

fix(AutocompleteInteraction): send locale correctly

Co-authored-by: space <spaceeec@yahoo.com>

* fix: resolving string bitfield (#9262)

fix: resolving bitfield

Co-authored-by: space <spaceeec@yahoo.com>

* fix: Keep symbols in actions manager (#9293)

fix: keep symbols in actions manager

* fix: add support for new guild feature `GUILD_WEB_PAGE_VANITY_URL` (#9219)

Co-authored-by: space <spaceeec@yahoo.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* docs: differ `User#send` (#9251)

* docs: differate user#send

* chore: format

* chore: remove some examples

* docs: add GuildMember#send example

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* docs: describe private properties (#8879)

* feat: describe private properties

* Update packages/discord.js/src/structures/GuildMember.js

Co-authored-by: MrMythicalYT <91077061+MrMythicalYT@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: MrMythicalYT <91077061+MrMythicalYT@users.noreply.github.com>

* Apply suggestions from code review

---------

Co-authored-by: MrMythicalYT <91077061+MrMythicalYT@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

---------

Co-authored-by: Erwan <erwan977@gmail.com>
Co-authored-by: Jaworek <jaworekwiadomosci@gmail.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Tetie <tjvssr@gmail.com>
Co-authored-by: DraftMan <contact@draftman.fr>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: Jaw0r3k <jaw0r3k.g@gmail.com>
Co-authored-by: space <spaceeec@yahoo.com>
Co-authored-by: pkdev08 <54294685+pkdev08@users.noreply.github.com>
Co-authored-by: MrMythicalYT <91077061+MrMythicalYT@users.noreply.github.com>
2023-04-01 23:46:23 +00:00
Jaw0r3k
51c3bf1f54 feat: backport chatInputApplicationCommandMention (#9245)
feat: chatInputApplicationCommandMention formatter
2023-03-25 23:05:00 +00:00
space
b9b037b886 chore(discord.js): release discord.js@13.14.0 (#9227) 2023-03-12 17:47:00 +00:00
MrMythicalYT
af6a0e5d51 fix(ThreadChannel): fetch starter message properly (#9217) 2023-03-12 17:29:13 +00:00
Sammy
e15b70f79a Fix a rare error regarding interactions (#9218) 2023-03-12 17:28:25 +00:00
Vlad Frangu
df68520319 fix(Actions): inject built data by using a symbol (#9204) 2023-03-06 10:31:19 +00:00
Elysia
4bc25c40f5 feat: add support for gif stickers (v13) (#9158)
* update new sticker file type

* update

* lint
2023-03-05 22:43:46 +00:00
Jaworek
120270e8dc feat: v13 support SUPPRESS_NOTIFICATIONS flag (#9184)
feat: add suppress notifications flag
2023-03-03 17:54:54 +00:00
space
9f7d1f3be5 chore(discord.js): release discord.js@13.13.1 (#9156) 2023-02-21 22:54:31 +00:00
Jiralite
224f21c9c0 fix(Constants): enum creation error (#9155)
fix: enum creation error
2023-02-21 22:26:02 +00:00
space
7f1735d50a chore(discord.js): release discord.js@13.13.0 (#9154) 2023-02-21 22:09:52 +00:00
Jaworek
fd494a385e feat(GuildMemberManager): add addRole and removeRole methods (#9108)
* feat: addrole and removerole

* fix: apply suggestions

* Apply suggestions from code review

Co-authored-by: space <spaceeec@yahoo.com>

* fix: missdeleted ban :(

---------

Co-authored-by: space <spaceeec@yahoo.com>
2023-02-21 20:14:54 +00:00
ckohen
b586df884b ci: create publish release workflow v13 (#9151) 2023-02-20 20:58:46 +00:00
Jaworek
0188e36283 feat(WebSocketShard): v13 add resume url (#9078)
feat: use resume url
2023-02-17 23:07:47 +00:00
Jaworek
84d34dc258 feat: v13 guildAuditLogEntryCreate event (#9092)
* feat: guildAuditLogEntryCreate event

* Update src/client/actions/GuildAuditLogEntryCreate.js

Co-authored-by: Elysia <71698422+aiko-chan-ai@users.noreply.github.com>

* Update src/client/actions/GuildAuditLogEntryCreate.js

Co-authored-by: space <spaceeec@yahoo.com>

---------

Co-authored-by: Elysia <71698422+aiko-chan-ai@users.noreply.github.com>
Co-authored-by: space <spaceeec@yahoo.com>
2023-02-17 23:07:30 +00:00
Jiralite
7737bbe2fe ci: add pull request triage and Kodiak merge workflow (#9109)
* chore: enable kodiak for auto merges

* ci: add pull request triage

* ci: update kentaro-m/auto-assign-action

This supports Node.js 16.

---------

Co-authored-by: iCrawl <buechler.noel@outlook.com>
2023-02-18 00:06:55 +01:00
Jaworek
61fa6f45b4 feat(Constants): add auto moderation events to WSEvents (#9102)
* fix: add automoderation events to WSEvents

* chore: typings :)

* chore: undo unrelated changes

---------

Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2023-02-17 23:32:53 +01:00
Jiralite
0afa405f5a feat: role subscriptions (#9040)
* feat: add role subscriptions (#8915)

* feat: add role subscriptions

* docs: casing

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* chore: version 13 specific things

---------

Co-authored-by: Almeida <almeidx@pm.me>
2023-02-17 23:12:47 +01:00
Jiralite
eed293f893 fix: minor forums cleanup (#9033)
* docs(GuildChannelResolvable): remove extra `ForumChannel`

* docs: replace `GuildForumThreadChannel`

* docs: `Channel` correct reference

* refactor: move `threadName` to the right method
2023-02-17 23:02:38 +01:00
Jaworek
86329ad66f feat(GuildChannelManager): add addFollower (#9050)
* feat: addFolower method

* Update src/structures/NewsChannel.js

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-02-17 22:47:13 +01:00
Jaworek
69d71e967e feat(Guild): add INVITES_DISABLED feature support (#9051)
* feat: disable invites

* Update src/structures/Guild.js

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2023-02-17 22:45:10 +01:00
Jaworek
a7dc40f1a8 feat(ClientApplication): add role connections (#9072)
* feat: add add role connections

* feat: add add role connections

* fix: export new class in the index

* Update typings/rawDataTypes.d.ts

Co-authored-by: Aura Román <kyradiscord@gmail.com>

* chore: invite scope

* docs(ApplicationRoleConnectionMetadata): add docstring for the class

* docs(Constants): fix ApplicationRoleConnectionMetadataTypes jsdoc syntax

---------

Co-authored-by: Aura Román <kyradiscord@gmail.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2023-02-17 22:44:04 +01:00
Jaworek
32cdaff7eb feat(Webhook): add channel getter (#9074)
* fix: add channel property

* fix: missing new line
2023-02-17 22:25:32 +01:00
Jiralite
123d0f1aca fix(InteractionResponses): spell messages correctly (#9080)
as well as `automatically` in Util.js
2023-02-17 22:21:03 +01:00
Jaworek
d69529e3fe feat(GuildMember): add flags (#9098)
* feat: guildMember flags

* Apply suggestions from code review

Co-authored-by: Almeida <almeidx@pm.me>

* Update GuildMember.js

---------

Co-authored-by: Almeida <almeidx@pm.me>
2023-02-17 22:16:49 +01:00
Jaworek
c2968b58f9 feat: applicationCommandPermissionsUpdate event (#9121)
* fix: applicationcommandpermissionupdate

* fix: missing types

* Update index.d.ts
2023-02-17 22:05:19 +01:00
Jaworek
428798374f feat: backport automod (#8886)
Co-authored-by: Aura Román <kyradiscord@gmail.com>
2023-01-13 11:26:32 +01:00
Jiralite
cf3c7a7c54 docs(ThreadEditOptions): Move info tag back to invitable (#9021) 2023-01-12 09:23:46 +01:00
Jiralite
a941cb6ec5 fix(ThreadChannel): reason as second parameter (#9023) 2023-01-10 10:55:21 +01:00
Jiralite
6854df4218 types(ThreadEditOptions): Add appliedTags (#9022) 2023-01-10 10:53:03 +01:00
Jiralite
35f6dadebf types(widget): Add missing name (#9031)
Co-authored-by: Ben <88249114+BenjammingKirby@users.noreply.github.com>
2023-01-10 10:52:12 +01:00
Jiralite
1779e1ba7e docs: Add missing @extends (#9028) 2023-01-10 10:51:10 +01:00
Jaworek
11d010f177 feat v13: add not_found to guildMembersChunk data (#9032) 2023-01-10 10:50:34 +01:00
Jiralite
b01c81dd72 refactor: Move me and add fetchMe() (#9029) 2023-01-10 10:49:43 +01:00
Jiralite
f0d42644df feat(GuildAuditLogs): Support after (#9012) 2023-01-10 10:49:07 +01:00
Jiralite
64575195b5 fix: Import errors correctly (#9030) 2023-01-10 10:46:02 +01:00
iCrawl
5115de9862 chore: add gitkeep files to keep vercel from failing 2023-01-09 18:22:47 +01:00
Elysia
546ac43911 feat: backport guild forum support to v13 (#8651)
Co-authored-by: Jaworek <jaworekwiadomosci@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2023-01-02 16:21:15 +01:00
Voxelli
56e67185fc fix(websocketshard): backport zombie connection fix (#9003) 2023-01-02 15:58:08 +01:00
Jiralite
649058055a types: Swap message reaction and emoji identifier types (#8970)
Co-authored-by: Aura Román <kyradiscord@gmail.com>
2023-01-01 18:28:56 +01:00
Almeida
4ec3355961 fix(Util): flatten ignoring certain fields (v13) (#8936)
Fixes https://github.com/discordjs/discord.js/issues/8929
2022-12-16 14:13:43 +01:00
Synbulat Biishev
ca662b4de8 feat: add Message#bulkDeletable (v13) (#8761) 2022-11-25 18:36:34 +01:00
Raraph84
98846cf863 fix: backport allow deletion of ephemeral messages to v13 (#8811)
Co-authored-by: Noel <buechler.noel@outlook.com>
2022-11-25 18:35:19 +01:00
Jaworek
0e0851aa18 feat(InteractionResponses): add message parameter (v13) (#8838)
Co-authored-by: MrMythicalYT <91077061+MrMythicalYT@users.noreply.github.com>
Co-authored-by: Noel <buechler.noel@outlook.com>
2022-11-25 18:19:25 +01:00
Eejit
eecc50bfda fix(Activity): Fix equals() not checking for differing emoji (v13) (#8842)
Co-authored-by: Jaworek <jaworekwiadomosci@gmail.com>
2022-11-25 18:09:22 +01:00
RedGuy12
caf6f66073 fix(escapeMarkdown): fix double escaping (v13) (#8799) 2022-10-31 19:01:06 +01:00
iCrawl
c312da795e chore: changelog 2022-10-10 19:38:44 +02:00
iCrawl
312923d370 chore(discord.js): release discord.js@13.12.0 2022-10-10 19:36:50 +02:00
RedGuy12
8a6588a132 feat(Util): backport escapeMarkdown PRs to v13 (#8703) 2022-10-10 19:14:36 +02:00
vunsh
ea117bfb7e docs: update UserContextMenu documentation v13 (#8721)
Co-authored-by: A. Román <kyradiscord@gmail.com>
Co-authored-by: Noel <buechler.noel@outlook.com>
2022-10-09 23:05:10 +02:00
iCrawl
8d6a55d2c7 chore(discord.js): release discord.js@13.11.0 2022-09-15 20:44:27 +02:00
iCrawl
5ef30a0173 chore: deps 2022-09-15 20:30:24 +02:00
Almeida
8f94a9ca2f chore: fix typescript tests (v13) (#8628) 2022-09-15 20:21:15 +02:00
Almeida
fcd52d7fc6 docs: update misleading Client#guildMemberAvailable event description (v13) (#8627) 2022-09-15 20:14:15 +02:00
RedGuy12
f4e81330bf types(GuildChannelManager): correct fetch return type (v13) (#8551) 2022-09-13 09:10:47 +02:00
Jeroen Claassens
e6ee7d8374 fix: fixed TS 4.8 compatibility (#8601) 2022-09-08 15:02:25 +02:00
JsCoder2022
56177998c5 add MESSAGE_CONTENT intent (v13) (#8580) 2022-09-02 17:22:59 +02:00
Jiralite
ca68fc3f6b feat(GuildBanManager): Add deleteMessageSeconds (#8575) 2022-09-02 17:22:36 +02:00
iCrawl
a507ed9590 chore(release): discord.js 13.10.3 2022-08-22 11:42:06 +02:00
iCrawl
f0c0166814 chore: deps 2022-08-22 11:38:39 +02:00
Jiralite
10b12ccea6 types: Disallow some channel types from webhook creation (#8535) 2022-08-22 09:46:17 +02:00
Jiralite
526ea74e66 types(ModalMessageModalSubmitInteraction): channelId is not nullable (v13) (#8505)
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
2022-08-17 09:47:31 +02:00
Jiralite
b6f48ec84a Specify time unit in awaitModalSubmit (v13) (#8506)
Co-authored-by: Marcus Otterström <github@otterstrom.dev>
2022-08-17 09:47:19 +02:00
GodderE2D
11d69491e0 docs: fix broken discord support link (#8485) 2022-08-15 15:53:11 +02:00
iCrawl
30e89a401d chore(discord.js): release discord.js@13.10.2 2022-08-10 20:54:17 +02:00
RedGuy12
03c59e3a83 types(Message): correct bulkDelete return type (v13) (#8469) 2022-08-10 20:37:51 +02:00
iCrawl
9ce7e5edcf chore(discord.js): release discord.js@13.10.1 2022-08-10 20:22:28 +02:00
Jiralite
2a46d9f58e fix(ThreadChannel): Handle possibly null parent (v13) (#8467) 2022-08-10 20:17:21 +02:00
iCrawl
78e494b06e chore(discord.js): release discord.js@13.10.0 2022-08-10 19:40:09 +02:00
Jiralite
ae43bca8b0 feat(Guild): Add max_video_channel_users (v13) (#8424) 2022-08-08 11:05:46 +02:00
iCrawl
7321507559 chore(discord.js): release discord.js@13.9.2 2022-07-29 10:55:59 +02:00
Almeida
d0a4199760 fix(MessageMentions): ignoreRepliedUser option in has() (v13) (#8365) 2022-07-29 10:47:13 +02:00
Jiralite
96125079a2 fix(GuildChannelManager): allow unsetting rtcRegion (v13) (#8362)
Co-authored-by: SpaceEEC <24881032+SpaceEEC@users.noreply.github.com>
2022-07-26 09:28:44 +02:00
Jiralite
7b41fb6b5a chore: disable scope-case rule for commitlint (v13) (#8363)
chore: disable scope-case rule for commitlint

Co-Authored-By: Vlad Frangu <kingdgrizzle@gmail.com>

Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2022-07-25 19:40:19 +02:00
Jiralite
4f7c1e35c3 fix(ThreadChannel): Omit webhook fetching (v13) (#8352) 2022-07-24 17:26:34 +02:00
iCrawl
622c77ba7a chore(discord.js): release discord.js@13.9.1 2022-07-24 00:12:06 +02:00
pat
be35db2410 refactor(embed): deprecate addField (#8318)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Arjun Sharda <77706434+ArjunSharda@users.noreply.github.com>
Co-authored-by: Almeida <almeidx@pm.me>
2022-07-23 23:39:03 +02:00
Jiralite
e95caa7e45 refactor(Presence): Remove redundant date parsing (v13) (#8341) 2022-07-23 18:21:53 +02:00
iCrawl
5c1e558570 ci: add vercel check deploy branch script 2022-07-20 23:09:40 +02:00
Almeida
4cf05559a2 fix(ApplicationCommandManager): allow passing 0n to defaultMemberPermissions (v13) (#8312) 2022-07-20 20:12:28 +02:00
iCrawl
d9432aba71 ci: correct path to docs.json 2022-07-18 17:58:32 +02:00
iCrawl
f2a6f9fc1d ci: remove build step 2022-07-18 17:55:37 +02:00
iCrawl
da3d4873a7 ci: fix documentation deployment for v13 2022-07-18 17:53:35 +02:00
iCrawl
64928abb9e chore(discord.js): release discord.js@13.9.0 2022-07-17 19:39:23 +02:00
iCrawl
7b7cc1c6cb chore: deps 2022-07-17 19:38:43 +02:00
Cinnamon
00a705707e docs: add new HTTP Error Codes 50068 (v13) (#8273) 2022-07-17 19:10:43 +02:00
BattleEye
4d86cf4ce0 fix(PermissionOverwriteManager): mutates user (#8282)
Fix PermissionOverwriteManager changing userOrRole

Since it's mutated the original Member object won't be passed to upset and will be seen as invalid if User cache is disabled.

Functions normally even with User cache disabled after the fix.
2022-07-17 19:10:03 +02:00
Jiralite
beb3d8ec26 fix(GuildChannelManager): Access resolveId correctly (v13) (#8297) 2022-07-17 18:51:39 +02:00
muchnameless
8fe166dcfd fix(GuildChannelManager): edit lockPermissions (#8267) 2022-07-12 22:34:40 +02:00
Cinnamon
9cc336c43b docs: Add MessageActivityType (v13) (#8257) 2022-07-09 19:42:43 +02:00
MateoDeveloper
a93f4b1ba2 feat(ApplicationCommand): add min_length and max_length for string option (v13) (#8217) 2022-07-06 20:39:55 +02:00
Almeida
f457cdd2de fix(applicationcommandmanager): explicitly allow passing builders to methods (v13) (#8229) 2022-07-05 11:12:13 +02:00
Vlad Frangu
f704b261c0 fix: pass in the expected query object type for application commands (#8189) 2022-07-03 18:04:44 +02:00
Jiralite
631abee693 types(GuildMemberManager): Non-void return of edit() (v13) (#8187) 2022-07-03 18:04:35 +02:00
Superchupu
feb8e30d2e docs(MessageInteraction): update commandName description (v13) (#8220) 2022-07-03 15:43:10 +02:00
Jiralite
4063b90cef fix: Use non-global flag whilst resolving regular expressions (#8178)
fix(DataResolver): remove global flag on resolving
2022-06-30 00:39:48 +02:00
KinectTheUnknown
0e0f784447 fix(GuildStickerManager.fetchUser): Changed guildId to guild.id (#8176)
fix(GuildStickerManager.fetchUser): guildId to guild.id
2022-06-30 00:39:28 +02:00
Almeida
e8d72c7245 fix(guildmemberremove): remove member's presence for v13 (#8182)
Backports #8181
2022-06-30 00:38:08 +02:00
Almeida
4ae08ad9ef docs(constants): document missing constants (#8168) 2022-06-30 00:37:21 +02:00
Almeida
222fc9c679 feat(interaction): add appPermissions (v13) (#8195) 2022-06-30 00:36:07 +02:00
Almeida
079973f1cf types: add missing shard types (v13) (#8192) 2022-06-30 00:35:51 +02:00
Almeida
125696fc79 feat: partially backport perms v2 for v13 (#8162) 2022-06-24 00:05:11 +02:00
DD
c198e893c9 fix(WebSocketShard): backport error handler preservation on connections (#8164) 2022-06-23 21:13:33 +02:00
iCrawl
7e1904c2ad chore(release): version 2022-06-23 17:38:54 +02:00
Jiralite
c61fc8082a fix(VoiceChannel): NSFW property (v13) (#8161)
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
Co-authored-by: pat <73502164+nyapat@users.noreply.github.com>
2022-06-23 14:51:42 +02:00
Jiralite
65444f510d docs: TextBasedChannel-> TextBasedChannels typos (v13) (#8155) 2022-06-23 12:37:28 +02:00
KinectTheUnknown
70450f6873 typings(Shard#reconnecting): Backport to v13 - Fix event name (#8126) 2022-06-20 14:47:54 +02:00
Superchupu
3638b4021a refactor: deprecate $ prefix from ws.properties keys (#8095) 2022-06-17 23:26:57 +02:00
MateoDeveloper
0ab2227984 fix(ModalSubmitInteraction): add isFromMessage() missing method (#8092) 2022-06-15 01:02:03 +02:00
Voxelli
afb18b99b7 fix: destroy options during cleanup (#8082) 2022-06-13 20:03:56 +02:00
Rodry
613fd43fcf types(AutocompleteOption): backport fix and improve types (#8078) 2022-06-13 20:03:39 +02:00
Jiralite
3095f350e0 fix(AuditLog): default changes to empty array (#8076) 2022-06-13 20:03:22 +02:00
Synbulat Biishev
0d0190a6fd types(GuildChannel): fix type of .isText() method (#8061) 2022-06-13 20:03:04 +02:00
iCrawl
8f6df90035 chore(release): version 2022-06-05 19:28:12 +02:00
Almeida
876816ab2a fix(guildchannelmanager): wrong parameter in _sortedChannels call (#8011) 2022-06-05 19:17:38 +02:00
iCrawl
a8f2b2cfb4 chore: deps 2022-06-05 19:07:36 +02:00
Suneet Tipirneni
ddfe15b872 feat: backport text-in-voice support to v13 (#7999)
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
2022-06-05 18:30:48 +02:00
Voxelli
114bcc07a9 fix(websocketshard): deal with zombie connection caused by 4009 (#7581)
Co-authored-by: Almeida <almeidx@pm.me>
Co-authored-by: Vitor <milagre.vitor@gmail.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Parbez <imranbarbhuiya.fsd@gmail.com>
2022-06-05 09:38:31 +02:00
Josh Wee
76df9fdc45 fix: video quality mode data property (#7946) 2022-06-05 09:36:07 +02:00
GrapeColor
a51420f7f8 fix(ApplicationCommandOptionType): Add attachment to jsdoc (#7952)
Co-authored-by: GrapeColor <grapecolor@users.noreply.github.com>
2022-06-05 09:35:36 +02:00
iCrawl
e3cbd45e7d chore: release 2022-05-13 11:49:56 +02:00
Synbulat Biishev
ea28638a0c fix(MessageEmbed): fix a typo (#7906) 2022-05-12 10:24:54 +02:00
Almeida
43a7870b23 docs(shardingmanager): fix type of execArgv option (v13) (#7863) 2022-05-02 09:38:11 +02:00
Hyro
6dcf0bda05 docs: fix and improve localization docs (v13 backport) (#7807) 2022-04-21 19:06:28 +02:00
Almeida
816936eafb fix(GuildEditData): some fields can be null for v13 (#7633)
* fix(GuildEditData): some fields can be null for v13

* fix: make even more things nullable
2022-04-19 16:01:59 +02:00
Sasial
1d09ad4652 types: fix ModalSubmitInteraction (#7768) 2022-04-19 15:59:58 +02:00
Jiralite
5165b18b85 feat: backport (#7776) 2022-04-19 15:59:05 +02:00
Rodry
7afcd9594a types(threadchannel): fix autoArchiveDuration types (#7817) 2022-04-19 15:54:39 +02:00
Jiralite
b9802f4b6f refactor: deprecate v13 properties and methods (#7782)
* refactor: deprecate splitting

* refactor: deprecate `IntegrationApplication#summary`

https://github.com/discordjs/discord.js/pull/7729

* docs: amend store channel wording

* refactor: deprecate fetching of application assets

* docs: deprecate vip field in voice regions
2022-04-17 10:52:50 +02:00
Jiralite
1040ce0e71 docs(ApplicationCommand): Fix ApplicationCommandOptionChoice (#7798) 2022-04-17 10:47:34 +02:00
Jiralite
3eb45e30b3 feat: backport (#7787) 2022-04-14 12:48:31 +02:00
Jiralite
ab324ea6ae feat: backport (#7786) 2022-04-14 12:48:10 +02:00
Hyro
022e138b9a feat: add support for localized slash commands (v13 backport) (#7766) 2022-04-14 12:47:46 +02:00
Superchupu
9e4a900e6d feat: app authorization links and tags for v13 (#7731) 2022-04-14 12:47:11 +02:00
Jiralite
6c5613255a feat: backport (#7777) 2022-04-14 12:45:54 +02:00
Jiralite
ff49b82db7 feat: backport (#7778) 2022-04-14 12:45:35 +02:00
Jiralite
ae7f991e8d feat: backport (#7779) 2022-04-14 12:45:16 +02:00
Jiralite
cedc333940 feat: backport (#7783) 2022-04-14 12:44:24 +02:00
Jiralite
6daee1b235 feat(VoiceChannel): Support video_quality_mode (v13) (#7785) 2022-04-14 12:43:25 +02:00
Jiralite
68498a87be feat(StageInstance): add support for associated guild event (#7713) 2022-04-12 17:19:59 +02:00
Jiralite
ab6c2bad84 fix: apply v14 fix (#7756) 2022-04-12 17:11:57 +02:00
Almeida
c9e4562fd5 fix(GuildChannelManager): delete method accessing wrong id (#7771) 2022-04-12 17:08:57 +02:00
Ryan Munro
e1cdcfa9a6 feat(modals): modals, input text components and modal submits, v13 style (#7431) 2022-04-09 11:36:49 +02:00
Jiralite
5e8162a137 feat: Backport Interaction#isRepliable (#7563) 2022-04-09 11:36:15 +02:00
Rodry
9f09702854 feat: add methods to managers for v13 (#7611) 2022-04-09 11:35:17 +02:00
Jiralite
8e7d15e49d feat: Add premiumSubscriptionCount to InviteGuild (#7629) 2022-04-09 11:34:24 +02:00
Jiralite
b9c5676006 refactor: remove non-breaking stuff (#7636) 2022-04-09 11:33:44 +02:00
Almeida
dfea9c27ce fix(GuildScheduledEvent): handle missing image for v13 (#7627) 2022-03-24 20:59:19 +01:00
Jiralite
78140748ce types(InteractionCollector): Fix guild and channel types (#7624) 2022-03-10 09:00:58 +01:00
Ben
a7535a2232 feat(scheduledevents): Event cover images for v13 (#7613)
Co-authored-by: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com>
2022-03-07 19:26:57 +01:00
Rodry
7a52785f7d fix(messagementions): fix has method for v13 (#7591)
Co-authored-by: Almeida <almeidx@pm.me>
Co-authored-by: Synbulat Biishev <syjalo.dev@gmail.com>
2022-03-06 16:26:57 +01:00
Ben
13dd82d7fa fix: check if member has admininistrator on moderatable (v13) (#7578) 2022-03-02 10:38:04 +01:00
Jiralite
93cdb2f2fa feat: Backport MessageMentions channel type fixes (#7562) 2022-03-02 10:32:57 +01:00
Jiralite
611d3a7b2f feat: Backport cache types resolving to never (#7561) 2022-03-02 10:32:46 +01:00
Jiralite
29d42ed319 feat: Backport sending message flags (#7560) 2022-03-02 10:32:36 +01:00
Jiralite
1d97dcff08 feat(ThreadChannel): Backport creation timestamp (#7559) 2022-03-02 10:32:25 +01:00
Jiralite
679b87c4f8 feat: Add custom image support to version 13 (#7557) 2022-03-02 10:32:13 +01:00
Jiralite
b231bece0e feat: Backport reason on pin and unpin (#7556) 2022-03-02 10:32:03 +01:00
Jiralite
49397c0ca4 fix(ThreadChannel): Require sendable for unarchivable (#7555) 2022-03-02 10:31:51 +01:00
Jiralite
215dfe02d5 feat(GuildPreview): Add stickers to version 13 (#7554) 2022-03-02 10:31:41 +01:00
Jiralite
69ba067a65 docs: Backport version 13 fixes (#7552)
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
2022-03-02 10:31:28 +01:00
Jiralite
5f621c1995 fix: Backport MessageReaction#me being incorrectly false (#7553) 2022-03-02 10:30:13 +01:00
Jiralite
ee1698d928 feat: Backport sweepStickers method (#7558) 2022-03-02 10:29:59 +01:00
Ben
2fcf8af421 feat(scheduledevents): add image option (v13) (#7549) 2022-02-26 11:14:48 +01:00
EhsanFox
f0960698d2 fix(typings): sweepStageInstances typo (#7521) 2022-02-23 08:39:05 +01:00
ckohen
30baff7ecb fix(MessagePayload): v13 don't set reply flags to target flags (#7515) 2022-02-23 08:37:59 +01:00
Jiralite
2b3db734df feat(thread): v13 add newlyCreated to threadCreate event (#7481) 2022-02-20 13:42:23 +01:00
Jiralite
0b54089c43 types: V13 channel create overloads fix (#7480) 2022-02-20 13:39:20 +01:00
Jiralite
77b8e01911 fix(Shard): V13 EventEmitter listener warning (#7479) 2022-02-17 17:46:06 +01:00
Parbez
bc5ddc36fa fix(MessageEmbed): set footer to undefined (#7358) 2022-02-13 12:44:16 +01:00
Ryan Munro
5bcca8b97f feat(commands): attachment options (#7441) 2022-02-13 12:41:41 +01:00
iCrawl
988a51b764 chore(release): version 2022-01-13 18:24:17 +01:00
Rodry
1f4e633ce3 docs(interaction): add locale list link (#7261) 2022-01-13 18:20:35 +01:00
Suneet Tipirneni
233084a601 feat: add Locales to Interactions (#7131)
Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com>
2022-01-13 18:18:02 +01:00
iCrawl
ac8c122c2a chore(release): version 2022-01-07 23:57:17 +01:00
ckohen
2dabd82e26 fix(sweepers): provide default for object param (#7182) 2022-01-07 23:53:27 +01:00
170 changed files with 19903 additions and 16327 deletions

5
.cliff-jumperrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "discord.js",
"packagePath": ".",
"tagTemplate": "{{new-version}}"
}

View File

@@ -5,6 +5,7 @@
2,
"always",
["chore", "build", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "types", "typings"]
]
],
"scope-case": [0]
}
}

11
.github/.kodiak.toml vendored Normal file
View File

@@ -0,0 +1,11 @@
version = 1
[merge]
require_automerge_label = false
blocking_labels = ['blocked']
method = 'squash'
[merge.message]
title = 'pull_request_title'
strip_html_comments = true
include_coauthors = true

8
.github/auto_assign.yml vendored Normal file
View File

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

13
.github/check_deploy_branch.sh vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
git diff HEAD^ HEAD --quiet .
if [[ "$VERCEL_GIT_COMMIT_REF" == "main" && $? -eq 1 ]]; then
# Proceed with the build
echo "✅ - Proceed"
exit 1;
else
# Don't build
echo "🛑 - Build cancelled"
exit 0;
fi

15
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
apps:guide:
- apps/guide/*
- apps/guide/**/*
apps:website:
- apps/website/*
- apps/website/**/*
packages:discord.js:
- scripts/*
- scripts/**/*
- src/*
- src/**/*
- test/*
- test/**/*
- typings/*
- typings/**/*

View File

@@ -1,29 +0,0 @@
name: Deployment
on:
push:
branches:
- '*'
- '!docs'
tags:
- '*'
jobs:
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v16
uses: actions/setup-node@v2
with:
node-version: 16
cache: npm
- name: Install dependencies
run: npm ci
- name: Build and deploy documentation
uses: discordjs/action-docs@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

@@ -0,0 +1,102 @@
name: Documentation
on:
push:
branches:
- 'v13'
- '!docs'
tags:
- '**'
workflow_dispatch:
inputs:
ref:
description: 'The branch, tag or SHA to checkout'
required: true
jobs:
build:
name: Build documentation
runs-on: ubuntu-latest
if: github.repository_owner == 'discordjs'
outputs:
BRANCH_NAME: ${{ steps.env.outputs.BRANCH_NAME }}
BRANCH_OR_TAG: ${{ steps.env.outputs.BRANCH_OR_TAG }}
SHA: ${{ steps.env.outputs.SHA }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.ref || '' }}
- name: Install node.js v16
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Build docs
run: npm run docs
- name: Upload docgen artifacts
uses: actions/upload-artifact@v3
with:
name: docgen
path: docs/docs.json
- name: Set outputs for upload job
id: env
run: |
echo "::set-output name=BRANCH_NAME::${GITHUB_REF_NAME}"
echo "::set-output name=BRANCH_OR_TAG::${GITHUB_REF_TYPE}"
echo "::set-output name=SHA::${GITHUB_SHA}"
upload:
name: Upload Documentation
needs: build
runs-on: ubuntu-latest
env:
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@v3
- name: Install node.js v16
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Download docgen artifacts
uses: actions/download-artifact@v3
with:
name: docgen
path: docs
- name: Checkout docs repository
uses: actions/checkout@v3
with:
repository: 'discordjs/docs'
token: ${{ secrets.DJS_DOCS }}
path: 'out'
- name: Move docs to correct directory
run: |
mkdir -p out/discord.js
mv docs/docs.json out/discord.js/${BRANCH_NAME}.json
- name: Commit and push
run: |
cd out
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git add .
git commit -m "Docs build for ${BRANCH_OR_TAG} ${BRANCH_NAME}: ${SHA}" || true
git push

17
.github/workflows/pr-triage.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: 'PR Triage'
on:
pull_request_target:
jobs:
pr-triage:
name: PR Triage
runs-on: ubuntu-latest
steps:
- name: Automatically label PR
uses: actions/labeler@v4
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
sync-labels: true
- name: Automatically assign reviewers
if: github.event.action == 'opened'
uses: kentaro-m/auto-assign-action@v1.2.4

30
.github/workflows/publish-release.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Publish Release
on:
release:
types: [released]
jobs:
npm-publish:
name: npm publish
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
if: github.repository_owner == 'discordjs'
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install node.js v16
uses: actions/setup-node@v3
with:
node-version: 16
registry-url: https://registry.npmjs.org/
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Publish package (v13)
run: npm publish --tag v13-lts
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

@@ -2,7 +2,7 @@ name: Tests
on: [push, pull_request]
jobs:
lint:
name: ESLint
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -17,7 +17,7 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Run ESLint
- name: Run Prettier and ESLint
run: npm run lint
typings:

3
.gitignore vendored
View File

@@ -28,3 +28,6 @@ docs/docs.json
.tmp/
.idea/
.DS_Store
.yarn/
.turbo/
.vercel/

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
<a href="https://discord.gg/djs"><img src="https://img.shields.io/discord/222078108977594368?color=5865F2&logo=discord&logoColor=white" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="npm version" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="npm downloads" /></a>
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/workflows/Testing/badge.svg" alt="Tests status" /></a>
<a href="https://github.com/discordjs/discord.js/actions"><img src="https://github.com/discordjs/discord.js/actions/workflows/test.yml/badge.svg" alt="Tests status" /></a>
</p>
</div>
@@ -100,9 +100,9 @@ client.login('token');
## 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.
- [Documentation](https://old.discordjs.dev/#/docs)
- [Guide](https://v13.discordjs.guide) ([source](https://github.com/discordjs/guide/tree/v13))
See also the [Update Guide](https://v13.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)
@@ -116,7 +116,7 @@ client.login('token');
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs).
[documentation](https://old.discordjs.dev/#/docs).
See [the contribution guide](https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help

0
apps/guide/.gitkeep Normal file
View File

0
apps/website/.gitkeep Normal file
View File

View File

@@ -1,33 +1,36 @@
[changelog]
header = """
# Changelog
All notable changes to this project will be documented in this file.\n
"""
body = """
{% if version %}\
# [{{ version | trim_start_matches(pat="v") }}]\
{% if previous %}\
{% if previous.version %}\
(https://github.com/discordjs/discord.js/compare/{{ previous.version }}...{{ version }})\
{% else %}
(https://github.com/discordjs/discord.js/tree/{{ version }}\
{% endif %}\
{% endif %} \
- ({{ timestamp | date(format="%Y-%m-%d") }})
# [{{ 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 %}\
{% for breakingChange in commit.footers %}\
\n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\
{% endfor %}\
{% endif %}\
{% endfor %}
{% endfor %}\n
"""
trim = true
@@ -37,25 +40,25 @@ 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]*"
skip_tags = "v[0-9]*|11|12"
skip_tags = "v[0-9]*|@discordjs"
ignore_tags = ""
topo_order = false
topo_order = true
sort_commits = "newest"

View File

@@ -1 +1 @@
## [View the documentation here.](https://discord.js.org/#/docs)
## [View the documentation here.](https://old.discordjs.dev/#/docs)

19281
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,20 @@
{
"name": "discord.js",
"version": "14.0.0-dev",
"version": "13.17.1",
"description": "A powerful library for interacting with the Discord API",
"scripts": {
"test": "npm run lint && npm run docs:test && npm run lint:typings && npm run test:typescript",
"test:typescript": "tsc --noEmit && tsd",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint": "prettier --check src/**/*.js typings/**/*.ts && eslint src",
"lint:fix": "npm run format && eslint src --fix",
"lint:typings": "tslint typings/index.d.ts",
"format": "prettier --write src/**/*.js typings/**/*.ts",
"prepare": "is-ci || husky install",
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --source src --custom docs/index.yml",
"prepublishOnly": "npm run test",
"changelog": "git cliff --prepend CHANGELOG.md -l"
"changelog": "git cliff --prepend CHANGELOG.md -u",
"release": "cliff-jumper"
},
"main": "./src/index.js",
"types": "./typings/index.d.ts",
@@ -50,36 +51,37 @@
},
"homepage": "https://discord.js.org",
"dependencies": {
"@discordjs/builders": "^0.11.0",
"@discordjs/collection": "^0.4.0",
"@sapphire/async-queue": "^1.1.9",
"@types/node-fetch": "^2.5.12",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.26.0",
"@discordjs/builders": "^0.16.0",
"@discordjs/collection": "^0.7.0",
"@sapphire/async-queue": "^1.5.0",
"@types/node-fetch": "^2.6.3",
"@types/ws": "^8.5.4",
"discord-api-types": "^0.33.5",
"form-data": "^4.0.0",
"node-fetch": "^2.6.1",
"ws": "^8.4.0"
"node-fetch": "^2.6.7",
"ws": "^8.13.0"
},
"devDependencies": {
"@commitlint/cli": "^16.0.1",
"@commitlint/config-angular": "^16.0.0",
"@discordjs/docgen": "^0.11.0",
"@favware/npm-deprecate": "^1.0.4",
"@types/node": "^16.11.12",
"@commitlint/cli": "^17.5.1",
"@commitlint/config-angular": "^17.4.4",
"@discordjs/docgen": "^0.11.1",
"@favware/cliff-jumper": "^2.0.0",
"@favware/npm-deprecate": "^1.0.7",
"@types/node": "^16.11.45",
"conventional-changelog-cli": "^2.2.2",
"dtslint": "^4.2.1",
"eslint": "^8.5.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4",
"eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^27.4.5",
"lint-staged": "^12.1.4",
"prettier": "^2.5.1",
"tsd": "^0.19.0",
"jest": "^29.5.0",
"lint-staged": "^13.2.0",
"prettier": "^2.8.7",
"tsd": "^0.28.1",
"tslint": "^6.1.3",
"typescript": "^4.5.4"
"typescript": "^5.0.3"
},
"engines": {
"node": ">=16.6.0",

View File

@@ -16,7 +16,18 @@ async function writeWebsocketHandlerImports() {
}
async function writeClientActionImports() {
const lines = ["'use strict';\n", 'class ActionsManager {', ' constructor(client) {', ' this.client = client;\n'];
const lines = [
"'use strict';\n",
'class ActionsManager {',
' constructor(client) {',
' this.client = client;\n',
' // These symbols represent fully built data that we inject at times when calling actions manually.',
' // Action#getUser for example, will return the injected data (which is assumed to be a built structure)',
' // instead of trying to make it from provided data',
" this.injectedUser = Symbol('djs.actions.injectedUser');",
" this.injectedChannel = Symbol('djs.actions.injectedChannel');",
" this.injectedMessage = Symbol('djs.actions.injectedMessage');\n",
];
const actionsDirectory = new URL('../src/client/actions', import.meta.url);
for (const file of (await readdir(actionsDirectory)).sort()) {

View File

@@ -617,5 +617,5 @@ module.exports = Client;
/**
* @external Collection
* @see {@link https://discord.js.org/#/docs/collection/main/class/Collection}
* @see {@link https://discord.js.org/docs/packages/collection/stable/Collection:Class}
*/

View File

@@ -32,26 +32,31 @@ class GenericAction {
}
getChannel(data) {
const payloadData = {};
const id = data.channel_id ?? data.id;
if ('recipients' in data) {
payloadData.recipients = data.recipients;
} else {
// Try to resolve the recipient, but do not add the client user.
const recipient = data.author ?? data.user ?? { id: data.user_id };
if (recipient.id !== this.client.user.id) payloadData.recipients = [recipient];
}
if (id !== undefined) payloadData.id = id;
if ('guild_id' in data) payloadData.guild_id = data.guild_id;
if ('last_message_id' in data) payloadData.last_message_id = data.last_message_id;
return (
data.channel ??
this.getPayload(
{
id,
guild_id: data.guild_id,
recipients: [data.author ?? data.user ?? { id: data.user_id }],
},
this.client.channels,
id,
PartialTypes.CHANNEL,
)
data[this.client.actions.injectedChannel] ??
this.getPayload(payloadData, this.client.channels, id, PartialTypes.CHANNEL)
);
}
getMessage(data, channel, cache) {
const id = data.message_id ?? data.id;
return (
data.message ??
data[this.client.actions.injectedMessage] ??
this.getPayload(
{
id,
@@ -86,7 +91,7 @@ class GenericAction {
getUser(data) {
const id = data.user_id;
return data.user ?? this.getPayload({ id }, this.client.users, id, PartialTypes.USER);
return data[this.client.actions.injectedUser] ?? this.getPayload({ id }, this.client.users, id, PartialTypes.USER);
}
getUserFromMember(data) {

View File

@@ -4,9 +4,22 @@ class ActionsManager {
constructor(client) {
this.client = client;
// These symbols represent fully built data that we inject at times when calling actions manually.
// Action#getUser for example, will return the injected data (which is assumed to be a built structure)
// instead of trying to make it from provided data
this.injectedUser = Symbol('djs.actions.injectedUser');
this.injectedChannel = Symbol('djs.actions.injectedChannel');
this.injectedMessage = Symbol('djs.actions.injectedMessage');
this.register(require('./ApplicationCommandPermissionsUpdate'));
this.register(require('./AutoModerationActionExecution'));
this.register(require('./AutoModerationRuleCreate'));
this.register(require('./AutoModerationRuleDelete'));
this.register(require('./AutoModerationRuleUpdate'));
this.register(require('./ChannelCreate'));
this.register(require('./ChannelDelete'));
this.register(require('./ChannelUpdate'));
this.register(require('./GuildAuditLogEntryCreate'));
this.register(require('./GuildBanAdd'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildChannelsPositionUpdate'));

View File

@@ -0,0 +1,34 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
/**
* The data received in the {@link Client#event:applicationCommandPermissionsUpdate} event
* @typedef {Object} ApplicationCommandPermissionsUpdateData
* @property {Snowflake} id The id of the command or global entity that was updated
* @property {Snowflake} guildId The id of the guild in which permissions were updated
* @property {Snowflake} applicationId The id of the application that owns the command or entity being updated
* @property {ApplicationCommandPermissions[]} permissions The updated permissions
*/
class ApplicationCommandPermissionsUpdateAction extends Action {
handle(data) {
const client = this.client;
/**
* Emitted whenever permissions for an application command in a guild were updated.
* <warn>This includes permission updates for other applications in addition to the logged in client,
* check `data.applicationId` to verify which application the update is for</warn>
* @event Client#applicationCommandPermissionsUpdate
* @param {ApplicationCommandPermissionsUpdateData} data The updated permissions
*/
client.emit(Events.APPLICATION_COMMAND_PERMISSIONS_UPDATE, {
permissions: data.permissions,
id: data.id,
guildId: data.guild_id,
applicationId: data.application_id,
});
}
}
module.exports = ApplicationCommandPermissionsUpdateAction;

View File

@@ -0,0 +1,26 @@
'use strict';
const Action = require('./Action');
const AutoModerationActionExecution = require('../../structures/AutoModerationActionExecution');
const { Events } = require('../../util/Constants');
class AutoModerationActionExecutionAction extends Action {
handle(data) {
const { client } = this;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
/**
* Emitted whenever an auto moderation rule is triggered.
* <info>This event requires the {@link Permissions.FLAGS.MANAGE_GUILD} permission.</info>
* @event Client#autoModerationActionExecution
* @param {AutoModerationActionExecution} autoModerationActionExecution The data of the execution
*/
client.emit(Events.AUTO_MODERATION_ACTION_EXECUTION, new AutoModerationActionExecution(data, guild));
}
return {};
}
}
module.exports = AutoModerationActionExecutionAction;

View File

@@ -0,0 +1,27 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class AutoModerationRuleCreateAction extends Action {
handle(data) {
const { client } = this;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const autoModerationRule = guild.autoModerationRules._add(data);
/**
* Emitted whenever an auto moderation rule is created.
* <info>This event requires the {@link Permissions.FLAGS.MANAGE_GUILD} permission.</info>
* @event Client#autoModerationRuleCreate
* @param {AutoModerationRule} autoModerationRule The created auto moderation rule
*/
client.emit(Events.AUTO_MODERATION_RULE_CREATE, autoModerationRule);
}
return {};
}
}
module.exports = AutoModerationRuleCreateAction;

View File

@@ -0,0 +1,31 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class AutoModerationRuleDeleteAction extends Action {
handle(data) {
const { client } = this;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const autoModerationRule = guild.autoModerationRules.cache.get(data.id);
if (autoModerationRule) {
guild.autoModerationRules.cache.delete(autoModerationRule.id);
/**
* Emitted whenever an auto moderation rule is deleted.
* <info>This event requires the {@link Permissions.FLAGS.MANAGE_GUILD} permission.</info>
* @event Client#autoModerationRuleDelete
* @param {AutoModerationRule} autoModerationRule The deleted auto moderation rule
*/
client.emit(Events.AUTO_MODERATION_RULE_DELETE, autoModerationRule);
}
}
return {};
}
}
module.exports = AutoModerationRuleDeleteAction;

View File

@@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
class AutoModerationRuleUpdateAction extends Action {
handle(data) {
const { client } = this;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const oldAutoModerationRule = guild.autoModerationRules.cache.get(data.id)?._clone() ?? null;
const newAutoModerationRule = guild.autoModerationRules._add(data);
/**
* Emitted whenever an auto moderation rule gets updated.
* <info>This event requires the {@link Permissions.FLAGS.MANAGE_GUILD} permission.</info>
* @event Client#autoModerationRuleUpdate
* @param {?AutoModerationRule} oldAutoModerationRule The auto moderation rule before the update
* @param {AutoModerationRule} newAutoModerationRule The auto moderation rule after the update
*/
client.emit(Events.AUTO_MODERATION_RULE_UPDATE, oldAutoModerationRule, newAutoModerationRule);
}
return {};
}
}
module.exports = AutoModerationRuleUpdateAction;

View File

@@ -7,14 +7,23 @@ const { ChannelTypes } = require('../../util/Constants');
class ChannelUpdateAction extends Action {
handle(data) {
const client = this.client;
let channel = client.channels.cache.get(data.id);
if (channel) {
const old = channel._update(data);
if (ChannelTypes[channel.type] !== data.type) {
const newChannel = Channel.create(this.client, data, channel.guild);
for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message);
if (!newChannel) {
this.client.channels.cache.delete(channel.id);
return {};
}
if (channel.isText() && newChannel.isText()) {
for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message);
}
channel = newChannel;
this.client.channels.cache.set(channel.id, channel);
}

View File

@@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const GuildAuditLogsEntry = require('../../structures/GuildAuditLogs').Entry;
const { Events } = require('../../util/Constants');
class GuildAuditLogEntryCreateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
let auditLogEntry;
if (guild) {
auditLogEntry = new GuildAuditLogsEntry(guild, data);
/**
* Emitted whenever a guild audit log entry is created.
* @event Client#guildAuditLogEntryCreate
* @param {GuildAuditLogsEntry} auditLogEntry The entry that was created
* @param {Guild} guild The guild where the entry was created
*/
client.emit(Events.GUILD_AUDIT_LOG_ENTRY_CREATE, auditLogEntry, guild);
}
return { auditLogEntry };
}
}
module.exports = GuildAuditLogEntryCreateAction;

View File

@@ -22,6 +22,7 @@ class GuildMemberRemoveAction extends Action {
*/
if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member);
}
guild.presences.cache.delete(data.user.id);
guild.voiceStates.cache.delete(data.user.id);
}
return { guild, member };

View File

@@ -30,7 +30,7 @@ class GuildMemberUpdateAction extends Action {
} else {
const newMember = guild.members._add(data);
/**
* Emitted whenever a member becomes available in a large guild.
* Emitted whenever a member becomes available.
* @event Client#guildMemberAvailable
* @param {GuildMember} member The member that became available
*/

View File

@@ -6,6 +6,7 @@ const AutocompleteInteraction = require('../../structures/AutocompleteInteractio
const ButtonInteraction = require('../../structures/ButtonInteraction');
const CommandInteraction = require('../../structures/CommandInteraction');
const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction');
const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction');
const SelectMenuInteraction = require('../../structures/SelectMenuInteraction');
const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction');
const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants');
@@ -17,9 +18,11 @@ class InteractionCreateAction extends Action {
const client = this.client;
// Resolve and cache partial channels for Interaction#channel getter
this.getChannel(data);
const channel = this.getChannel(data);
// Do not emit this for interactions that cache messages that are non-text-based.
let InteractionType;
switch (data.type) {
case InteractionTypes.APPLICATION_COMMAND:
switch (data.data.type) {
@@ -30,6 +33,7 @@ class InteractionCreateAction extends Action {
InteractionType = UserContextMenuInteraction;
break;
case ApplicationCommandTypes.MESSAGE:
if (channel && !channel.isText()) return;
InteractionType = MessageContextMenuInteraction;
break;
default:
@@ -41,6 +45,8 @@ class InteractionCreateAction extends Action {
}
break;
case InteractionTypes.MESSAGE_COMPONENT:
if (channel && !channel.isText()) return;
switch (data.data.component_type) {
case MessageComponentTypes.BUTTON:
InteractionType = ButtonInteraction;
@@ -59,6 +65,9 @@ class InteractionCreateAction extends Action {
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE:
InteractionType = AutocompleteInteraction;
break;
case InteractionTypes.MODAL_SUBMIT:
InteractionType = ModalSubmitInteraction;
break;
default:
client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
return;

View File

@@ -13,8 +13,9 @@ class ThreadCreateAction extends Action {
* Emitted whenever a thread is created or when the client user is added to a thread.
* @event Client#threadCreate
* @param {ThreadChannel} thread The thread that was created
* @param {boolean} newlyCreated Whether the thread was newly created
*/
client.emit(Events.THREAD_CREATE, thread);
client.emit(Events.THREAD_CREATE, thread, data.newly_created ?? false);
}
return { thread };
}

View File

@@ -10,7 +10,8 @@ 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|StageChannel|ForumChannel} channel
* The channel that had a webhook update
*/
if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel);
}

View File

@@ -20,7 +20,7 @@ const BeforeReadyWhitelist = [
WSEvents.GUILD_MEMBER_REMOVE,
];
const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1).map(Number);
const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(2).map(Number);
const UNRESUMABLE_CLOSE_CODES = [
RPCErrorCodes.UnknownError,
RPCErrorCodes.InvalidPermissions,
@@ -31,7 +31,7 @@ const UNRESUMABLE_CLOSE_CODES = [
* The WebSocket manager for this client.
* <info>This class forwards raw dispatch events,
* read more about it here {@link https://discord.com/developers/docs/topics/gateway}</info>
* @extends EventEmitter
* @extends {EventEmitter}
*/
class WebSocketManager extends EventEmitter {
constructor(client) {
@@ -216,13 +216,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.INVALID_SESSION, () => {

View File

@@ -1,9 +1,9 @@
'use strict';
const EventEmitter = require('node:events');
const { setTimeout, setInterval } = require('node:timers');
const { setTimeout, setInterval, clearTimeout } = require('node:timers');
const WebSocket = require('../../WebSocket');
const { Status, Events, ShardEvents, Opcodes, WSEvents } = require('../../util/Constants');
const { Status, Events, ShardEvents, Opcodes, WSEvents, WSCodes } = require('../../util/Constants');
const Intents = require('../../util/Intents');
const STATUS_KEYS = Object.keys(Status);
@@ -17,6 +17,7 @@ try {
/**
* Represents a Shard's WebSocket connection
* @extends {EventEmitter}
*/
class WebSocketShard extends EventEmitter {
constructor(manager, id) {
@@ -34,6 +35,13 @@ class WebSocketShard extends EventEmitter {
*/
this.id = id;
/**
* The resume URL for this shard
* @type {?string}
* @private
*/
this.resumeURL = null;
/**
* The current status of the shard
* @type {Status}
@@ -81,6 +89,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
@@ -126,6 +141,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
@@ -175,12 +198,14 @@ class WebSocketShard extends EventEmitter {
* or reject if we couldn't connect
*/
connect() {
const { gateway, client } = this.manager;
const { client } = this.manager;
if (this.connection?.readyState === WebSocket.OPEN && this.status === Status.READY) {
return Promise.resolve();
}
const gateway = this.resumeURL ?? this.manager.gateway;
return new Promise((resolve, reject) => {
const cleanup = () => {
this.removeListener(ShardEvents.CLOSE, onClose);
@@ -250,10 +275,11 @@ class WebSocketShard extends EventEmitter {
this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING;
this.setHelloTimeout();
this.setWsCloseTimeout(-1);
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);
@@ -340,21 +366,39 @@ 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();
// Having this after _cleanupConnection to just clean up the connection and not listen to ws.onclose
this.destroy({ reset: !this.sessionId, emit: false, log: false });
}
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: WSCodes[1011],
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
@@ -383,10 +427,11 @@ class WebSocketShard extends EventEmitter {
*/
this.emit(ShardEvents.READY);
this.resumeURL = packet.d.resume_gateway_url;
this.sessionId = packet.d.session_id;
this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id));
this.status = Status.WAITING_FOR_GUILDS;
this.debug(`[READY] Session ${this.sessionId}.`);
this.debug(`[READY] Session ${this.sessionId} | Resume url ${this.resumeURL}.`);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ReadyHeartbeat');
break;
@@ -432,6 +477,10 @@ class WebSocketShard extends EventEmitter {
// Set the status to reconnecting
this.status = Status.RECONNECTING;
// Finally, emit the INVALID_SESSION event
/**
* Emitted when the session has been invalidated.
* @event WebSocketShard#invalidSession
*/
this.emit(ShardEvents.INVALID_SESSION);
break;
case Opcodes.HEARTBEAT_ACK:
@@ -523,6 +572,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);
// Check if close event was emitted.
if (this.closeEmitted) {
this.debug(`[WebSocket] close was already emitted, assuming the connection was closed properly.`);
// Setting the variable false to check for zombie connections.
this.closeEmitted = false;
return;
}
this.debug(
// eslint-disable-next-line max-len
`[WebSocket] Close Emitted: ${this.closeEmitted} | did not close properly, assuming a zombie connection.\nEmitting close and reconnecting again.`,
);
if (this.connection) this._cleanupConnection();
this.emitClose({
code: 4009,
reason: 'Session time out.',
wasClean: false,
});
}, time);
}
/**
* Sets the heartbeat timer for this shard.
* @param {number} time If -1, clears the interval, any other number sets an interval
@@ -563,8 +653,7 @@ class WebSocketShard extends EventEmitter {
Sequence : ${this.sequence}
Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`,
);
this.destroy({ closeCode: 4009, reset: true });
this.destroy({ reset: true, closeCode: 4009 });
return;
}
@@ -646,7 +735,7 @@ class WebSocketShard extends EventEmitter {
/**
* Adds a packet to the queue to be sent to the gateway.
* <warn>If you use this method, make sure you understand that you need to provide
* a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
* a full [Payload](https://discord.com/developers/docs/topics/gateway-events#payload-structure).
* Do not use this method if you don't know what you're doing.</warn>
* @param {Object} data The full packet to send
* @param {boolean} [important=false] If this packet should be added first in queue
@@ -713,21 +802,30 @@ 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]}`);
// Remove listeners from the connection
this._cleanupConnection();
// 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();
@@ -737,6 +835,12 @@ class WebSocketShard extends EventEmitter {
this._emitDestroyed();
}
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;
@@ -746,8 +850,9 @@ class WebSocketShard extends EventEmitter {
// Step 4: Cache the old sequence (use to attempt a resume)
if (this.sequence !== -1) this.closeSequence = this.sequence;
// Step 5: Reset the sequence and session id if requested
// Step 5: Reset the sequence, resume URL and session id if requested
if (reset) {
this.resumeURL = null;
this.sequence = -1;
this.sessionId = null;
}
@@ -766,7 +871,8 @@ class WebSocketShard extends EventEmitter {
* @private
*/
_cleanupConnection() {
this.connection.onopen = this.connection.onclose = this.connection.onerror = this.connection.onmessage = null;
this.connection.onopen = this.connection.onclose = this.connection.onmessage = null;
this.connection.onerror = () => null;
}
/**

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.ApplicationCommandPermissionsUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.AutoModerationActionExecution.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.AutoModerationRuleCreate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.AutoModerationRuleDelete.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.AutoModerationRuleUpdate.handle(packet.d);
};

View File

@@ -0,0 +1,5 @@
'use strict';
module.exports = (client, packet) => {
client.actions.GuildAuditLogEntryCreate.handle(packet.d);
};

View File

@@ -8,6 +8,13 @@ module.exports = (client, { d: data }, shard) => {
if (!guild.available && !data.unavailable) {
// A newly available guild
guild._patch(data);
/**
* Emitted whenever a guild becomes available.
* @event Client#guildAvailable
* @param {Guild} guild The guild that became available
*/
client.emit(Events.GUILD_AVAILABLE, guild);
}
} else {
// A new guild

View File

@@ -19,6 +19,8 @@ module.exports = (client, { d: data }) => {
* @property {number} index Index of the received chunk
* @property {number} count Number of chunks the client should receive
* @property {?string} nonce Nonce for this chunk
* @property {Array<*>} notFound An array of whatever could not be found
* when using {@link Opcodes.REQUEST_GUILD_MEMBERS}
*/
/**
@@ -32,5 +34,6 @@ module.exports = (client, { d: data }) => {
count: data.chunk_count,
index: data.chunk_index,
nonce: data.nonce,
notFound: data.not_found,
});
};

View File

@@ -6,6 +6,11 @@ const handlers = Object.fromEntries([
['APPLICATION_COMMAND_CREATE', require('./APPLICATION_COMMAND_CREATE')],
['APPLICATION_COMMAND_DELETE', require('./APPLICATION_COMMAND_DELETE')],
['APPLICATION_COMMAND_UPDATE', require('./APPLICATION_COMMAND_UPDATE')],
['APPLICATION_COMMAND_PERMISSIONS_UPDATE', require('./APPLICATION_COMMAND_PERMISSIONS_UPDATE')],
['AUTO_MODERATION_ACTION_EXECUTION', require('./AUTO_MODERATION_ACTION_EXECUTION')],
['AUTO_MODERATION_RULE_CREATE', require('./AUTO_MODERATION_RULE_CREATE')],
['AUTO_MODERATION_RULE_DELETE', require('./AUTO_MODERATION_RULE_DELETE')],
['AUTO_MODERATION_RULE_UPDATE', require('./AUTO_MODERATION_RULE_UPDATE')],
['GUILD_CREATE', require('./GUILD_CREATE')],
['GUILD_DELETE', require('./GUILD_DELETE')],
['GUILD_UPDATE', require('./GUILD_UPDATE')],
@@ -56,6 +61,7 @@ const handlers = Object.fromEntries([
['GUILD_SCHEDULED_EVENT_DELETE', require('./GUILD_SCHEDULED_EVENT_DELETE')],
['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD')],
['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE')],
['GUILD_AUDIT_LOG_ENTRY_CREATE', require('./GUILD_AUDIT_LOG_ENTRY_CREATE')],
]);
module.exports = handlers;

View File

@@ -58,6 +58,14 @@ const Messages = {
SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string',
SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string',
TEXT_INPUT_CUSTOM_ID: 'TextInputComponent customId must be a string',
TEXT_INPUT_LABEL: 'TextInputComponent label must be a string',
TEXT_INPUT_PLACEHOLDER: 'TextInputComponent placeholder must be a string',
TEXT_INPUT_VALUE: 'TextInputComponent value must be a string',
MODAL_CUSTOM_ID: 'Modal customId must be a string',
MODAL_TITLE: 'Modal title must be a string',
INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`,
FILE_NOT_FOUND: file => `File could not be found: ${file}`,
@@ -137,6 +145,7 @@ const Messages = {
INTERACTION_ALREADY_REPLIED: 'The reply to this interaction has already been sent or deferred.',
INTERACTION_NOT_REPLIED: 'The reply to this interaction has not been sent or deferred.',
/** @deprecated */
INTERACTION_EPHEMERAL_REPLIED: 'Ephemeral responses cannot be deleted.',
COMMAND_INTERACTION_OPTION_NOT_FOUND: name => `Required option "${name}" not found.`,
@@ -148,11 +157,17 @@ const Messages = {
COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.',
AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.',
MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`,
MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) =>
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite',
NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,
SWEEP_FILTER_RETURN: 'The return value of the sweepFilter function was not false or a Function',
GUILD_FORUM_MESSAGE_REQUIRED: 'You must provide a message to create a guild forum thread',
};
for (const [name, message] of Object.entries(Messages)) register(name, message);

View File

@@ -11,6 +11,7 @@ exports.WebhookClient = require('./client/WebhookClient');
// Utilities
exports.ActivityFlags = require('./util/ActivityFlags');
exports.ApplicationFlags = require('./util/ApplicationFlags');
exports.AttachmentFlags = require('./util/AttachmentFlags');
exports.BaseManager = require('./managers/BaseManager');
exports.BitField = require('./util/BitField');
exports.Collection = require('@discordjs/collection').Collection;
@@ -18,6 +19,7 @@ exports.Constants = require('./util/Constants');
exports.DataResolver = require('./util/DataResolver');
exports.DiscordAPIError = require('./rest/DiscordAPIError');
exports.Formatters = require('./util/Formatters');
exports.GuildMemberFlags = require('./util/GuildMemberFlags');
exports.HTTPError = require('./rest/HTTPError');
exports.Intents = require('./util/Intents');
exports.LimitedCollection = require('./util/LimitedCollection');
@@ -25,6 +27,7 @@ exports.MessageFlags = require('./util/MessageFlags');
exports.Options = require('./util/Options');
exports.Permissions = require('./util/Permissions');
exports.RateLimitError = require('./rest/RateLimitError');
exports.RoleFlags = require('./util/RoleFlags');
exports.SnowflakeUtil = require('./util/SnowflakeUtil');
exports.Sweepers = require('./util/Sweepers');
exports.SystemChannelFlags = require('./util/SystemChannelFlags');
@@ -36,6 +39,7 @@ exports.version = require('../package.json').version;
// Managers
exports.ApplicationCommandManager = require('./managers/ApplicationCommandManager');
exports.ApplicationCommandPermissionsManager = require('./managers/ApplicationCommandPermissionsManager');
exports.AutoModerationRuleManager = require('./managers/AutoModerationRuleManager');
exports.BaseGuildEmojiManager = require('./managers/BaseGuildEmojiManager');
exports.CachedManager = require('./managers/CachedManager');
exports.ChannelManager = require('./managers/ChannelManager');
@@ -71,7 +75,11 @@ exports.Activity = require('./structures/Presence').Activity;
exports.AnonymousGuild = require('./structures/AnonymousGuild');
exports.Application = require('./structures/interfaces/Application');
exports.ApplicationCommand = require('./structures/ApplicationCommand');
exports.ApplicationRoleConnectionMetadata =
require('./structures/ApplicationRoleConnectionMetadata').ApplicationRoleConnectionMetadata;
exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction');
exports.AutoModerationActionExecution = require('./structures/AutoModerationActionExecution');
exports.AutoModerationRule = require('./structures/AutoModerationRule');
exports.Base = require('./structures/Base');
exports.BaseCommandInteraction = require('./structures/BaseCommandInteraction');
exports.BaseGuild = require('./structures/BaseGuild');
@@ -122,6 +130,8 @@ exports.MessageMentions = require('./structures/MessageMentions');
exports.MessagePayload = require('./structures/MessagePayload');
exports.MessageReaction = require('./structures/MessageReaction');
exports.MessageSelectMenu = require('./structures/MessageSelectMenu');
exports.Modal = require('./structures/Modal');
exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction');
exports.NewsChannel = require('./structures/NewsChannel');
exports.OAuth2Guild = require('./structures/OAuth2Guild');
exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
@@ -140,6 +150,7 @@ exports.StoreChannel = require('./structures/StoreChannel');
exports.Team = require('./structures/Team');
exports.TeamMember = require('./structures/TeamMember');
exports.TextChannel = require('./structures/TextChannel');
exports.TextInputComponent = require('./structures/TextInputComponent');
exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember');
exports.Typing = require('./structures/Typing');

View File

@@ -1,11 +1,13 @@
'use strict';
const { isJSONEncodable } = require('@discordjs/builders');
const { Collection } = require('@discordjs/collection');
const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ApplicationCommand = require('../structures/ApplicationCommand');
const { ApplicationCommandTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
/**
* Manages API methods for application commands and stores their cache.
@@ -53,6 +55,13 @@ class ApplicationCommandManager extends CachedManager {
* @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable
*/
/* eslint-disable max-len */
/**
* Data that resolves to the data of an ApplicationCommand
* @typedef {ApplicationCommandData|APIApplicationCommand|SlashCommandBuilder|ContextMenuCommandBuilder} ApplicationCommandDataResolvable
*/
/* eslint-enable max-len */
/**
* Options used to fetch data from Discord
* @typedef {Object} BaseFetchOptions
@@ -64,6 +73,8 @@ class ApplicationCommandManager extends CachedManager {
* Options used to fetch Application Commands from Discord
* @typedef {BaseFetchOptions} FetchApplicationCommandOptions
* @property {Snowflake} [guildId] The guild's id to fetch commands for, for when the guild is not cached
* @property {LocaleString} [locale] The locale to use when fetching this command
* @property {boolean} [withLocalizations] Whether to fetch all localization data
*/
/**
@@ -82,9 +93,9 @@ class ApplicationCommandManager extends CachedManager {
* .then(commands => console.log(`Fetched ${commands.size} commands`))
* .catch(console.error);
*/
async fetch(id, { guildId, cache = true, force = false } = {}) {
async fetch(id, { guildId, cache = true, force = false, locale, withLocalizations } = {}) {
if (typeof id === 'object') {
({ guildId, cache = true } = id);
({ guildId, cache = true, locale, withLocalizations } = id);
} else if (id) {
if (!force) {
const existing = this.cache.get(id);
@@ -94,13 +105,18 @@ class ApplicationCommandManager extends CachedManager {
return this._add(command, cache);
}
const data = await this.commandPath({ guildId }).get();
const data = await this.commandPath({ guildId }).get({
headers: {
'X-Discord-Locale': locale,
},
query: typeof withLocalizations === 'boolean' ? { with_localizations: withLocalizations } : undefined,
});
return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection());
}
/**
* Creates an application command.
* @param {ApplicationCommandData|APIApplicationCommand} command The command
* @param {ApplicationCommandDataResolvable} command The command
* @param {Snowflake} [guildId] The guild's id to create this command in,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {Promise<ApplicationCommand>}
@@ -122,7 +138,7 @@ class ApplicationCommandManager extends CachedManager {
/**
* Sets all the commands for this application or guild.
* @param {ApplicationCommandData[]|APIApplicationCommand[]} commands The commands
* @param {ApplicationCommandDataResolvable[]} commands The commands
* @param {Snowflake} [guildId] The guild's id to create the commands in,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {Promise<Collection<Snowflake, ApplicationCommand>>}
@@ -152,7 +168,7 @@ class ApplicationCommandManager extends CachedManager {
/**
* Edits an application command.
* @param {ApplicationCommandResolvable} command The command to edit
* @param {ApplicationCommandData|APIApplicationCommand} data The data to update the command with
* @param {Partial<ApplicationCommandDataResolvable>} data The data to update the command with
* @param {Snowflake} [guildId] The guild's id where the command registered,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {Promise<ApplicationCommand>}
@@ -199,19 +215,50 @@ class ApplicationCommandManager extends CachedManager {
/**
* Transforms an {@link ApplicationCommandData} object into something that can be used with the API.
* @param {ApplicationCommandData|APIApplicationCommand} command The command to transform
* @param {ApplicationCommandDataResolvable} command The command to transform
* @returns {APIApplicationCommand}
* @private
*/
static transformCommand(command) {
if (isJSONEncodable(command)) return command.toJSON();
let default_member_permissions;
if ('default_member_permissions' in command) {
default_member_permissions = command.default_member_permissions
? new Permissions(BigInt(command.default_member_permissions)).bitfield.toString()
: command.default_member_permissions;
}
if ('defaultMemberPermissions' in command) {
default_member_permissions =
command.defaultMemberPermissions !== null
? new Permissions(command.defaultMemberPermissions).bitfield.toString()
: command.defaultMemberPermissions;
}
return {
name: command.name,
name_localizations: command.nameLocalizations ?? command.name_localizations,
description: command.description,
description_localizations: command.descriptionLocalizations ?? command.description_localizations,
type: typeof command.type === 'number' ? command.type : ApplicationCommandTypes[command.type],
options: command.options?.map(o => ApplicationCommand.transformOption(o)),
default_permission: command.defaultPermission ?? command.default_permission,
default_member_permissions,
dm_permission: command.dmPermission ?? command.dm_permission,
};
}
}
module.exports = ApplicationCommandManager;
/**
* @external SlashCommandBuilder
* @see {@link https://discord.js.org/docs/packages/builders/stable/SlashCommandBuilder:Class}
*/
/**
* @external ContextMenuCommandBuilder
* @see {@link https://discord.js.org/docs/packages/builders/stable/ContextMenuCommandBuilder:Class}
*/

View File

@@ -0,0 +1,296 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager');
const AutoModerationRule = require('../structures/AutoModerationRule');
const {
AutoModerationRuleEventTypes,
AutoModerationRuleTriggerTypes,
AutoModerationActionTypes,
AutoModerationRuleKeywordPresetTypes,
} = require('../util/Constants');
/**
* Manages API methods for auto moderation rules and stores their cache.
* @extends {CachedManager}
*/
class AutoModerationRuleManager extends CachedManager {
constructor(guild, iterable) {
super(guild.client, AutoModerationRule, iterable);
/**
* The guild this manager belongs to.
* @type {Guild}
*/
this.guild = guild;
}
/**
* Resolves an {@link AutoModerationRuleResolvable} to an {@link AutoModerationRule} object.
* @method resolve
* @memberof AutoModerationRuleManager
* @instance
* @param {AutoModerationRuleResolvable} autoModerationRule The AutoModerationRule resolvable to resolve
* @returns {?AutoModerationRule}
*/
/**
* Resolves an {@link AutoModerationRuleResolvable} to a {@link AutoModerationRule} id.
* @method resolveId
* @memberof AutoModerationRuleManager
* @instance
* @param {AutoModerationRuleResolvable} autoModerationRule The AutoModerationRule resolvable to resolve
* @returns {?Snowflake}
*/
_add(data, cache) {
return super._add(data, cache, { extras: [this.guild] });
}
/**
* Options used to set the trigger metadata of an auto moderation rule.
* @typedef {Object} AutoModerationTriggerMetadataOptions
* @property {string[]} [keywordFilter] The substrings that will be searched for in the content
* @property {string[]} [regexPatterns] The regular expression patterns which will be matched against the content
* <info>Only Rust-flavored regular expressions are supported.</info>
* @property {AutoModerationRuleKeywordPresetType[]} [presets]
* The internally pre-defined wordsets which will be searched for in the content
* @property {string[]} [allowList] The substrings that will be exempt from triggering
* {@link AutoModerationRuleTriggerType.KEYWORD} and {@link AutoModerationRuleTriggerType.KEYWORD_PRESET}
* @property {?number} [mentionTotalLimit] The total number of role & user mentions allowed per message
* @property {boolean} [mentionRaidProtectionEnabled] Whether to automatically detect mention raids
*/
/**
* Options used to set the actions of an auto moderation rule.
* @typedef {Object} AutoModerationActionOptions
* @property {AutoModerationActionType} type The type of this auto moderation rule action
* @property {AutoModerationActionMetadataOptions} [metadata] Additional metadata needed during execution
* <info>This property is required if using a `type` of
* {@link AutoModerationActionType.SEND_ALERT_MESSAGE} or {@link AutoModerationActionType.TIMEOUT}.</info>
*/
/**
* Options used to set the metadata of an auto moderation rule action.
* @typedef {Object} AutoModerationActionMetadataOptions
* @property {GuildTextChannelResolvable|ThreadChannel} [channel] The channel to which content will be logged
* @property {number} [durationSeconds] The timeout duration in seconds
* @property {string} [customMessage] The custom message that is shown whenever a message is blocked
*/
/**
* Options used to create an auto moderation rule.
* @typedef {Object} AutoModerationRuleCreateOptions
* @property {string} name The name of the auto moderation rule
* @property {AutoModerationRuleEventType} eventType The event type of the auto moderation rule
* @property {AutoModerationRuleTriggerType} triggerType The trigger type of the auto moderation rule
* @property {AutoModerationTriggerMetadataOptions} [triggerMetadata] The trigger metadata of the auto moderation rule
* <info>This property is required if the following `triggerType`s are used:
* * {@link AutoModerationRuleTriggerType.KEYWORD KEYWORD}
* * {@link AutoModerationRuleTriggerType.KEYWORD_PRESET KEYWORD_PRESET}
* * {@link AutoModerationRuleTriggerType.MENTION_SPAM MENTION_SPAM}
* </info>
* @property {AutoModerationActionOptions[]} actions
* The actions that will execute when the auto moderation rule is triggered
* @property {boolean} [enabled] Whether the auto moderation rule should be enabled
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [exemptRoles]
* The roles that should not be affected by the auto moderation rule
* @property {Collection<Snowflake, GuildChannel|ThreadChannel>|GuildChannelResolvable[]} [exemptChannels]
* The channels that should not be affected by the auto moderation rule
* @property {string} [reason] The reason for creating the auto moderation rule
*/
/**
* Creates a new auto moderation rule.
* @param {AutoModerationRuleCreateOptions} options Options for creating the auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
async create({
name,
eventType,
triggerType,
triggerMetadata,
actions,
enabled,
exemptRoles,
exemptChannels,
reason,
}) {
const data = await this.client.api.guilds(this.guild.id)['auto-moderation'].rules.post({
data: {
name,
event_type: typeof eventType === 'number' ? eventType : AutoModerationRuleEventTypes[eventType],
trigger_type: typeof triggerType === 'number' ? triggerType : AutoModerationRuleTriggerTypes[triggerType],
trigger_metadata: triggerMetadata && {
keyword_filter: triggerMetadata.keywordFilter,
regex_patterns: triggerMetadata.regexPatterns,
presets: triggerMetadata.presets?.map(preset =>
typeof preset === 'number' ? preset : AutoModerationRuleKeywordPresetTypes[preset],
),
allow_list: triggerMetadata.allowList,
mention_total_limit: triggerMetadata.mentionTotalLimit,
mention_raid_protection_enabled: triggerMetadata.mentionRaidProtectionEnabled,
},
actions: actions.map(action => ({
type: typeof action.type === 'number' ? action.type : AutoModerationActionTypes[action.type],
metadata: {
duration_seconds: action.metadata?.durationSeconds,
channel_id: action.metadata?.channel && this.guild.channels.resolveId(action.metadata.channel),
custom_message: action.metadata?.customMessage,
},
})),
enabled,
exempt_roles: exemptRoles?.map(exemptRole => this.guild.roles.resolveId(exemptRole)),
exempt_channels: exemptChannels?.map(exemptChannel => this.guild.channels.resolveId(exemptChannel)),
},
reason,
});
return this._add(data);
}
/**
* Options used to edit an auto moderation rule.
* @typedef {Object} AutoModerationRuleEditOptions
* @property {string} [name] The name of the auto moderation rule
* @property {AutoModerationRuleEventType} [eventType] The event type of the auto moderation rule
* @property {AutoModerationTriggerMetadataOptions} [triggerMetadata] The trigger metadata of the auto moderation rule
* @property {AutoModerationActionOptions[]} [actions]
* The actions that will execute when the auto moderation rule is triggered
* @property {boolean} [enabled] Whether the auto moderation rule should be enabled
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [exemptRoles]
* The roles that should not be affected by the auto moderation rule
* @property {Collection<Snowflake, GuildChannel|ThreadChannel>|GuildChannelResolvable[]} [exemptChannels]
* The channels that should not be affected by the auto moderation rule
* @property {string} [reason] The reason for creating the auto moderation rule
*/
/**
* Edits an auto moderation rule.
* @param {AutoModerationRuleResolvable} autoModerationRule The auto moderation rule to edit
* @param {AutoModerationRuleEditOptions} options Options for editing the auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
async edit(
autoModerationRule,
{ name, eventType, triggerMetadata, actions, enabled, exemptRoles, exemptChannels, reason },
) {
const autoModerationRuleId = this.resolveId(autoModerationRule);
const data = await this.client.api
.guilds(this.guild.id)('auto-moderation')
.rules(autoModerationRuleId)
.patch({
data: {
name,
event_type: typeof eventType === 'number' ? eventType : AutoModerationRuleEventTypes[eventType],
trigger_metadata: triggerMetadata && {
keyword_filter: triggerMetadata.keywordFilter,
regex_patterns: triggerMetadata.regexPatterns,
presets: triggerMetadata.presets?.map(preset =>
typeof preset === 'number' ? preset : AutoModerationRuleKeywordPresetTypes[preset],
),
allow_list: triggerMetadata.allowList,
mention_total_limit: triggerMetadata.mentionTotalLimit,
mention_raid_protection_enabled: triggerMetadata.mentionRaidProtectionEnabled,
},
actions: actions?.map(action => ({
type: typeof action.type === 'number' ? action.type : AutoModerationActionTypes[action.type],
metadata: {
duration_seconds: action.metadata?.durationSeconds,
channel_id: action.metadata?.channel && this.guild.channels.resolveId(action.metadata.channel),
custom_message: action.metadata?.customMessage,
},
})),
enabled,
exempt_roles: exemptRoles?.map(exemptRole => this.guild.roles.resolveId(exemptRole)),
exempt_channels: exemptChannels?.map(exemptChannel => this.guild.channels.resolveId(exemptChannel)),
},
reason,
});
return this._add(data);
}
/**
* Data that can be resolved to give an AutoModerationRule object. This can be:
* * An AutoModerationRule
* * A Snowflake
* @typedef {AutoModerationRule|Snowflake} AutoModerationRuleResolvable
*/
/**
* Options used to fetch a single auto moderation rule from a guild.
* @typedef {BaseFetchOptions} FetchAutoModerationRuleOptions
* @property {AutoModerationRuleResolvable} autoModerationRule The auto moderation rule to fetch
*/
/**
* Options used to fetch all auto moderation rules from a guild.
* @typedef {Object} FetchAutoModerationRulesOptions
* @property {boolean} [cache] Whether to cache the fetched auto moderation rules
*/
/**
* Fetches auto moderation rules from Discord.
* @param {AutoModerationRuleResolvable|FetchAutoModerationRuleOptions|FetchAutoModerationRulesOptions} [options]
* Options for fetching auto moderation rule(s)
* @returns {Promise<AutoModerationRule|Collection<Snowflake, AutoModerationRule>>}
* @example
* // Fetch all auto moderation rules from a guild without caching
* guild.autoModerationRules.fetch({ cache: false })
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single auto moderation rule
* guild.autoModerationRules.fetch('979083472868098119')
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single auto moderation rule without checking cache and without caching
* guild.autoModerationRules.fetch({ autoModerationRule: '979083472868098119', cache: false, force: true })
* .then(console.log)
* .catch(console.error)
*/
fetch(options) {
if (!options) return this._fetchMany();
const { autoModerationRule, cache, force } = options;
const resolvedAutoModerationRule = this.resolveId(autoModerationRule ?? options);
if (resolvedAutoModerationRule) {
return this._fetchSingle({ autoModerationRule: resolvedAutoModerationRule, cache, force });
}
return this._fetchMany(options);
}
async _fetchSingle({ autoModerationRule, cache, force = false }) {
if (!force) {
const existing = this.cache.get(autoModerationRule);
if (existing) return existing;
}
const data = await this.client.api.guilds(this.guild.id)('auto-moderation').rules(autoModerationRule).get();
return this._add(data, cache);
}
async _fetchMany(options = {}) {
const data = await this.client.api.guilds(this.guild.id)('auto-moderation').rules.get();
return data.reduce(
(col, autoModerationRule) => col.set(autoModerationRule.id, this._add(autoModerationRule, options.cache)),
new Collection(),
);
}
/**
* Deletes an auto moderation rule.
* @param {AutoModerationRuleResolvable} autoModerationRule The auto moderation rule to delete
* @param {string} [reason] The reason for deleting the auto moderation rule
* @returns {Promise<void>}
*/
async delete(autoModerationRule, reason) {
const autoModerationRuleId = this.resolveId(autoModerationRule);
await this.client.api.guilds(this.guild.id)('auto-moderation').rules(autoModerationRuleId).delete({ reason });
}
}
module.exports = AutoModerationRuleManager;

View File

@@ -50,9 +50,9 @@ class BaseGuildEmojiManager extends CachedManager {
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * The `<a:name:id>`, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji
* * An EmojiResolvable
* * The `<a:name:id>`, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji
* * The Unicode representation of an emoji
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/

View File

@@ -12,6 +12,13 @@ class CachedManager extends DataManager {
constructor(client, holds, iterable) {
super(client, holds);
/**
* The private cache of items for this manager.
* @type {Collection}
* @private
* @readonly
* @name CachedManager#_cache
*/
Object.defineProperty(this, '_cache', { value: this.client.options.makeCache(this.constructor, this.holds) });
let cleanup = this._cache[_cleanupSymbol]?.();

View File

@@ -33,10 +33,10 @@ class ChannelManager extends CachedManager {
* @name ChannelManager#cache
*/
_add(data, guild, { cache = true, allowUnknownGuild = false, fromInteraction = false } = {}) {
_add(data, guild, { cache = true, allowUnknownGuild = false } = {}) {
const existing = this.cache.get(data.id);
if (existing) {
if (cache) existing._patch(data, fromInteraction);
if (cache) existing._patch(data);
guild?.channels?._add(existing);
if (ThreadChannelTypes.includes(existing.type)) {
existing.parent?.threads?._add(existing);
@@ -44,7 +44,7 @@ class ChannelManager extends CachedManager {
return existing;
}
const channel = Channel.create(this.client, data, guild, { allowUnknownGuild, fromInteraction });
const channel = Channel.create(this.client, data, guild, { allowUnknownGuild });
if (!channel) {
this.client.emit(Events.DEBUG, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`);

View File

@@ -1,11 +1,14 @@
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const GuildBan = require('../structures/GuildBan');
const { GuildMember } = require('../structures/GuildMember');
let deprecationEmittedForDays = false;
/**
* Manages API methods for GuildBans and stores their cache.
* @extends {CachedManager}
@@ -54,9 +57,12 @@ class GuildBanManager extends CachedManager {
*/
/**
* Options used to fetch all bans from a guild.
* Options used to fetch multiple bans from a guild.
* @typedef {Object} FetchBansOptions
* @property {boolean} cache Whether or not to cache the fetched bans
* @property {number} [limit] The maximum number of bans to return
* @property {Snowflake} [before] Consider only bans before this id
* @property {Snowflake} [after] Consider only bans after this id
* @property {boolean} [cache] Whether to cache the fetched bans
*/
/**
@@ -64,13 +70,13 @@ class GuildBanManager extends CachedManager {
* @param {UserResolvable|FetchBanOptions|FetchBansOptions} [options] Options for fetching guild ban(s)
* @returns {Promise<GuildBan|Collection<Snowflake, GuildBan>>}
* @example
* // Fetch all bans from a guild
* // Fetch multiple bans from a guild
* guild.bans.fetch()
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch all bans from a guild without caching
* guild.bans.fetch({ cache: false })
* // Fetch a maximum of 5 bans from a guild without caching
* guild.bans.fetch({ limit: 5, cache: false })
* .then(console.log)
* .catch(console.error);
* @example
@@ -91,14 +97,15 @@ class GuildBanManager extends CachedManager {
*/
fetch(options) {
if (!options) return this._fetchMany();
const user = this.client.users.resolveId(options);
if (user) return this._fetchSingle({ user, cache: true });
options.user &&= this.client.users.resolveId(options.user);
if (!options.user) {
if ('cache' in options) return this._fetchMany(options.cache);
const { user, cache, force, limit, before, after } = options;
const resolvedUser = this.client.users.resolveId(user ?? options);
if (resolvedUser) return this._fetchSingle({ user: resolvedUser, cache, force });
if (!before && !after && !limit && typeof cache === 'undefined') {
return Promise.reject(new Error('FETCH_BAN_RESOLVE_ID'));
}
return this._fetchSingle(options);
return this._fetchMany(options);
}
async _fetchSingle({ user, cache, force = false }) {
@@ -111,15 +118,20 @@ class GuildBanManager extends CachedManager {
return this._add(data, cache);
}
async _fetchMany(cache) {
const data = await this.client.api.guilds(this.guild.id).bans.get();
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, cache)), new Collection());
}
async _fetchMany(options = {}) {
const data = await this.client.api.guilds(this.guild.id).bans.get({
query: options,
});
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, options.cache)), new Collection());
}
/**
* Options used to ban a user from a guild.
* @typedef {Object} BanOptions
* @property {number} [days=0] Number of days of messages to delete, must be between 0 and 7, inclusive
* <warn>This property is deprecated. Use `deleteMessageSeconds` instead.</warn>
* @property {number} [deleteMessageSeconds] Number of seconds of messages to delete,
* must be between 0 and 604800 (7 days), inclusive
* @property {string} [reason] The reason for the ban
*/
@@ -136,15 +148,30 @@ class GuildBanManager extends CachedManager {
* .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`))
* .catch(console.error);
*/
async create(user, options = { days: 0 }) {
async create(user, options = {}) {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
const id = this.client.users.resolveId(user);
if (!id) throw new Error('BAN_RESOLVE_ID', true);
if (typeof options.days !== 'undefined' && !deprecationEmittedForDays) {
process.emitWarning(
'The days option for GuildBanManager#create() is deprecated. Use the deleteMessageSeconds option instead.',
'DeprecationWarning',
);
deprecationEmittedForDays = true;
}
await this.client.api
.guilds(this.guild.id)
.bans(id)
.put({
data: { delete_message_days: options.days },
data: {
delete_message_seconds:
typeof options.deleteMessageSeconds !== 'undefined'
? options.deleteMessageSeconds
: (options.days ?? 0) * 24 * 60 * 60,
},
reason: options.reason,
});
if (user instanceof GuildMember) return user;

View File

@@ -4,11 +4,22 @@ const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager');
const ThreadManager = require('./ThreadManager');
const { Error } = require('../errors');
const { Error, TypeError } = require('../errors');
const GuildChannel = require('../structures/GuildChannel');
const PermissionOverwrites = require('../structures/PermissionOverwrites');
const ThreadChannel = require('../structures/ThreadChannel');
const { ChannelTypes, ThreadChannelTypes } = require('../util/Constants');
const Webhook = require('../structures/Webhook');
const ChannelFlags = require('../util/ChannelFlags');
const {
ThreadChannelTypes,
ChannelTypes,
VideoQualityModes,
SortOrderTypes,
ForumLayoutTypes,
} = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Util = require('../util/Util');
const { resolveAutoArchiveMaxLimit, transformGuildForumTag, transformGuildDefaultReaction } = require('../util/Util');
let cacheWarningEmitted = false;
let storeChannelDeprecationEmitted = false;
@@ -134,6 +145,12 @@ class GuildChannelManager extends CachedManager {
position,
rateLimitPerUser,
rtcRegion,
videoQualityMode,
availableTags,
defaultReactionEmoji,
defaultSortOrder,
defaultForumLayout,
defaultThreadRateLimitPerUser,
reason,
} = {},
) {
@@ -141,6 +158,13 @@ class GuildChannelManager extends CachedManager {
permissionOverwrites &&= permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
const intType = typeof type === 'number' ? type : ChannelTypes[type] ?? ChannelTypes.GUILD_TEXT;
const videoMode = typeof videoQualityMode === 'number' ? videoQualityMode : VideoQualityModes[videoQualityMode];
const sortMode = typeof defaultSortOrder === 'number' ? defaultSortOrder : SortOrderTypes[defaultSortOrder];
const layoutMode =
typeof defaultForumLayout === 'number' ? defaultForumLayout : ForumLayoutTypes[defaultForumLayout];
if (intType === ChannelTypes.GUILD_STORE && !storeChannelDeprecationEmitted) {
storeChannelDeprecationEmitted = true;
process.emitWarning(
@@ -163,17 +187,199 @@ class GuildChannelManager extends CachedManager {
permission_overwrites: permissionOverwrites,
rate_limit_per_user: rateLimitPerUser,
rtc_region: rtcRegion,
video_quality_mode: videoMode,
available_tags: availableTags?.map(availableTag => transformGuildForumTag(availableTag)),
default_reaction_emoji: defaultReactionEmoji && transformGuildDefaultReaction(defaultReactionEmoji),
default_sort_order: sortMode,
default_forum_layout: layoutMode,
default_thread_rate_limit_per_user: defaultThreadRateLimitPerUser,
},
reason,
});
return this.client.actions.ChannelCreate.handle(data).channel;
}
/**
* Creates a webhook for the channel.
* @param {GuildChannelResolvable} channel The channel to create the webhook for
* @param {string} name The name of the webhook
* @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook
* @returns {Promise<Webhook>} Returns the created Webhook
* @example
* // Create a webhook for the current channel
* guild.channels.createWebhook('222197033908436994', 'Snek', {
* avatar: 'https://i.imgur.com/mI8XcpG.jpg',
* reason: 'Needed a cool new Webhook'
* })
* .then(console.log)
* .catch(console.error)
*/
async createWebhook(channel, name, { avatar, reason } = {}) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
if (typeof avatar === 'string' && !avatar.startsWith('data:')) {
avatar = await DataResolver.resolveImage(avatar);
}
const data = await this.client.api.channels[id].webhooks.post({
data: {
name,
avatar,
},
reason,
});
return new Webhook(this.client, data);
}
/**
* Adds the target channel to a channel's followers.
* @param {NewsChannel|Snowflake} channel The channel to follow
* @param {TextChannelResolvable} targetChannel The channel where published announcements will be posted at
* @param {string} [reason] Reason for creating the webhook
* @returns {Promise<Snowflake>} Returns created target webhook id.
*/
async addFollower(channel, targetChannel, reason) {
const channelId = this.resolveId(channel);
const targetChannelId = this.resolveId(targetChannel);
if (!channelId || !targetChannelId) throw new Error('GUILD_CHANNEL_RESOLVE');
const { webhook_id } = await this.client.api.channels[channelId].followers.post({
data: { webhook_channel_id: targetChannelId },
reason,
});
return webhook_id;
}
/**
* The data for a guild channel.
* @typedef {Object} ChannelData
* @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 {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
* @property {?CategoryChannelResolvable} [parent] The parent of the channel
* @property {boolean} [lockPermissions]
* Lock the permissions of the channel to what the parent's permissions are
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites for the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The RTC region of the channel
* @property {?VideoQualityMode|number} [videoQualityMode] The camera video quality mode of the channel
* @property {ChannelFlagsResolvable} [flags] The flags to set on the channel
* @property {GuildForumTagData[]} [availableTags] The tags to set as available in a forum channel
* @property {?DefaultReactionEmoji} [defaultReactionEmoji] The emoji to set as the default reaction emoji
* @property {number} [defaultThreadRateLimitPerUser] The rate limit per user (slowmode) to set on forum posts
* @property {?SortOrderType} [defaultSortOrder] The default sort order mode to set on the channel
*/
/**
* Edits the channel.
* @param {GuildChannelResolvable} channel The channel to edit
* @param {ChannelData} data The new data for the channel
* @param {string} [reason] Reason for editing this channel
* @returns {Promise<GuildChannel>}
* @example
* // Edit a channel
* guild.channels.edit('222197033908436994', { name: 'new-channel' })
* .then(console.log)
* .catch(console.error);
*/
async edit(channel, data, reason) {
channel = this.resolve(channel);
if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const parent = data.parent && this.client.channels.resolveId(data.parent);
if (typeof data.position !== 'undefined') await this.setPosition(channel, data.position, { reason });
let permission_overwrites = data.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild));
if (data.lockPermissions) {
if (parent) {
const newParent = this.guild.channels.resolve(parent);
if (newParent?.type === 'GUILD_CATEGORY') {
permission_overwrites = newParent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
} else if (channel.parent) {
permission_overwrites = channel.parent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
}
let defaultAutoArchiveDuration = data.defaultAutoArchiveDuration;
if (defaultAutoArchiveDuration === 'MAX') defaultAutoArchiveDuration = resolveAutoArchiveMaxLimit(this.guild);
const newData = await this.client.api.channels(channel.id).patch({
data: {
name: (data.name ?? channel.name).trim(),
type: data.type,
topic: data.topic,
nsfw: data.nsfw,
bitrate: data.bitrate ?? channel.bitrate,
user_limit: data.userLimit ?? channel.userLimit,
rtc_region: 'rtcRegion' in data ? data.rtcRegion : channel.rtcRegion,
video_quality_mode:
typeof data.videoQualityMode === 'string' ? VideoQualityModes[data.videoQualityMode] : data.videoQualityMode,
parent_id: parent,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: defaultAutoArchiveDuration,
permission_overwrites,
available_tags: data.availableTags?.map(availableTag => transformGuildForumTag(availableTag)),
default_reaction_emoji: data.defaultReactionEmoji && transformGuildDefaultReaction(data.defaultReactionEmoji),
default_thread_rate_limit_per_user: data.defaultThreadRateLimitPerUser,
flags: 'flags' in data ? ChannelFlags.resolve(data.flags) : undefined,
default_sort_order:
typeof data.defaultSortOrder === 'string' ? SortOrderTypes[data.defaultSortOrder] : data.defaultSortOrder,
},
reason,
});
return this.client.actions.ChannelUpdate.handle(newData).updated;
}
/**
* Sets a new position for the guild channel.
* @param {GuildChannelResolvable} channel The channel to set the position for
* @param {number} position The new position for the guild channel
* @param {SetChannelPositionOptions} [options] Options for setting position
* @returns {Promise<GuildChannel>}
* @example
* // Set a new channel position
* guild.channels.setPosition('222078374472843266', 2)
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
* .catch(console.error);
*/
async setPosition(channel, position, { relative, reason } = {}) {
channel = this.resolve(channel);
if (!channel) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const updatedChannels = await Util.setPosition(
channel,
position,
relative,
this.guild._sortedChannels(channel),
this.client.api.guilds(this.guild.id).channels,
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
return channel;
}
/**
* Obtains one or more guild channels from Discord, or the channel cache if they're already available.
* @param {Snowflake} [id] The channel's id
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<?GuildChannel|Collection<Snowflake, GuildChannel>>}
* @returns {Promise<?GuildChannel|ThreadChannel|Collection<Snowflake, ?GuildChannel>>}
* @example
* // Fetch all channels from the guild (excluding threads)
* message.guild.channels.fetch()
@@ -204,6 +410,39 @@ class GuildChannelManager extends CachedManager {
return channels;
}
/**
* Fetches all webhooks for the channel.
* @param {GuildChannelResolvable} channel The channel to fetch webhooks for
* @returns {Promise<Collection<Snowflake, Webhook>>}
* @example
* // Fetch webhooks
* guild.channels.fetchWebhooks('769862166131245066')
* .then(hooks => console.log(`This channel has ${hooks.size} hooks`))
* .catch(console.error);
*/
async fetchWebhooks(channel) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
const data = await this.client.api.channels[id].webhooks.get();
return data.reduce((hooks, hook) => hooks.set(hook.id, new Webhook(this.client, hook)), new Collection());
}
/**
* Data that can be resolved to give a Category Channel object. This can be:
* * A CategoryChannel object
* * A Snowflake
* @typedef {CategoryChannel|Snowflake} CategoryChannelResolvable
*/
/**
* The data needed for updating a channel's position.
* @typedef {Object} ChannelPosition
* @property {GuildChannel|Snowflake} channel Channel to update
* @property {number} [position] New position for the channel
* @property {CategoryChannelResolvable} [parent] Parent channel for this channel
* @property {boolean} [lockPermissions] If the overwrites should be locked to the parents overwrites
*/
/**
* Batch-updates the guild's channels' positions.
* <info>Only one channel's parent can be changed at a time</info>
@@ -219,7 +458,7 @@ class GuildChannelManager extends CachedManager {
id: this.client.channels.resolveId(r.channel),
position: r.position,
lock_permissions: r.lockPermissions,
parent_id: typeof r.parent !== 'undefined' ? this.channels.resolveId(r.parent) : undefined,
parent_id: typeof r.parent !== 'undefined' ? this.resolveId(r.parent) : undefined,
}));
await this.client.api.guilds(this.guild.id).channels.patch({ data: channelPositions });
@@ -243,6 +482,23 @@ class GuildChannelManager extends CachedManager {
const raw = await this.client.api.guilds(this.guild.id).threads.active.get();
return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache });
}
/**
* Deletes the channel.
* @param {GuildChannelResolvable} channel The channel to delete
* @param {string} [reason] Reason for deleting this channel
* @returns {Promise<void>}
* @example
* // Delete the channel
* guild.channels.delete('858850993013260338', 'making room for new channels')
* .then(console.log)
* .catch(console.error);
*/
async delete(channel, reason) {
const id = this.resolveId(channel);
if (!id) throw new TypeError('INVALID_TYPE', 'channel', 'GuildChannelResolvable');
await this.client.api.channels(id).delete({ reason });
}
}
module.exports = GuildChannelManager;

View File

@@ -2,8 +2,9 @@
const { Collection } = require('@discordjs/collection');
const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
const { TypeError } = require('../errors');
const { Error, TypeError } = require('../errors');
const DataResolver = require('../util/DataResolver');
const Permissions = require('../util/Permissions');
/**
* Manages API methods for GuildEmojis and stores their cache.
@@ -100,6 +101,71 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache));
return emojis;
}
/**
* Deletes an emoji.
* @param {EmojiResolvable} emoji The Emoji resolvable to delete
* @param {string} [reason] Reason for deleting the emoji
* @returns {Promise<void>}
*/
async delete(emoji, reason) {
const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
await this.client.api.guilds(this.guild.id).emojis(id).delete({ reason });
}
/**
* Edits an emoji.
* @param {EmojiResolvable} emoji The Emoji resolvable to edit
* @param {GuildEmojiEditData} data The new data for the emoji
* @param {string} [reason] Reason for editing this emoji
* @returns {Promise<GuildEmoji>}
*/
async edit(emoji, data, reason) {
const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
const roles = data.roles?.map(r => this.guild.roles.resolveId(r));
const newData = await this.client.api
.guilds(this.guild.id)
.emojis(id)
.patch({
data: {
name: data.name,
roles,
},
reason,
});
const existing = this.cache.get(id);
if (existing) {
const clone = existing._clone();
clone._patch(newData);
return clone;
}
return this._add(newData);
}
/**
* Fetches the author for this emoji
* @param {EmojiResolvable} emoji The emoji to fetch the author of
* @returns {Promise<User>}
*/
async fetchAuthor(emoji) {
emoji = this.resolve(emoji);
if (!emoji) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
if (emoji.managed) {
throw new Error('EMOJI_MANAGED');
}
const { me } = this.guild.members;
if (!me) throw new Error('GUILD_UNCACHED_ME');
if (!me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
}
const data = await this.client.api.guilds(this.guild.id).emojis(emoji.id).get();
emoji._patch(data);
return emoji.author;
}
}
module.exports = GuildEmojiManager;

View File

@@ -0,0 +1,91 @@
'use strict';
const ThreadManager = require('./ThreadManager');
const { TypeError } = require('../errors');
const MessagePayload = require('../structures/MessagePayload');
const { resolveAutoArchiveMaxLimit } = require('../util/Util');
/**
* Manages API methods for threads in forum channels and stores their cache.
* @extends {ThreadManager}
*/
class GuildForumThreadManager extends ThreadManager {
/**
* The channel this Manager belongs to
* @name GuildForumThreadManager#channel
* @type {ForumChannel}
*/
/**
* @typedef {BaseMessageOptions} GuildForumThreadMessageCreateOptions
* @property {StickerResolvable} [stickers] The stickers to send with the message
* @property {BitFieldResolvable} [flags] The flags to send with the message.
* Only `SUPPRESS_EMBEDS` and `SUPPRESS_NOTIFICATIONS` can be set.
*/
/**
* Options for creating a thread.
* @typedef {StartThreadOptions} GuildForumThreadCreateOptions
* @property {GuildForumThreadMessageCreateOptions|MessagePayload} message The message associated with the thread post
* @property {Snowflake[]} [appliedTags] The tags to apply to the thread
*/
/**
* Creates a new thread in the channel.
* @param {GuildForumThreadCreateOptions} [options] Options to create a new thread
* @returns {Promise<ThreadChannel>}
* @example
* // Create a new forum post
* forum.threads
* .create({
* name: 'Food Talk',
* autoArchiveDuration: 60,
* message: {
* content: 'Discuss your favorite food!',
* },
* reason: 'Needed a separate thread for food',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
message,
reason,
rateLimitPerUser,
appliedTags,
} = {}) {
if (!message) {
throw new TypeError('GUILD_FORUM_MESSAGE_REQUIRED');
}
let messagePayload;
if (message instanceof MessagePayload) {
messagePayload = message.resolveData();
} else {
messagePayload = MessagePayload.create(this, message).resolveData();
}
const { data: body, files } = await messagePayload.resolveFiles();
if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);
const data = await this.client.api.channels(this.channel.id).threads.post({
data: {
name,
auto_archive_duration: autoArchiveDuration,
rate_limit_per_user: rateLimitPerUser,
applied_tags: appliedTags,
message: body,
},
files,
reason,
});
return this.client.actions.ThreadCreate.handle(data).thread;
}
}
module.exports = GuildForumThreadManager;

View File

@@ -18,6 +18,7 @@ const {
VerificationLevels,
DefaultMessageNotificationLevels,
ExplicitContentFilterLevels,
VideoQualityModes,
} = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const Permissions = require('../util/Permissions');
@@ -94,6 +95,7 @@ class GuildManager extends CachedManager {
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the channel
* @property {?string} [rtcRegion] The RTC region of the channel
* @property {VideoQualityMode|number} [videoQualityMode] The camera video quality mode of the channel
* @property {PartialOverwriteData[]} [permissionOverwrites]
* Overwrites of the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) of the channel in seconds
@@ -200,6 +202,11 @@ class GuildManager extends CachedManager {
delete channel.rateLimitPerUser;
channel.rtc_region = channel.rtcRegion;
delete channel.rtcRegion;
channel.video_quality_mode =
typeof channel.videoQualityMode === 'string'
? VideoQualityModes[channel.videoQualityMode]
: channel.videoQualityMode;
delete channel.videoQualityMode;
if (!channel.permissionOverwrites) continue;
for (const overwrite of channel.permissionOverwrites) {

View File

@@ -1,3 +1,4 @@
/* eslint-disable newline-per-chained-call */
'use strict';
const { Buffer } = require('node:buffer');
@@ -9,6 +10,8 @@ const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel');
const { GuildMember } = require('../structures/GuildMember');
const { Role } = require('../structures/Role');
const { Events, Opcodes } = require('../util/Constants');
const { PartialTypes } = require('../util/Constants');
const GuildMemberFlags = require('../util/GuildMemberFlags');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
@@ -118,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(PartialTypes.GUILD_MEMBER)
? this._add({ user: { id: this.client.user.id } }, true)
: null)
);
}
/**
* Options used to fetch a single member from a guild.
* @typedef {BaseFetchOptions} FetchMemberOptions
@@ -189,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
@@ -236,6 +262,7 @@ class GuildMemberManager extends CachedManager {
* (if they are connected to voice), or `null` if you want to disconnect them from voice
* @property {DateResolvable|null} [communicationDisabledUntil] The date or timestamp
* for the member's communication to be disabled until. Provide `null` to enable communication again.
* @property {GuildMemberFlagsResolvable} [flags] The flags to set for the member
*/
/**
@@ -268,6 +295,8 @@ class GuildMemberManager extends CachedManager {
_data.communication_disabled_until =
_data.communicationDisabledUntil && new Date(_data.communicationDisabledUntil).toISOString();
_data.flags = _data.flags && GuildMemberFlags.resolve(_data.flags);
let endpoint = this.client.api.guilds(this.guild.id);
if (id === this.client.user.id) {
const keys = Object.keys(data);
@@ -353,7 +382,7 @@ class GuildMemberManager extends CachedManager {
* @example
* // Kick a user by id (or with a user/guild member object)
* guild.members.kick('84484653687267328')
* .then(banInfo => console.log(`Kicked ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`))
* .then(kickInfo => console.log(`Kicked ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`))
* .catch(console.error);
*/
async kick(user, reason) {
@@ -376,10 +405,10 @@ class GuildMemberManager extends CachedManager {
* @example
* // Ban a user by id (or with a user/guild member object)
* guild.members.ban('84484653687267328')
* .then(kickInfo => console.log(`Banned ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`))
* .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`))
* .catch(console.error);
*/
ban(user, options = { days: 0 }) {
ban(user, options) {
return this.guild.bans.create(user, options);
}
@@ -387,7 +416,7 @@ class GuildMemberManager extends CachedManager {
* Unbans a user from the guild. Internally calls the {@link GuildBanManager#remove} method.
* @param {UserResolvable} user The user to unban
* @param {string} [reason] Reason for unbanning user
* @returns {Promise<User>} The user that was unbanned
* @returns {Promise<?User>} The user that was unbanned
* @example
* // Unban a user by id (or with a user/guild member object)
* guild.members.unban('84484653687267328')
@@ -408,6 +437,38 @@ class GuildMemberManager extends CachedManager {
return this._add(data, cache);
}
/**
* Adds a role to a member.
* @param {GuildMemberResolvable} user The user to add the role from
* @param {RoleResolvable} role The role to add
* @param {string} [reason] Reason for adding the role
* @returns {Promise<GuildMember|User|Snowflake>}
*/
async addRole(user, role, reason) {
const userId = this.guild.members.resolveId(user);
const roleId = this.guild.roles.resolveId(role);
await this.client.api.guilds(this.guild.id).members(userId).roles(roleId).put({ reason });
return this.resolve(user) ?? this.client.users.resolve(user) ?? userId;
}
/**
* Removes a role from a member.
* @param {UserResolvable} user The user to remove the role from
* @param {RoleResolvable} role The role to remove
* @param {string} [reason] Reason for removing the role
* @returns {Promise<GuildMember|User|Snowflake>}
*/
async removeRole(user, role, reason) {
const userId = this.guild.members.resolveId(user);
const roleId = this.guild.roles.resolveId(role);
await this.client.api.guilds(this.guild.id).members(userId).roles(roleId).delete({ reason });
return this.resolve(user) ?? this.client.users.resolve(user) ?? userId;
}
_fetchMany({
limit = 0,
withPresences: presences = false,

View File

@@ -5,6 +5,7 @@ const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent');
const { PrivacyLevels, GuildScheduledEventEntityTypes, GuildScheduledEventStatuses } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
/**
* Manages API methods for GuildScheduledEvents and stores their cache.
@@ -49,6 +50,7 @@ class GuildScheduledEventManager extends CachedManager {
* @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the
* guild scheduled event
* <warn>This is required if `entityType` is 'EXTERNAL'</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for creating the guild scheduled event
*/
@@ -76,6 +78,7 @@ class GuildScheduledEventManager extends CachedManager {
scheduledEndTime,
entityMetadata,
reason,
image,
} = options;
if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel];
@@ -99,6 +102,7 @@ class GuildScheduledEventManager extends CachedManager {
scheduled_start_time: new Date(scheduledStartTime).toISOString(),
scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime,
description,
image: image && (await DataResolver.resolveImage(image)),
entity_type: entityType,
entity_metadata,
},
@@ -172,6 +176,7 @@ class GuildScheduledEventManager extends CachedManager {
* @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the
* guild scheduled event
* <warn>This can be modified only if `entityType` of the `GuildScheduledEvent` to be edited is 'EXTERNAL'</warn>
* @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event
* @property {string} [reason] The reason for editing the guild scheduled event
*/
@@ -197,6 +202,7 @@ class GuildScheduledEventManager extends CachedManager {
scheduledEndTime,
entityMetadata,
reason,
image,
} = options;
if (typeof privacyLevel === 'string') privacyLevel = PrivacyLevels[privacyLevel];
@@ -220,6 +226,7 @@ class GuildScheduledEventManager extends CachedManager {
description,
entity_type: entityType,
status,
image: image && (await DataResolver.resolveImage(image)),
entity_metadata,
},
reason,

View File

@@ -161,6 +161,19 @@ class GuildStickerManager extends CachedManager {
const data = await this.client.api.guilds(this.guild.id).stickers.get();
return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)]));
}
/**
* Fetches the user who uploaded this sticker, if this is a guild sticker.
* @param {StickerResolvable} sticker The sticker to fetch the user for
* @returns {Promise<?User>}
*/
async fetchUser(sticker) {
sticker = this.resolve(sticker);
if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
const data = await this.client.api.guilds(this.guild.id).stickers(sticker.id).get();
sticker._patch(data);
return sticker.user;
}
}
module.exports = GuildStickerManager;

View File

@@ -0,0 +1,98 @@
'use strict';
const ThreadManager = require('./ThreadManager');
const { TypeError } = require('../errors');
const { ChannelTypes } = require('../util/Constants');
const { resolveAutoArchiveMaxLimit } = require('../util/Util');
/**
* Manages API methods for {@link ThreadChannel} objects and stores their cache.
* @extends {ThreadManager}
*/
class GuildTextThreadManager extends ThreadManager {
/**
* The channel this Manager belongs to
* @name GuildTextThreadManager#channel
* @type {TextChannel|NewsChannel}
*/
/**
* Options for creating a thread. <warn>Only one of `startMessage` or `type` can be defined.</warn>
* @typedef {StartThreadOptions} GuildTextThreadCreateOptions
* @property {MessageResolvable} [startMessage] The message to start a thread from. <warn>If this is defined then type
* of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored</warn>
* @property {ThreadChannelTypes|number} [type] The type of thread to create. Defaults to `GUILD_PUBLIC_THREAD` if
* created in a {@link TextChannel} <warn>When creating threads in a {@link NewsChannel} this is ignored and is always
* `GUILD_NEWS_THREAD`</warn>
* @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread
* <info>Can only be set when type will be `GUILD_PRIVATE_THREAD`</info>
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds
*/
/**
* Creates a new thread in the channel.
* @param {GuildTextThreadCreateOptions} [options] Options to create a new thread
* @returns {Promise<ThreadChannel>}
* @example
* // Create a new public thread
* channel.threads
* .create({
* name: 'food-talk',
* autoArchiveDuration: 60,
* reason: 'Needed a separate thread for food',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
* @example
* // Create a new private thread
* channel.threads
* .create({
* name: 'mod-talk',
* autoArchiveDuration: 60,
* type: 'GUILD_PRIVATE_THREAD',
* reason: 'Needed a separate thread for moderation',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
startMessage,
type,
invitable,
reason,
rateLimitPerUser,
} = {}) {
let path = this.client.api.channels(this.channel.id);
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number');
}
let resolvedType =
this.channel.type === 'GUILD_NEWS' ? ChannelTypes.GUILD_NEWS_THREAD : ChannelTypes.GUILD_PUBLIC_THREAD;
if (startMessage) {
const startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
path = path.messages(startMessageId);
} else if (this.channel.type !== 'GUILD_NEWS') {
resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType;
}
if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);
const data = await path.threads.post({
data: {
name,
auto_archive_duration: autoArchiveDuration,
type: resolvedType,
invitable: resolvedType === ChannelTypes.GUILD_PRIVATE_THREAD ? invitable : undefined,
rate_limit_per_user: rateLimitPerUser,
},
reason,
});
return this.client.actions.ThreadCreate.handle(data).thread;
}
}
module.exports = GuildTextThreadManager;

View File

@@ -156,25 +156,27 @@ class MessageManager extends CachedManager {
/**
* Pins a message to the channel's pinned messages, even if it's not cached.
* @param {MessageResolvable} message The message to pin
* @param {string} [reason] Reason for pinning
* @returns {Promise<void>}
*/
async pin(message) {
async pin(message, reason) {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).put();
await this.client.api.channels(this.channel.id).pins(message).put({ reason });
}
/**
* Unpins a message from the channel's pinned messages, even if it's not cached.
* @param {MessageResolvable} message The message to unpin
* @param {string} [reason] Reason for unpinning
* @returns {Promise<void>}
*/
async unpin(message) {
async unpin(message, reason) {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).delete();
await this.client.api.channels(this.channel.id).pins(message).delete({ reason });
}
/**

View File

@@ -142,8 +142,9 @@ class PermissionOverwriteManager extends CachedManager {
* .catch(console.error);
*/
edit(userOrRole, options, overwriteOptions) {
userOrRole = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole);
const existing = this.cache.get(userOrRole);
const existing = this.cache.get(
this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole),
);
return this.upsert(userOrRole, options, overwriteOptions, existing);
}

View File

@@ -32,6 +32,7 @@ class ReactionManager extends CachedManager {
* Data that can be resolved to a MessageReaction object. This can be:
* * A MessageReaction
* * A Snowflake
* * The Unicode representation of an emoji
* @typedef {MessageReaction|Snowflake} MessageReactionResolvable
*/

View File

@@ -7,7 +7,8 @@ const { TypeError } = require('../errors');
const { Role } = require('../structures/Role');
const DataResolver = require('../util/DataResolver');
const Permissions = require('../util/Permissions');
const { resolveColor, setPosition } = require('../util/Util');
const { resolveColor } = require('../util/Util');
const Util = require('../util/Util');
let cacheWarningEmitted = false;
@@ -159,7 +160,7 @@ class RoleManager extends CachedManager {
guild_id: this.guild.id,
role: data,
});
if (position) return role.setPosition(position, reason);
if (position) return this.setPosition(role, position, { reason });
return role;
}
@@ -179,21 +180,7 @@ class RoleManager extends CachedManager {
role = this.resolve(role);
if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable');
if (typeof data.position === 'number') {
const updatedRoles = await setPosition(
role,
data.position,
false,
this.guild._sortedRoles(),
this.client.api.guilds(this.guild.id).roles,
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: updatedRoles,
});
}
if (typeof data.position === 'number') await this.setPosition(role, data.position, { reason });
let icon = data.icon;
if (icon) {
@@ -227,7 +214,7 @@ class RoleManager extends CachedManager {
* @example
* // Delete a role
* guild.roles.delete('222079219327434752', 'The role needed to go')
* .then(deleted => console.log(`Deleted role ${deleted.name}`))
* .then(() => console.log('Deleted the role.'))
* .catch(console.error);
*/
async delete(role, reason) {
@@ -236,6 +223,44 @@ class RoleManager extends CachedManager {
this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id });
}
/**
* Sets the new position of the role.
* @param {RoleResolvable} role The role to change the position of
* @param {number} position The new position for the role
* @param {SetRolePositionOptions} [options] Options for setting the position
* @returns {Promise<Role>}
* @example
* // Set the position of the role
* guild.roles.setPosition('222197033908436994', 1)
* .then(updated => console.log(`Role position: ${updated.position}`))
* .catch(console.error);
*/
async setPosition(role, position, { relative, reason } = {}) {
role = this.resolve(role);
if (!role) throw new TypeError('INVALID_TYPE', 'role', 'RoleResolvable');
const updatedRoles = await Util.setPosition(
role,
position,
relative,
this.guild._sortedRoles(),
this.client.api.guilds(this.guild.id).roles,
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: updatedRoles,
});
return role;
}
/**
* The data needed for updating a guild role's position
* @typedef {Object} GuildRolePosition
* @property {RoleResolvable} role The role's id
* @property {number} position The position to update
*/
/**
* Batch-updates the guild's role positions
* @param {GuildRolePosition[]} rolePositions Role positions to update
@@ -274,11 +299,14 @@ class RoleManager extends CachedManager {
const resolvedRole2 = this.resolve(role2);
if (!resolvedRole1 || !resolvedRole2) throw new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake');
if (resolvedRole1.position === resolvedRole2.position) {
const role1Position = resolvedRole1.position;
const role2Position = resolvedRole2.position;
if (role1Position === role2Position) {
return Number(BigInt(resolvedRole2.id) - BigInt(resolvedRole1.id));
}
return resolvedRole1.position - resolvedRole2.position;
return role1Position - role2Position;
}
/**

View File

@@ -31,6 +31,9 @@ class StageInstanceManager extends CachedManager {
* @typedef {Object} StageInstanceCreateOptions
* @property {string} topic The topic of the stage instance
* @property {PrivacyLevel|number} [privacyLevel] The privacy level of the stage instance
* @property {boolean} [sendStartNotification] Whether to notify `@everyone` that the stage instance has started
* @property {GuildScheduledEventResolvable} [guildScheduledEvent]
* The guild scheduled event associated with the stage instance
*/
/**
@@ -58,15 +61,18 @@ class StageInstanceManager extends CachedManager {
const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
let { topic, privacyLevel } = options;
let { guildScheduledEvent, topic, privacyLevel, sendStartNotification } = options;
privacyLevel &&= typeof privacyLevel === 'number' ? privacyLevel : PrivacyLevels[privacyLevel];
const guildScheduledEventId = guildScheduledEvent && this.resolveId(guildScheduledEvent);
const data = await this.client.api['stage-instances'].post({
data: {
channel_id: channelId,
topic,
privacy_level: privacyLevel,
send_start_notification: sendStartNotification,
guild_scheduled_event_id: guildScheduledEventId,
},
});

View File

@@ -4,7 +4,6 @@ const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ThreadChannel = require('../structures/ThreadChannel');
const { ChannelTypes } = require('../util/Constants');
/**
* Manages API methods for {@link ThreadChannel} objects and stores their cache.
@@ -60,94 +59,9 @@ class ThreadManager extends CachedManager {
*/
/**
* Options for creating a thread. <warn>Only one of `startMessage` or `type` can be defined.</warn>
* @typedef {StartThreadOptions} ThreadCreateOptions
* @property {MessageResolvable} [startMessage] The message to start a thread from. <warn>If this is defined then type
* of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored</warn>
* @property {ThreadChannelTypes|number} [type] The type of thread to create. Defaults to `GUILD_PUBLIC_THREAD` if
* created in a {@link TextChannel} <warn>When creating threads in a {@link NewsChannel} this is ignored and is always
* `GUILD_NEWS_THREAD`</warn>
* @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread
* <info>Can only be set when type will be `GUILD_PRIVATE_THREAD`</info>
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds
*/
/**
* Creates a new thread in the channel.
* @param {ThreadCreateOptions} [options] Options to create a new thread
* @returns {Promise<ThreadChannel>}
* @example
* // Create a new public thread
* channel.threads
* .create({
* name: 'food-talk',
* autoArchiveDuration: 60,
* reason: 'Needed a separate thread for food',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
* @example
* // Create a new private thread
* channel.threads
* .create({
* name: 'mod-talk',
* autoArchiveDuration: 60,
* type: 'GUILD_PRIVATE_THREAD',
* reason: 'Needed a separate thread for moderation',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
startMessage,
type,
invitable,
reason,
rateLimitPerUser,
} = {}) {
let path = this.client.api.channels(this.channel.id);
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number');
}
let resolvedType =
this.channel.type === 'GUILD_NEWS' ? ChannelTypes.GUILD_NEWS_THREAD : ChannelTypes.GUILD_PUBLIC_THREAD;
if (startMessage) {
const startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
path = path.messages(startMessageId);
} else if (this.channel.type !== 'GUILD_NEWS') {
resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType;
}
if (autoArchiveDuration === 'MAX') {
autoArchiveDuration = 1440;
if (this.channel.guild.features.includes('SEVEN_DAY_THREAD_ARCHIVE')) {
autoArchiveDuration = 10080;
} else if (this.channel.guild.features.includes('THREE_DAY_THREAD_ARCHIVE')) {
autoArchiveDuration = 4320;
}
}
const data = await path.threads.post({
data: {
name,
auto_archive_duration: autoArchiveDuration,
type: resolvedType,
invitable: resolvedType === ChannelTypes.GUILD_PRIVATE_THREAD ? invitable : undefined,
rate_limit_per_user: rateLimitPerUser,
},
reason,
});
return this.client.actions.ThreadCreate.handle(data).thread;
}
/**
* The options for fetching multiple threads, the properties are mutually exclusive
* Options for fetching multiple threads.
* @typedef {Object} FetchThreadsOptions
* @property {FetchArchivedThreadOptions} [archived] The options used to fetch archived threads
* @property {boolean} [active] When true, fetches active threads. <warn>If `archived` is set, this is ignored!</warn>
* @property {FetchArchivedThreadOptions} [archived] Options used to fetch archived threads
*/
/**
@@ -163,10 +77,10 @@ class ThreadManager extends CachedManager {
* .then(channel => console.log(channel.name))
* .catch(console.error);
*/
fetch(options, { cache = true, force = false } = {}) {
fetch(options, { cache, force } = {}) {
if (!options) return this.fetchActive(cache);
const channel = this.client.channels.resolveId(options);
if (channel) return this.client.channels.fetch(channel, cache, force);
if (channel) return this.client.channels.fetch(channel, { cache, force });
if (options.archived) {
return this.fetchArchived(options.archived, cache);
}
@@ -187,7 +101,7 @@ class ThreadManager extends CachedManager {
* @property {string} [type='public'] The type of threads to fetch, either `public` or `private`
* @property {boolean} [fetchAll=false] Whether to fetch **all** archived threads when type is `private`.
* Requires `MANAGE_THREADS` if true
* @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were created before this Date
* @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were archived before this Date
* or Snowflake. <warn>Must be a {@link ThreadChannelResolvable} when type is `private` and fetchAll is `false`</warn>
* @property {number} [limit] Maximum number of threads to return
*/
@@ -213,7 +127,7 @@ class ThreadManager extends CachedManager {
let timestamp;
let id;
if (typeof before !== 'undefined') {
if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) {
if (before instanceof ThreadChannel || /^\d{17,19}$/.test(String(before))) {
id = this.resolveId(before);
timestamp = this.resolve(before)?.archivedAt?.toISOString();
} else {

View File

@@ -1,10 +1,13 @@
'use strict';
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ThreadMember = require('../structures/ThreadMember');
let deprecationEmittedForPassingBoolean = false;
/**
* Manages API methods for GuildMembers and stores their cache.
* @extends {CachedManager}
@@ -28,14 +31,32 @@ class ThreadMemberManager extends CachedManager {
_add(data, cache = true) {
const existing = this.cache.get(data.user_id);
if (cache) existing?._patch(data);
if (cache) existing?._patch(data, { cache });
if (existing) return existing;
const member = new ThreadMember(this.thread, data);
const member = new ThreadMember(this.thread, data, { cache });
if (cache) this.cache.set(data.user_id, member);
return member;
}
/**
* Fetches the client user as a ThreadMember of the thread.
* @param {BaseFetchOptions} [options] The options for fetching the member
* @returns {Promise<ThreadMember>}
*/
fetchMe(options) {
return this.fetch(this.client.user.id, options);
}
/**
* The client user as a ThreadMember of this ThreadChannel
* @type {?ThreadMember}
* @readonly
*/
get me() {
return this.resolve(this.client.user.id);
}
/**
* Data that resolves to give a ThreadMember object. This can be:
* * A ThreadMember object
@@ -92,32 +113,73 @@ class ThreadMemberManager extends CachedManager {
return id;
}
async _fetchOne(memberId, cache, force) {
async _fetchOne(memberId, { cache, force = false, withMember }) {
if (!force) {
const existing = this.cache.get(memberId);
if (existing) return existing;
}
const data = await this.client.api.channels(this.thread.id, 'thread-members', memberId).get();
const data = await this.client.api.channels(this.thread.id, 'thread-members', memberId).get({
query: { with_member: withMember },
});
return this._add(data, cache);
}
async _fetchMany(cache) {
const raw = await this.client.api.channels(this.thread.id, 'thread-members').get();
async _fetchMany({ cache, limit, after, withMember } = {}) {
const raw = await this.client.api.channels(this.thread.id, 'thread-members').get({
query: { with_member: withMember, limit, after },
});
return raw.reduce((col, member) => col.set(member.user_id, this._add(member, cache)), new Collection());
}
/**
* Fetches member(s) for the thread from Discord, requires access to the `GUILD_MEMBERS` gateway intent.
* @param {UserResolvable|boolean} [member] The member to fetch. If `undefined`, all members
* in the thread are fetched, and will be cached based on `options.cache`. If boolean, this serves
* the purpose of `options.cache`.
* @param {BaseFetchOptions} [options] Additional options for this fetch
* Options used to fetch a thread member.
* @typedef {BaseFetchOptions} FetchThreadMemberOptions
* @property {boolean} [withMember] Whether to also return the guild member associated with this thread member
*/
/**
* Options used to fetch multiple thread members with guild member data.
* <info>With `withMember` set to `true`, pagination is enabled.</info>
* @typedef {Object} FetchThreadMembersWithGuildMemberDataOptions
* @property {true} withMember Whether to also return the guild member data
* @property {Snowflake} [after] Consider only thread members after this id
* @property {number} [limit] The maximum number of thread members to return
* @property {boolean} [cache] Whether to cache the fetched thread members and guild members
*/
/**
* Options used to fetch multiple thread members without guild member data.
* @typedef {Object} FetchThreadMembersWithoutGuildMemberDataOptions
* @property {false} [withMember] Whether to also return the guild member data
* @property {boolean} [cache] Whether to cache the fetched thread members
*/
/**
* Options used to fetch multiple thread members.
* @typedef {FetchThreadMembersWithGuildMemberDataOptions|
* FetchThreadMembersWithoutGuildMemberDataOptions} FetchThreadMembersOptions
*/
/**
* Fetches member(s) for the thread from Discord.
* @param {UserResolvable|FetchThreadMembersOptions|boolean} [member] The member to fetch. If `undefined`, all members
* in the thread are fetched, and will be cached based on `options.cache`.
* @param {FetchThreadMemberOptions|FetchThreadMembersOptions} [options] Additional options for this fetch
* @returns {Promise<ThreadMember|Collection<Snowflake, ThreadMember>>}
*/
fetch(member, { cache = true, force = false } = {}) {
fetch(member, options = { cache: true, force: false }) {
if (typeof member === 'boolean' && !deprecationEmittedForPassingBoolean) {
process.emitWarning(
'Passing boolean to member option is deprecated, use cache property instead.',
'DeprecationWarning',
);
deprecationEmittedForPassingBoolean = true;
}
const id = this.resolveId(member);
return id ? this._fetchOne(id, cache, force) : this._fetchMany(member ?? cache);
return id
? this._fetchOne(id, options)
: this._fetchMany(typeof member === 'boolean' ? { ...options, cache: member } : options);
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const CachedManager = require('./CachedManager');
const { Error } = require('../errors');
const { GuildMember } = require('../structures/GuildMember');
const { Message } = require('../structures/Message');
const ThreadMember = require('../structures/ThreadMember');

View File

@@ -15,7 +15,7 @@ class RateLimitError extends Error {
this.name = 'RateLimitError';
/**
* Time until this rate limit ends, in ms
* Time until this rate limit ends, in milliseconds
* @type {number}
*/
this.timeout = timeout;

View File

@@ -11,7 +11,7 @@ const {
} = require('../util/Constants');
function parseResponse(res) {
if (res.headers.get('content-type').startsWith('application/json')) return res.json();
if (res.headers.get('content-type')?.startsWith('application/json')) return res.json();
return res.buffer();
}
@@ -280,7 +280,7 @@ class RequestHandler {
/**
* @typedef {Object} InvalidRequestWarningData
* @property {number} count Number of invalid requests that have been made in the window
* @property {number} remainingTime Time in ms remaining before the count resets
* @property {number} remainingTime Time in milliseconds remaining before the count resets
*/
/**

View File

@@ -14,7 +14,7 @@ let Worker = null;
* A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains
* an instance of the bot and its {@link Client}. When its child process/worker exits for any reason, the shard will
* spawn a new one to replace it as necessary.
* @extends EventEmitter
* @extends {EventEmitter}
*/
class Shard extends EventEmitter {
constructor(manager, id) {
@@ -249,14 +249,18 @@ class Shard extends EventEmitter {
const listener = message => {
if (message?._fetchProp !== prop) return;
child.removeListener('message', listener);
this.decrementMaxListeners(child);
this._fetches.delete(prop);
if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error));
};
this.incrementMaxListeners(child);
child.on('message', listener);
this.send({ _fetchProp: prop }).catch(err => {
child.removeListener('message', listener);
this.decrementMaxListeners(child);
this._fetches.delete(prop);
reject(err);
});
@@ -288,14 +292,18 @@ class Shard extends EventEmitter {
const listener = message => {
if (message?._eval !== _eval) return;
child.removeListener('message', listener);
this.decrementMaxListeners(child);
this._evals.delete(_eval);
if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error));
};
this.incrementMaxListeners(child);
child.on('message', listener);
this.send({ _eval }).catch(err => {
child.removeListener('message', listener);
this.decrementMaxListeners(child);
this._evals.delete(_eval);
reject(err);
});
@@ -406,6 +414,30 @@ class Shard extends EventEmitter {
if (respawn) this.spawn(timeout).catch(err => this.emit('error', err));
}
/**
* Increments max listeners by one for a given emitter, if they are not zero.
* @param {EventEmitter|process} emitter The emitter that emits the events.
* @private
*/
incrementMaxListeners(emitter) {
const maxListeners = emitter.getMaxListeners();
if (maxListeners !== 0) {
emitter.setMaxListeners(maxListeners + 1);
}
}
/**
* Decrements max listeners by one for a given emitter, if they are not zero.
* @param {EventEmitter|process} emitter The emitter that emits the events.
* @private
*/
decrementMaxListeners(emitter) {
const maxListeners = emitter.getMaxListeners();
if (maxListeners !== 0) {
emitter.setMaxListeners(maxListeners - 1);
}
}
}
module.exports = Shard;

View File

@@ -111,13 +111,16 @@ class ShardClientUtil {
const listener = message => {
if (message?._sFetchProp !== prop || message._sFetchPropShard !== shard) return;
parent.removeListener('message', listener);
this.decrementMaxListeners(parent);
if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error));
};
this.incrementMaxListeners(parent);
parent.on('message', listener);
this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => {
parent.removeListener('message', listener);
this.decrementMaxListeners(parent);
reject(err);
});
});
@@ -146,13 +149,15 @@ class ShardClientUtil {
const listener = message => {
if (message?._sEval !== script || message._sEvalShard !== options.shard) return;
parent.removeListener('message', listener);
this.decrementMaxListeners(parent);
if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error));
};
this.incrementMaxListeners(parent);
parent.on('message', listener);
this.send({ _sEval: script, _sEvalShard: options.shard }).catch(err => {
parent.removeListener('message', listener);
this.decrementMaxListeners(parent);
reject(err);
});
});
@@ -241,6 +246,30 @@ class ShardClientUtil {
if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildId, shardCount);
return shard;
}
/**
* Increments max listeners by one for a given emitter, if they are not zero.
* @param {EventEmitter|process} emitter The emitter that emits the events.
* @private
*/
incrementMaxListeners(emitter) {
const maxListeners = emitter.getMaxListeners();
if (maxListeners !== 0) {
emitter.setMaxListeners(maxListeners + 1);
}
}
/**
* Decrements max listeners by one for a given emitter, if they are not zero.
* @param {EventEmitter|process} emitter The emitter that emits the events.
* @private
*/
decrementMaxListeners(emitter) {
const maxListeners = emitter.getMaxListeners();
if (maxListeners !== 0) {
emitter.setMaxListeners(maxListeners - 1);
}
}
}
module.exports = ShardClientUtil;

View File

@@ -36,7 +36,7 @@ class ShardingManager extends EventEmitter {
* @property {boolean} [respawn=true] Whether shards should automatically respawn upon exiting
* @property {string[]} [shardArgs=[]] Arguments to pass to the shard script when spawning
* (only available when mode is set to 'process')
* @property {string} [execArgv=[]] Arguments to pass to the shard script executable when spawning
* @property {string[]} [execArgv=[]] Arguments to pass to the shard script executable when spawning
* (only available when mode is set to 'process')
* @property {string} [token] Token to use for automatic shard count and passing to shards
*/

View File

@@ -64,6 +64,16 @@ class AnonymousGuild extends BaseGuild {
*/
this.nsfwLevel = NSFWLevels[data.nsfw_level];
}
if ('premium_subscription_count' in data) {
/**
* The total number of boosts for this server
* @type {?number}
*/
this.premiumSubscriptionCount = data.premium_subscription_count;
} else {
this.premiumSubscriptionCount ??= null;
}
}
/**

View File

@@ -3,6 +3,7 @@
const Base = require('./Base');
const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager');
const { ApplicationCommandOptionTypes, ApplicationCommandTypes, ChannelTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
@@ -62,6 +63,26 @@ class ApplicationCommand extends Base {
this.name = data.name;
}
if ('name_localizations' in data) {
/**
* The name localizations for this command
* @type {?Object<Locale, string>}
*/
this.nameLocalizations = data.name_localizations;
} else {
this.nameLocalizations ??= null;
}
if ('name_localized' in data) {
/**
* The localized name for this command
* @type {?string}
*/
this.nameLocalized = data.name_localized;
} else {
this.nameLocalized ??= null;
}
if ('description' in data) {
/**
* The description of this command
@@ -70,6 +91,26 @@ class ApplicationCommand extends Base {
this.description = data.description;
}
if ('description_localizations' in data) {
/**
* The description localizations for this command
* @type {?Object<Locale, string>}
*/
this.descriptionLocalizations = data.description_localizations;
} else {
this.descriptionLocalizations ??= null;
}
if ('description_localized' in data) {
/**
* The localized description for this command
* @type {?string}
*/
this.descriptionLocalized = data.description_localized;
} else {
this.descriptionLocalized ??= null;
}
if ('options' in data) {
/**
* The options of this command
@@ -80,13 +121,39 @@ class ApplicationCommand extends Base {
this.options ??= [];
}
/* eslint-disable max-len */
if ('default_permission' in data) {
/**
* Whether the command is enabled by default when the app is added to a guild
* @type {boolean}
* @deprecated Use {@link ApplicationCommand.defaultMemberPermissions} and {@link ApplicationCommand.dmPermission} instead.
*/
this.defaultPermission = data.default_permission;
}
/* eslint-disable max-len */
if ('default_member_permissions' in data) {
/**
* The default bitfield used to determine whether this command be used in a guild
* @type {?Readonly<Permissions>}
*/
this.defaultMemberPermissions = data.default_member_permissions
? new Permissions(BigInt(data.default_member_permissions)).freeze()
: null;
} else {
this.defaultMemberPermissions ??= null;
}
if ('dm_permission' in data) {
/**
* Whether the command can be used in DMs
* <info>This property is always `null` on guild commands</info>
* @type {?boolean}
*/
this.dmPermission = data.dm_permission;
} else {
this.dmPermission ??= null;
}
if ('version' in data) {
/**
@@ -128,10 +195,15 @@ class ApplicationCommand extends Base {
* Data for creating or editing an application command.
* @typedef {Object} ApplicationCommandData
* @property {string} name The name of the command
* @property {Object<Locale, string>} [nameLocalizations] The localizations for the command name
* @property {string} description The description of the command
* @property {Object<Locale, string>} [descriptionLocalizations] The localizations for the command description
* @property {ApplicationCommandType} [type] The type of the command
* @property {ApplicationCommandOptionData[]} [options] Options for the command
* @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild
* @property {?PermissionResolvable} [defaultMemberPermissions] The bitfield used to determine the default permissions
* a member needs in order to run the command
* @property {boolean} [dmPermission] Whether the command is enabled in DMs
*/
/**
@@ -143,20 +215,33 @@ class ApplicationCommand extends Base {
* @typedef {Object} ApplicationCommandOptionData
* @property {ApplicationCommandOptionType|number} type The type of the option
* @property {string} name The name of the option
* @property {Object<Locale, string>} [nameLocalizations] The name localizations for the option
* @property {string} description The description of the option
* @property {Object<Locale, string>} [descriptionLocalizations] The description localizations for the option
* @property {boolean} [autocomplete] Whether the option is an autocomplete option
* @property {boolean} [required] Whether the option is required
* @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from
* @property {ApplicationCommandOptionChoiceData[]} [choices] The choices of the option for the user to pick from
* @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group)
* @property {ChannelType[]|number[]} [channelTypes] When the option type is channel,
* the allowed types of channels that can be selected
* @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option
* @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
* @property {number} [minLength] The minimum length for a `STRING` option
* (maximum of `6000`)
* @property {number} [maxLength] The maximum length for a `STRING` option
* (maximum of `6000`)
*/
/**
* @typedef {Object} ApplicationCommandOptionChoiceData
* @property {string} name The name of the choice
* @property {Object<Locale, string>} [nameLocalizations] The localized names for this choice
* @property {string|number} value The value of the choice
*/
/**
* Edits this application command.
* @param {ApplicationCommandData} data The data to update the command with
* @param {Partial<ApplicationCommandData>} data The data to update the command with
* @returns {Promise<ApplicationCommand>}
* @example
* // Edit the description of this command
@@ -179,6 +264,23 @@ class ApplicationCommand extends Base {
return this.edit({ name });
}
/**
* Edits the localized names of this ApplicationCommand
* @param {Object<Locale, string>} nameLocalizations The new localized names for the command
* @returns {Promise<ApplicationCommand>}
* @example
* // Edit the name localizations of this command
* command.setLocalizedNames({
* 'en-GB': 'test',
* 'pt-BR': 'teste',
* })
* .then(console.log)
* .catch(console.error)
*/
setNameLocalizations(nameLocalizations) {
return this.edit({ nameLocalizations });
}
/**
* Edits the description of this ApplicationCommand
* @param {string} description The new description of the command
@@ -188,14 +290,52 @@ class ApplicationCommand extends Base {
return this.edit({ description });
}
/**
* Edits the localized descriptions of this ApplicationCommand
* @param {Object<Locale, string>} descriptionLocalizations The new localized descriptions for the command
* @returns {Promise<ApplicationCommand>}
* @example
* // Edit the description localizations of this command
* command.setLocalizedDescriptions({
* 'en-GB': 'A test command',
* 'pt-BR': 'Um comando de teste',
* })
* .then(console.log)
* .catch(console.error)
*/
setDescriptionLocalizations(descriptionLocalizations) {
return this.edit({ descriptionLocalizations });
}
/* eslint-disable max-len */
/**
* Edits the default permission of this ApplicationCommand
* @param {boolean} [defaultPermission=true] The default permission for this command
* @returns {Promise<ApplicationCommand>}
* @deprecated Use {@link ApplicationCommand#setDefaultMemberPermissions} and {@link ApplicationCommand#setDMPermission} instead.
*/
setDefaultPermission(defaultPermission = true) {
return this.edit({ defaultPermission });
}
/* eslint-enable max-len */
/**
* Edits the default member permissions of this ApplicationCommand
* @param {?PermissionResolvable} defaultMemberPermissions The default member permissions required to run this command
* @returns {Promise<ApplicationCommand>}
*/
setDefaultMemberPermissions(defaultMemberPermissions) {
return this.edit({ defaultMemberPermissions });
}
/**
* Edits the DM permission of this ApplicationCommand
* @param {boolean} [dmPermission=true] Whether the command can be used in DMs
* @returns {Promise<ApplicationCommand>}
*/
setDMPermission(dmPermission = true) {
return this.edit({ dmPermission });
}
/**
* Edits the options of this ApplicationCommand
@@ -232,6 +372,20 @@ class ApplicationCommand extends Base {
// If given an id, check if the id matches
if (command.id && this.id !== command.id) return false;
let defaultMemberPermissions = null;
let dmPermission = command.dmPermission ?? command.dm_permission;
if ('default_member_permissions' in command) {
defaultMemberPermissions = command.default_member_permissions
? new Permissions(BigInt(command.default_member_permissions)).bitfield
: null;
}
if ('defaultMemberPermissions' in command) {
defaultMemberPermissions =
command.defaultMemberPermissions !== null ? new Permissions(command.defaultMemberPermissions).bitfield : null;
}
// Check top level parameters
const commandType = typeof command.type === 'string' ? command.type : ApplicationCommandTypes[command.type];
if (
@@ -240,6 +394,8 @@ class ApplicationCommand extends Base {
('version' in command && command.version !== this.version) ||
('autocomplete' in command && command.autocomplete !== this.autocomplete) ||
(commandType && commandType !== this.type) ||
defaultMemberPermissions !== (this.defaultMemberPermissions?.bitfield ?? null) ||
(typeof dmPermission !== 'undefined' && dmPermission !== this.dmPermission) ||
// Future proof for options being nullable
// TODO: remove ?? 0 on each when nullable
(command.options?.length ?? 0) !== (this.options?.length ?? 0) ||
@@ -301,7 +457,9 @@ class ApplicationCommand extends Base {
option.options?.length !== existing.options?.length ||
(option.channelTypes ?? option.channel_types)?.length !== existing.channelTypes?.length ||
(option.minValue ?? option.min_value) !== existing.minValue ||
(option.maxValue ?? option.max_value) !== existing.maxValue
(option.maxValue ?? option.max_value) !== existing.maxValue ||
(option.minLength ?? option.min_length) !== existing.minLength ||
(option.maxLength ?? option.max_length) !== existing.maxLength
) {
return false;
}
@@ -344,7 +502,11 @@ class ApplicationCommand extends Base {
* @typedef {Object} ApplicationCommandOption
* @property {ApplicationCommandOptionType} type The type of the option
* @property {string} name The name of the option
* @property {Object<string, string>} [nameLocalizations] The localizations for the option name
* @property {string} [nameLocalized] The localized name for this option
* @property {string} description The description of the option
* @property {Object<string, string>} [descriptionLocalizations] The localizations for the option description
* @property {string} [descriptionLocalized] The localized description for this option
* @property {boolean} [required] Whether the option is required
* @property {boolean} [autocomplete] Whether the option is an autocomplete option
* @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from
@@ -353,18 +515,24 @@ class ApplicationCommand extends Base {
* the allowed types of channels that can be selected
* @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option
* @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
* @property {number} [minLength] The minimum length for a `STRING` option
* (maximum of `6000`)
* @property {number} [maxLength] The maximum length for a `STRING` option
* (maximum of `6000`)
*/
/**
* A choice for an application command option.
* @typedef {Object} ApplicationCommandOptionChoice
* @property {string} name The name of the choice
* @property {?string} nameLocalized The localized name of the choice in the provided locale, if any
* @property {?Object<string, string>} [nameLocalizations] The localized names for this choice
* @property {string|number} value The value of the choice
*/
/**
* Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API.
* @param {ApplicationCommandOptionData} option The option to transform
* @param {ApplicationCommandOptionData|ApplicationCommandOption} option The option to transform
* @param {boolean} [received] Whether this option has been received from Discord
* @returns {APIApplicationCommandOption}
* @private
@@ -374,14 +542,29 @@ class ApplicationCommand extends Base {
const channelTypesKey = received ? 'channelTypes' : 'channel_types';
const minValueKey = received ? 'minValue' : 'min_value';
const maxValueKey = received ? 'maxValue' : 'max_value';
const minLengthKey = received ? 'minLength' : 'min_length';
const maxLengthKey = received ? 'maxLength' : 'max_length';
const nameLocalizationsKey = received ? 'nameLocalizations' : 'name_localizations';
const nameLocalizedKey = received ? 'nameLocalized' : 'name_localized';
const descriptionLocalizationsKey = received ? 'descriptionLocalizations' : 'description_localizations';
const descriptionLocalizedKey = received ? 'descriptionLocalized' : 'description_localized';
return {
type: typeof option.type === 'number' && !received ? option.type : ApplicationCommandOptionTypes[option.type],
name: option.name,
[nameLocalizationsKey]: option.nameLocalizations ?? option.name_localizations,
[nameLocalizedKey]: option.nameLocalized ?? option.name_localized,
description: option.description,
[descriptionLocalizationsKey]: option.descriptionLocalizations ?? option.description_localizations,
[descriptionLocalizedKey]: option.descriptionLocalized ?? option.description_localized,
required:
option.required ?? (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP' ? undefined : false),
autocomplete: option.autocomplete,
choices: option.choices,
choices: option.choices?.map(choice => ({
name: choice.name,
[nameLocalizedKey]: choice.nameLocalized ?? choice.name_localized,
[nameLocalizationsKey]: choice.nameLocalizations ?? choice.name_localizations,
value: choice.value,
})),
options: option.options?.map(o => this.transformOption(o, received)),
[channelTypesKey]: received
? option.channel_types?.map(type => ChannelTypes[type])
@@ -390,6 +573,8 @@ class ApplicationCommand extends Base {
option.channel_types,
[minValueKey]: option.minValue ?? option.min_value,
[maxValueKey]: option.maxValue ?? option.max_value,
[minLengthKey]: option.minLength ?? option.min_length,
[maxLengthKey]: option.maxLength ?? option.max_length,
};
}
}

View File

@@ -0,0 +1,48 @@
'use strict';
const { ApplicationRoleConnectionMetadataTypes } = require('../util/Constants');
/**
* Role connection metadata object for an application.
*/
class ApplicationRoleConnectionMetadata {
constructor(data) {
/**
* The name of this metadata field
* @type {string}
*/
this.name = data.name;
/**
* The name localizations for this metadata field
* @type {?Object<Locale, string>}
*/
this.nameLocalizations = data.name_localizations ?? null;
/**
* The description of this metadata field
* @type {string}
*/
this.description = data.description;
/**
* The description localizations for this metadata field
* @type {?Object<Locale, string>}
*/
this.descriptionLocalizations = data.description_localizations ?? null;
/**
* The dictionary key for this metadata field
* @type {string}
*/
this.key = data.key;
/**
* The type of this metadata field
* @type {ApplicationRoleConnectionMetadataType}
*/
this.type = typeof data.type === 'number' ? ApplicationRoleConnectionMetadataTypes[data.type] : data.type;
}
}
exports.ApplicationRoleConnectionMetadata = ApplicationRoleConnectionMetadata;

View File

@@ -0,0 +1,89 @@
'use strict';
const { AutoModerationRuleTriggerTypes } = require('../util/Constants');
/**
* Represents the structure of an executed action when an {@link AutoModerationRule} is triggered.
*/
class AutoModerationActionExecution {
constructor(data, guild) {
/**
* The guild where this action was executed from.
* @type {Guild}
*/
this.guild = guild;
/**
* The action that was executed.
* @type {AutoModerationAction}
*/
this.action = data.action;
/**
* The id of the auto moderation rule this action belongs to.
* @type {Snowflake}
*/
this.ruleId = data.rule_id;
/**
* The trigger type of the auto moderation rule which was triggered.
* @type {AutoModerationRuleTriggerType}
*/
this.ruleTriggerType = AutoModerationRuleTriggerTypes[data.rule_trigger_type];
/**
* The id of the user that triggered this action.
* @type {Snowflake}
*/
this.userId = data.user_id;
/**
* The id of the channel where this action was triggered from.
* @type {?Snowflake}
*/
this.channelId = data.channel_id ?? null;
/**
* The id of the message that triggered this action.
* @type {?Snowflake}
* <info>This will not be present if the message was blocked or the content was not part of any message.</info>
*/
this.messageId = data.message_id ?? null;
/**
* The id of any system auto moderation messages posted as a result of this action.
* @type {?Snowflake}
*/
this.alertSystemMessageId = data.alert_system_message_id ?? null;
/**
* The content that triggered this action.
* <info>This property requires the {@link Intents.FLAGS.MESSAGE_CONTENT} privileged gateway intent.</info>
* @type {string}
*/
this.content = data.content;
/**
* The word or phrase configured in the rule that triggered this action.
* @type {?string}
*/
this.matchedKeyword = data.matched_keyword ?? null;
/**
* The substring in content that triggered this action.
* @type {?string}
*/
this.matchedContent = data.matched_content ?? null;
}
/**
* The auto moderation rule this action belongs to.
* @type {?AutoModerationRule}
* @readonly
*/
get autoModerationRule() {
return this.guild.autoModerationRules.cache.get(this.ruleId) ?? null;
}
}
module.exports = AutoModerationActionExecution;

View File

@@ -0,0 +1,294 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const Base = require('./Base');
const {
AutoModerationRuleKeywordPresetTypes,
AutoModerationRuleTriggerTypes,
AutoModerationRuleEventTypes,
AutoModerationActionTypes,
} = require('../util/Constants');
/**
* Represents an auto moderation rule.
* @extends {Base}
*/
class AutoModerationRule extends Base {
constructor(client, data, guild) {
super(client);
/**
* The id of this auto moderation rule.
* @type {Snowflake}
*/
this.id = data.id;
/**
* The guild this auto moderation rule is for.
* @type {Guild}
*/
this.guild = guild;
/**
* The user that created this auto moderation rule.
* @type {Snowflake}
*/
this.creatorId = data.creator_id;
/**
* The trigger type of this auto moderation rule.
* @type {AutoModerationRuleTriggerType}
*/
this.triggerType = AutoModerationRuleTriggerTypes[data.trigger_type];
this._patch(data);
}
_patch(data) {
if ('name' in data) {
/**
* The name of this auto moderation rule.
* @type {string}
*/
this.name = data.name;
}
if ('event_type' in data) {
/**
* The event type of this auto moderation rule.
* @type {AutoModerationRuleEventType}
*/
this.eventType = AutoModerationRuleEventTypes[data.event_type];
}
if ('trigger_metadata' in data) {
/**
* Additional data used to determine whether an auto moderation rule should be triggered.
* @typedef {Object} AutoModerationTriggerMetadata
* @property {string[]} keywordFilter The substrings that will be searched for in the content
* @property {string[]} regexPatterns The regular expression patterns which will be matched against the content
* <info>Only Rust-flavored regular expressions are supported.</info>
* @property {AutoModerationRuleKeywordPresetType[]} presets
* The internally pre-defined wordsets which will be searched for in the content
* @property {string[]} allowList The substrings that will be exempt from triggering
* {@link AutoModerationRuleTriggerType.KEYWORD} and {@link AutoModerationRuleTriggerType.KEYWORD_PRESET}
* @property {?number} mentionTotalLimit The total number of role & user mentions allowed per message
* @property {boolean} mentionRaidProtectionEnabled Whether mention raid protection is enabled
*/
/**
* The trigger metadata of the rule.
* @type {AutoModerationTriggerMetadata}
*/
this.triggerMetadata = {
keywordFilter: data.trigger_metadata.keyword_filter ?? [],
regexPatterns: data.trigger_metadata.regex_patterns ?? [],
presets: data.trigger_metadata.presets?.map(preset => AutoModerationRuleKeywordPresetTypes[preset]) ?? [],
allowList: data.trigger_metadata.allow_list ?? [],
mentionTotalLimit: data.trigger_metadata.mention_total_limit ?? null,
mentionRaidProtectionEnabled: data.trigger_metadata.mention_raid_protection_enabled ?? false,
};
}
if ('actions' in data) {
/**
* An object containing information about an auto moderation rule action.
* @typedef {Object} AutoModerationAction
* @property {AutoModerationActionType} type The type of this auto moderation rule action
* @property {AutoModerationActionMetadata} metadata Additional metadata needed during execution
*/
/**
* Additional data used when an auto moderation rule is executed.
* @typedef {Object} AutoModerationActionMetadata
* @property {?Snowflake} channelId The id of the channel to which content will be logged
* @property {?number} durationSeconds The timeout duration in seconds
* @property {?string} customMessage The custom message that is shown whenever a message is blocked
*/
/**
* The actions of this auto moderation rule.
* @type {AutoModerationAction[]}
*/
this.actions = data.actions.map(action => ({
type: AutoModerationActionTypes[action.type],
metadata: {
durationSeconds: action.metadata.duration_seconds ?? null,
channelId: action.metadata.channel_id ?? null,
customMessage: action.metadata.custom_message ?? null,
},
}));
}
if ('enabled' in data) {
/**
* Whether this auto moderation rule is enabled.
* @type {boolean}
*/
this.enabled = data.enabled;
}
if ('exempt_roles' in data) {
/**
* The roles exempt by this auto moderation rule.
* @type {Collection<Snowflake, Role>}
*/
this.exemptRoles = new Collection(
data.exempt_roles.map(exemptRole => [exemptRole, this.guild.roles.cache.get(exemptRole)]),
);
}
if ('exempt_channels' in data) {
/**
* The channels exempt by this auto moderation rule.
* @type {Collection<Snowflake, GuildChannel|ThreadChannel>}
*/
this.exemptChannels = new Collection(
data.exempt_channels.map(exemptChannel => [exemptChannel, this.guild.channels.cache.get(exemptChannel)]),
);
}
}
/**
* Edits this auto moderation rule.
* @param {AutoModerationRuleEditOptions} options Options for editing this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
edit(options) {
return this.guild.autoModerationRules.edit(this.id, options);
}
/**
* Deletes this auto moderation rule.
* @param {string} [reason] The reason for deleting this auto moderation rule
* @returns {Promise<void>}
*/
delete(reason) {
return this.guild.autoModerationRules.delete(this.id, reason);
}
/**
* Sets the name for this auto moderation rule.
* @param {string} name The name of this auto moderation rule
* @param {string} [reason] The reason for changing the name of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setName(name, reason) {
return this.edit({ name, reason });
}
/**
* Sets the event type for this auto moderation rule.
* @param {AutoModerationRuleEventType} eventType The event type of this auto moderation rule
* @param {string} [reason] The reason for changing the event type of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setEventType(eventType, reason) {
return this.edit({ eventType, reason });
}
/**
* Sets the keyword filter for this auto moderation rule.
* @param {string[]} keywordFilter The keyword filter of this auto moderation rule
* @param {string} [reason] The reason for changing the keyword filter of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setKeywordFilter(keywordFilter, reason) {
return this.edit({ triggerMetadata: { ...this.triggerMetadata, keywordFilter }, reason });
}
/**
* Sets the regular expression patterns for this auto moderation rule.
* @param {string[]} regexPatterns The regular expression patterns of this auto moderation rule
* <info>Only Rust-flavored regular expressions are supported.</info>
* @param {string} [reason] The reason for changing the regular expression patterns of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setRegexPatterns(regexPatterns, reason) {
return this.edit({ triggerMetadata: { ...this.triggerMetadata, regexPatterns }, reason });
}
/**
* Sets the presets for this auto moderation rule.
* @param {AutoModerationRuleKeywordPresetType[]} presets The presets of this auto moderation rule
* @param {string} [reason] The reason for changing the presets of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setPresets(presets, reason) {
return this.edit({ triggerMetadata: { ...this.triggerMetadata, presets }, reason });
}
/**
* Sets the allow list for this auto moderation rule.
* @param {string[]} allowList The allow list of this auto moderation rule
* @param {string} [reason] The reason for changing the allow list of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setAllowList(allowList, reason) {
return this.edit({ triggerMetadata: { ...this.triggerMetadata, allowList }, reason });
}
/**
* Sets the mention total limit for this auto moderation rule.
* @param {number} mentionTotalLimit The mention total limit of this auto moderation rule
* @param {string} [reason] The reason for changing the mention total limit of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setMentionTotalLimit(mentionTotalLimit, reason) {
return this.edit({ triggerMetadata: { ...this.triggerMetadata, mentionTotalLimit }, reason });
}
/**
* Sets whether to enable mention raid protection for this auto moderation rule.
* @param {boolean} mentionRaidProtectionEnabled
* Whether to enable mention raid protection for this auto moderation rule
* @param {string} [reason] The reason for changing the mention raid protection of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setMentionRaidProtectionEnabled(mentionRaidProtectionEnabled, reason) {
return this.edit({ triggerMetadata: { ...this.triggerMetadata, mentionRaidProtectionEnabled }, reason });
}
/**
* Sets the actions for this auto moderation rule.
* @param {AutoModerationActionOptions[]} actions The actions of this auto moderation rule
* @param {string} [reason] The reason for changing the actions of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setActions(actions, reason) {
return this.edit({ actions, reason });
}
/**
* Sets whether this auto moderation rule should be enabled.
* @param {boolean} [enabled=true] Whether to enable this auto moderation rule
* @param {string} [reason] The reason for enabling or disabling this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setEnabled(enabled = true, reason) {
return this.edit({ enabled, reason });
}
/**
* Sets the exempt roles for this auto moderation rule.
* @param {Collection<Snowflake, Role>|RoleResolvable[]} [exemptRoles] The exempt roles of this auto moderation rule
* @param {string} [reason] The reason for changing the exempt roles of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setExemptRoles(exemptRoles, reason) {
return this.edit({ exemptRoles, reason });
}
/**
* Sets the exempt channels for this auto moderation rule.
* @param {Collection<Snowflake, GuildChannel|ThreadChannel>|GuildChannelResolvable[]} [exemptChannels]
* The exempt channels of this auto moderation rule
* @param {string} [reason] The reason for changing the exempt channels of this auto moderation rule
* @returns {Promise<AutoModerationRule>}
*/
setExemptChannels(exemptChannels, reason) {
return this.edit({ exemptChannels, reason });
}
}
module.exports = AutoModerationRule;

View File

@@ -2,6 +2,7 @@
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
const Interaction = require('./Interaction');
const { Error } = require('../errors');
const { InteractionResponseTypes, ApplicationCommandOptionTypes } = require('../util/Constants');
/**
@@ -76,7 +77,7 @@ class AutocompleteInteraction extends Interaction {
/**
* Sends results for the autocomplete of this interaction.
* @param {ApplicationCommandOptionChoice[]} options The options for the autocomplete
* @param {ApplicationCommandOptionChoiceData[]} options The options for the autocomplete
* @returns {Promise<void>}
* @example
* // respond to autocomplete interaction
@@ -95,9 +96,7 @@ class AutocompleteInteraction extends Interaction {
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,
data: {
choices: options,
},
data: { choices: options.map(choice => ({ ...choice, name_localizations: options.nameLocalizations })) },
},
auth: false,
});

View File

@@ -3,6 +3,7 @@
const { Collection } = require('@discordjs/collection');
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const MessageAttachment = require('./MessageAttachment');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { ApplicationCommandOptionTypes } = require('../util/Constants');
@@ -76,6 +77,7 @@ class BaseCommandInteraction extends Interaction {
* @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles
* @property {Collection<Snowflake, Channel|APIChannel>} [channels] The resolved channels
* @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages
* @property {Collection<Snowflake, MessageAttachment>} [attachments] The resolved attachments
*/
/**
@@ -84,7 +86,7 @@ class BaseCommandInteraction extends Interaction {
* @returns {CommandInteractionResolvedData}
* @private
*/
transformResolved({ members, users, channels, roles, messages }) {
transformResolved({ members, users, channels, roles, messages, attachments }) {
const result = {};
if (members) {
@@ -123,6 +125,14 @@ class BaseCommandInteraction extends Interaction {
}
}
if (attachments) {
result.attachments = new Collection();
for (const attachment of Object.values(attachments)) {
const patched = new MessageAttachment(attachment.url, attachment.filename, attachment);
result.attachments.set(attachment.id, patched);
}
}
return result;
}
@@ -139,6 +149,7 @@ class BaseCommandInteraction extends Interaction {
* @property {GuildMember|APIGuildMember} [member] The resolved member
* @property {GuildChannel|ThreadChannel|APIChannel} [channel] The resolved channel
* @property {Role|APIRole} [role] The resolved role
* @property {MessageAttachment} [attachment] The resolved attachment
*/
/**
@@ -169,6 +180,9 @@ class BaseCommandInteraction extends Interaction {
const role = resolved.roles?.[option.value];
if (role) result.role = this.guild?.roles._add(role) ?? role;
const attachment = resolved.attachments?.[option.value];
if (attachment) result.attachment = new MessageAttachment(attachment.url, attachment.filename, attachment);
}
return result;
@@ -182,6 +196,8 @@ class BaseCommandInteraction extends Interaction {
editReply() {}
deleteReply() {}
followUp() {}
showModal() {}
awaitModalSubmit() {}
}
InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']);

View File

@@ -1,12 +1,9 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const GuildChannel = require('./GuildChannel');
const Webhook = require('./Webhook');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const GuildTextThreadManager = require('../managers/GuildTextThreadManager');
const MessageManager = require('../managers/MessageManager');
const ThreadManager = require('../managers/ThreadManager');
const DataResolver = require('../util/DataResolver');
/**
* Represents a text-based guild channel on Discord.
@@ -25,9 +22,9 @@ class BaseGuildTextChannel extends GuildChannel {
/**
* A manager of the threads belonging to this channel
* @type {ThreadManager}
* @type {GuildTextThreadManager}
*/
this.threads = new ThreadManager(this);
this.threads = new GuildTextThreadManager(this);
/**
* If the guild considers this channel NSFW
@@ -72,11 +69,21 @@ class BaseGuildTextChannel extends GuildChannel {
if ('default_auto_archive_duration' in data) {
/**
* The default auto archive duration for newly created threads in this channel
* @type {?ThreadAutoArchiveDuration}
* @type {?number}
*/
this.defaultAutoArchiveDuration = data.default_auto_archive_duration;
}
if ('default_thread_rate_limit_per_user' in data) {
/**
* The initial rate limit per user (slowmode) to set on newly created threads in a channel.
* @type {?number}
*/
this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user;
} else {
this.defaultThreadRateLimitPerUser ??= null;
}
if ('messages' in data) {
for (const message of data.messages) this.messages._add(message);
}
@@ -92,16 +99,6 @@ class BaseGuildTextChannel extends GuildChannel {
return this.edit({ defaultAutoArchiveDuration }, reason);
}
/**
* Sets whether this channel is flagged as NSFW.
* @param {boolean} [nsfw=true] Whether the channel should be considered NSFW
* @param {string} [reason] Reason for changing the channel's NSFW flag
* @returns {Promise<TextChannel>}
*/
setNSFW(nsfw = true, reason) {
return this.edit({ nsfw }, reason);
}
/**
* Sets the type of this channel (only conversion between text and news is supported)
* @param {string} type The new channel type
@@ -112,57 +109,6 @@ class BaseGuildTextChannel extends GuildChannel {
return this.edit({ type }, reason);
}
/**
* Fetches all webhooks for the channel.
* @returns {Promise<Collection<Snowflake, Webhook>>}
* @example
* // Fetch webhooks
* channel.fetchWebhooks()
* .then(hooks => console.log(`This channel has ${hooks.size} hooks`))
* .catch(console.error);
*/
async fetchWebhooks() {
const data = await this.client.api.channels[this.id].webhooks.get();
const hooks = new Collection();
for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
return hooks;
}
/**
* Options used to create a {@link Webhook} in a {@link TextChannel} or a {@link NewsChannel}.
* @typedef {Object} ChannelWebhookCreateOptions
* @property {?(BufferResolvable|Base64Resolvable)} [avatar] Avatar for the webhook
* @property {string} [reason] Reason for creating the webhook
*/
/**
* Creates a webhook for the channel.
* @param {string} name The name of the webhook
* @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook
* @returns {Promise<Webhook>} Returns the created Webhook
* @example
* // Create a webhook for the current channel
* channel.createWebhook('Snek', {
* avatar: 'https://i.imgur.com/mI8XcpG.jpg',
* reason: 'Needed a cool new Webhook'
* })
* .then(console.log)
* .catch(console.error)
*/
async createWebhook(name, { avatar, reason } = {}) {
if (typeof avatar === 'string' && !avatar.startsWith('data:')) {
avatar = await DataResolver.resolveImage(avatar);
}
const data = await this.client.api.channels[this.id].webhooks.post({
data: {
name,
avatar,
},
reason,
});
return new Webhook(this.client, data);
}
/**
* Sets a new topic for the guild channel.
* @param {?string} topic The new topic for the guild channel
@@ -178,6 +124,14 @@ class BaseGuildTextChannel extends GuildChannel {
return this.edit({ topic }, reason);
}
/**
* Data that can be resolved to an Application. This can be:
* * An Application
* * An Activity with associated Application
* * A Snowflake
* @typedef {Application|Snowflake} ApplicationResolvable
*/
/**
* Options used to create an invite to a guild channel.
* @typedef {Object} CreateInviteOptions
@@ -229,6 +183,10 @@ class BaseGuildTextChannel extends GuildChannel {
createMessageComponentCollector() {}
awaitMessageComponent() {}
bulkDelete() {}
fetchWebhooks() {}
createWebhook() {}
setRateLimitPerUser() {}
setNSFW() {}
}
TextBasedChannel.applyToClass(BaseGuildTextChannel, true);

View File

@@ -2,24 +2,37 @@
const { Collection } = require('@discordjs/collection');
const GuildChannel = require('./GuildChannel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const MessageManager = require('../managers/MessageManager');
const { VideoQualityModes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
/**
* Represents a voice-based guild channel on Discord.
* @extends {GuildChannel}
* @implements {TextBasedChannel}
*/
class BaseGuildVoiceChannel extends GuildChannel {
constructor(guild, data, client) {
super(guild, data, client, false);
/**
* A manager of the messages sent to this channel
* @type {MessageManager}
*/
this.messages = new MessageManager(this);
/**
* If the guild considers this channel NSFW
* @type {boolean}
*/
this.nsfw = Boolean(data.nsfw);
this._patch(data);
}
_patch(data) {
super._patch(data);
if ('rtc_region' in data) {
/**
* The RTC region for this voice-based channel. This region is automatically selected if `null`.
* @type {?string}
*/
this.rtcRegion = data.rtc_region;
}
if ('bitrate' in data) {
/**
* The bitrate of this voice-based channel
@@ -28,6 +41,14 @@ class BaseGuildVoiceChannel extends GuildChannel {
this.bitrate = data.bitrate;
}
if ('rtc_region' in data) {
/**
* The RTC region for this voice-based channel. This region is automatically selected if `null`.
* @type {?string}
*/
this.rtcRegion = data.rtc_region;
}
if ('user_limit' in data) {
/**
* The maximum amount of users allowed in this channel.
@@ -35,6 +56,40 @@ class BaseGuildVoiceChannel extends GuildChannel {
*/
this.userLimit = data.user_limit;
}
if ('video_quality_mode' in data) {
/**
* The camera video quality mode of the channel.
* @type {?VideoQualityMode}
*/
this.videoQualityMode = VideoQualityModes[data.video_quality_mode];
} else {
this.videoQualityMode ??= null;
}
if ('last_message_id' in data) {
/**
* The last message id sent in the channel, if one was sent
* @type {?Snowflake}
*/
this.lastMessageId = data.last_message_id;
}
if ('messages' in data) {
for (const message of data.messages) this.messages._add(message);
}
if ('rate_limit_per_user' in data) {
/**
* The rate limit per user (slowmode) for this channel in seconds
* @type {number}
*/
this.rateLimitPerUser = data.rate_limit_per_user;
}
if ('nsfw' in data) {
this.nsfw = data.nsfw;
}
}
/**
@@ -75,26 +130,11 @@ class BaseGuildVoiceChannel extends GuildChannel {
if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true;
return (
this.guild.me.communicationDisabledUntilTimestamp < Date.now() &&
this.guild.members.me.communicationDisabledUntilTimestamp < Date.now() &&
permissions.has(Permissions.FLAGS.CONNECT, false)
);
}
/**
* Sets the RTC region of the channel.
* @param {?string} region The new region of the channel. Set to `null` to remove a specific region for the channel
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Set the RTC region to europe
* channel.setRTCRegion('europe');
* @example
* // Remove a fixed region for this channel - let Discord decide automatically
* channel.setRTCRegion(null);
*/
setRTCRegion(region) {
return this.edit({ rtcRegion: region });
}
/**
* Creates an invite to this guild channel.
* @param {CreateInviteOptions} [options={}] The options for creating the invite
@@ -118,6 +158,79 @@ class BaseGuildVoiceChannel extends GuildChannel {
fetchInvites(cache = true) {
return this.guild.invites.fetch({ channelId: this.id, cache });
}
/**
* Sets the bitrate of the channel.
* @param {number} bitrate The new bitrate
* @param {string} [reason] Reason for changing the channel's bitrate
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Set the bitrate of a voice channel
* channel.setBitrate(48_000)
* .then(channel => console.log(`Set bitrate to ${channel.bitrate}bps for ${channel.name}`))
* .catch(console.error);
*/
setBitrate(bitrate, reason) {
return this.edit({ bitrate }, reason);
}
/**
* Sets the RTC region of the channel.
* @param {?string} rtcRegion The new region of the channel. Set to `null` to remove a specific region for the channel
* @param {string} [reason] The reason for modifying this region.
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Set the RTC region to sydney
* channel.setRTCRegion('sydney');
* @example
* // Remove a fixed region for this channel - let Discord decide automatically
* channel.setRTCRegion(null, 'We want to let Discord decide.');
*/
setRTCRegion(rtcRegion, reason) {
return this.edit({ rtcRegion }, reason);
}
/**
* Sets the user limit of the channel.
* @param {number} userLimit The new user limit
* @param {string} [reason] Reason for changing the user limit
* @returns {Promise<BaseGuildVoiceChannel>}
* @example
* // Set the user limit of a voice channel
* channel.setUserLimit(42)
* .then(channel => console.log(`Set user limit to ${channel.userLimit} for ${channel.name}`))
* .catch(console.error);
*/
setUserLimit(userLimit, reason) {
return this.edit({ userLimit }, reason);
}
/**
* Sets the camera video quality mode of the channel.
* @param {VideoQualityMode|number} videoQualityMode The new camera video quality mode.
* @param {string} [reason] Reason for changing the camera video quality mode.
* @returns {Promise<BaseGuildVoiceChannel>}
*/
setVideoQualityMode(videoQualityMode, reason) {
return this.edit({ videoQualityMode }, reason);
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
get lastMessage() {}
send() {}
sendTyping() {}
createMessageCollector() {}
awaitMessages() {}
createMessageComponentCollector() {}
awaitMessageComponent() {}
bulkDelete() {}
fetchWebhooks() {}
createWebhook() {}
setRateLimitPerUser() {}
setNSFW() {}
}
TextBasedChannel.applyToClass(BaseGuildVoiceChannel, true, ['lastPinAt']);
module.exports = BaseGuildVoiceChannel;

View File

@@ -4,7 +4,7 @@ const { TypeError } = require('../errors');
const { MessageComponentTypes, Events } = require('../util/Constants');
/**
* Represents an interactive component of a Message. It should not be necessary to construct this directly.
* Represents an interactive component of a Message or Modal. It should not be necessary to construct this directly.
* See {@link MessageComponent}
*/
class BaseMessageComponent {
@@ -15,18 +15,20 @@ class BaseMessageComponent {
*/
/**
* Data that can be resolved into options for a MessageComponent. This can be:
* Data that can be resolved into options for a component. This can be:
* * MessageActionRowOptions
* * MessageButtonOptions
* * MessageSelectMenuOptions
* * TextInputComponentOptions
* @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
*/
/**
* Components that can be sent in a message. These can be:
* Components that can be sent in a payload. These can be:
* * MessageActionRow
* * MessageButton
* * MessageSelectMenu
* * TextInputComponent
* @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
*/
@@ -51,10 +53,10 @@ class BaseMessageComponent {
}
/**
* Constructs a MessageComponent based on the type of the incoming data
* Constructs a component based on the type of the incoming data
* @param {MessageComponentOptions} data Data for a MessageComponent
* @param {Client|WebhookClient} [client] Client constructing this component
* @returns {?MessageComponent}
* @returns {?(MessageComponent|ModalComponent)}
* @private
*/
static create(data, client) {
@@ -79,6 +81,11 @@ class BaseMessageComponent {
component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data);
break;
}
case MessageComponentTypes.TEXT_INPUT: {
const TextInputComponent = require('./TextInputComponent');
component = data instanceof TextInputComponent ? data : new TextInputComponent(data);
break;
}
default:
if (client) {
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
@@ -90,7 +97,7 @@ class BaseMessageComponent {
}
/**
* Resolves the type of a MessageComponent
* Resolves the type of a component
* @param {MessageComponentTypeResolvable} type The type to resolve
* @returns {MessageComponentType}
* @private

View File

@@ -7,6 +7,19 @@ const GuildChannel = require('./GuildChannel');
* @extends {GuildChannel}
*/
class CategoryChannel extends GuildChannel {
/**
* The id of the parent of this channel.
* @name CategoryChannel#parentId
* @type {null}
*/
/**
* The parent of this channel.
* @name CategoryChannel#parent
* @type {null}
* @readonly
*/
/**
* Channels that are a part of this category
* @type {Collection<Snowflake, GuildChannel>}
@@ -18,7 +31,7 @@ class CategoryChannel extends GuildChannel {
/**
* Sets the category parent of this channel.
* <warn>It is not currently possible to set the parent of a CategoryChannel.</warn>
* <warn>It is not possible to set the parent of a CategoryChannel.</warn>
* @method setParent
* @memberof CategoryChannel
* @instance
@@ -30,16 +43,27 @@ class CategoryChannel extends GuildChannel {
/**
* Options for creating a channel using {@link CategoryChannel#createChannel}.
* @typedef {Object} CategoryCreateChannelOptions
* @property {string} [name] The name of the new channel
* @property {ChannelType|number} [type='GUILD_TEXT'] The type of the new channel.
* @property {number} [position] Position of the new channel
* @property {string} [topic] The topic for the new channel
* @property {boolean} [nsfw] Whether the new channel is NSFW
* @property {number} [bitrate] Bitrate of the new channel in bits (only voice)
* @property {number} [userLimit] Maximum amount of users allowed in the new channel (only voice)
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites of the new channel
* @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 {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The specific region of the new channel
* @property {?VideoQualityMode|number} [videoQualityMode] The camera video quality mode of the new channel
* @property {ChannelFlagsResolvable} [flags] The flags to set on the new channel
* @property {GuildForumTagData[]} [availableTags] The tags to set as available in a forum channel
* @property {?DefaultReactionEmoji} [defaultReactionEmoji] The emoji to set as the default reaction emoji
* @property {number} [defaultThreadRateLimitPerUser] The rate limit per user (slowmode) to set on forum posts
* @property {?SortOrderType} [defaultSortOrder] The default sort order mode to set on the new channel
* @property {number} [defaultThreadRateLimitPerUser] The initial rate limit per user (slowmode)
* to set on newly created threads in a channel.
* @property {string} [reason] Reason for creating the new channel
*/

View File

@@ -10,6 +10,9 @@ let StoreChannel;
let TextChannel;
let ThreadChannel;
let VoiceChannel;
let DirectoryChannel;
let ForumChannel;
const ChannelFlags = require('../util/ChannelFlags');
const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
@@ -46,6 +49,17 @@ class Channel extends Base {
* @type {Snowflake}
*/
this.id = data.id;
if ('flags' in data) {
/**
* The flags that are applied to the channel.
* <info>This is only `null` in a {@link PartialGroupDMChannel}. In all other cases, it is not `null`.</info>
* @type {?Readonly<ChannelFlags>}
*/
this.flags = new ChannelFlags(data.flags).freeze();
} else {
this.flags ??= new ChannelFlags().freeze();
}
}
/**
@@ -164,7 +178,15 @@ class Channel extends Base {
return ThreadChannelTypes.includes(this.type);
}
static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) {
/**
* Indicates whether this channel is a {@link DirectoryChannel}
* @returns {boolean}
*/
isDirectory() {
return this.type === 'GUILD_DIRECTORY';
}
static create(client, data, guild, { allowUnknownGuild } = {}) {
CategoryChannel ??= require('./CategoryChannel');
DMChannel ??= require('./DMChannel');
NewsChannel ??= require('./NewsChannel');
@@ -173,6 +195,8 @@ class Channel extends Base {
TextChannel ??= require('./TextChannel');
ThreadChannel ??= require('./ThreadChannel');
VoiceChannel ??= require('./VoiceChannel');
DirectoryChannel ??= require('./DirectoryChannel');
ForumChannel ??= require('./ForumChannel');
let channel;
if (!data.guild_id && !guild) {
@@ -214,10 +238,18 @@ class Channel extends Base {
case ChannelTypes.GUILD_NEWS_THREAD:
case ChannelTypes.GUILD_PUBLIC_THREAD:
case ChannelTypes.GUILD_PRIVATE_THREAD: {
channel = new ThreadChannel(guild, data, client, fromInteraction);
channel = new ThreadChannel(guild, data, client);
if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel);
break;
}
case ChannelTypes.GUILD_DIRECTORY:
channel = new DirectoryChannel(client, data);
break;
case ChannelTypes.GUILD_FORUM:
channel = new ForumChannel(guild, data, client);
break;
}
if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel);
}

View File

@@ -1,12 +1,21 @@
'use strict';
const { ApplicationRoleConnectionMetadata } = require('./ApplicationRoleConnectionMetadata');
const Team = require('./Team');
const Application = require('./interfaces/Application');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
const ApplicationFlags = require('../util/ApplicationFlags');
const { ApplicationRoleConnectionMetadataTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
/**
* Represents a Client OAuth2 Application.
* @typedef {Object} ClientApplicationInstallParams
* @property {InviteScope[]} scopes The scopes to add the application to the server with
* @property {Readonly<Permissions>} permissions The permissions this bot will request upon joining
*/
/**
* Represents a client application.
* @extends {Application}
*/
class ClientApplication extends Application {
@@ -23,6 +32,35 @@ class ClientApplication extends Application {
_patch(data) {
super._patch(data);
/**
* The tags this application has (max of 5)
* @type {string[]}
*/
this.tags = data.tags ?? [];
if ('install_params' in data) {
/**
* Settings for this application's default in-app authorization
* @type {?ClientApplicationInstallParams}
*/
this.installParams = {
scopes: data.install_params.scopes,
permissions: new Permissions(data.install_params.permissions).freeze(),
};
} else {
this.installParams ??= null;
}
if ('custom_install_url' in data) {
/**
* This application's custom installation URL
* @type {?string}
*/
this.customInstallURL = data.custom_install_url;
} else {
this.customInstallURL = null;
}
if ('flags' in data) {
/**
* The flags this application has
@@ -31,6 +69,26 @@ class ClientApplication extends Application {
this.flags = new ApplicationFlags(data.flags).freeze();
}
if ('approximate_guild_count' in data) {
/**
* An approximate amount of guilds this application is in.
* @type {?number}
*/
this.approximateGuildCount = data.approximate_guild_count;
} else {
this.approximateGuildCount ??= null;
}
if ('guild_id' in data) {
/**
* The id of the guild associated with this application.
* @type {?Snowflake}
*/
this.guildId = data.guild_id;
} else {
this.guildId ??= null;
}
if ('cover_image' in data) {
/**
* The hash of the application's cover image
@@ -82,6 +140,15 @@ class ClientApplication extends Application {
: this.owner ?? null;
}
/**
* The guild associated with this application.
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.cache.get(this.guildId) ?? null;
}
/**
* Whether this application is partial
* @type {boolean}
@@ -96,10 +163,52 @@ class ClientApplication extends Application {
* @returns {Promise<ClientApplication>}
*/
async fetch() {
const app = await this.client.api.oauth2.applications('@me').get();
this._patch(app);
const data = await this.client.api.applications('@me').get();
this._patch(data);
return this;
}
/**
* Gets this application's role connection metadata records
* @returns {Promise<ApplicationRoleConnectionMetadata[]>}
*/
async fetchRoleConnectionMetadataRecords() {
const metadata = await this.client.api.applications(this.client.user.id)('role-connections').metadata.get();
return metadata.map(data => new ApplicationRoleConnectionMetadata(data));
}
/**
* Data for creating or editing an application role connection metadata.
* @typedef {Object} ApplicationRoleConnectionMetadataEditOptions
* @property {string} name The name of the metadata field
* @property {?Object<Locale, string>} [nameLocalizations] The name localizations for the metadata field
* @property {string} description The description of the metadata field
* @property {?Object<Locale, string>} [descriptionLocalizations] The description localizations for the metadata field
* @property {string} key The dictionary key of the metadata field
* @property {ApplicationRoleConnectionMetadataType} type The type of the metadata field
*/
/**
* Updates this application's role connection metadata records
* @param {ApplicationRoleConnectionMetadataEditOptions[]} records The new role connection metadata records
* @returns {Promise<ApplicationRoleConnectionMetadata[]>}
*/
async editRoleConnectionMetadataRecords(records) {
const newRecords = await this.client.api
.applications(this.client.user.id)('role-connections')
.metadata.put({
data: records.map(record => ({
type: typeof record.type === 'string' ? ApplicationRoleConnectionMetadataTypes[record.type] : record.type,
key: record.key,
name: record.name,
name_localizations: record.nameLocalizations,
description: record.description,
description_localizations: record.descriptionLocalizations,
})),
});
return newRecords.map(data => new ApplicationRoleConnectionMetadata(data));
}
}
module.exports = ClientApplication;

View File

@@ -4,6 +4,8 @@ const { Presence } = require('./Presence');
const { TypeError } = require('../errors');
const { ActivityTypes, Opcodes } = require('../util/Constants');
const CustomStatusActivityTypes = [ActivityTypes.CUSTOM, ActivityTypes[ActivityTypes.CUSTOM]];
/**
* Represents the client's presence.
* @extends {Presence}
@@ -49,11 +51,18 @@ class ClientPresence extends Presence {
if (activities?.length) {
for (const [i, activity] of activities.entries()) {
if (typeof activity.name !== 'string') throw new TypeError('INVALID_TYPE', `activities[${i}].name`, 'string');
activity.type ??= 0;
activity.type ??= ActivityTypes.PLAYING;
if (CustomStatusActivityTypes.includes(activity.type) && !activity.state) {
activity.state = activity.name;
activity.name = 'Custom Status';
}
data.activities.push({
type: typeof activity.type === 'number' ? activity.type : ActivityTypes[activity.type],
name: activity.name,
state: activity.state,
url: activity.url,
});
}
@@ -62,6 +71,7 @@ class ClientPresence extends Presence {
...this.activities.map(a => ({
name: a.name,
type: ActivityTypes[a.type],
state: a.state ?? undefined,
url: a.url ?? undefined,
})),
);

View File

@@ -53,11 +53,13 @@ class ClientUser extends User {
* @param {ClientUserEditData} data The new data
* @returns {Promise<ClientUser>}
*/
async edit(data) {
if (typeof data.avatar !== 'undefined') data.avatar = await DataResolver.resolveImage(data.avatar);
const newData = await this.client.api.users('@me').patch({ data });
this.client.token = newData.token;
const { updated } = this.client.actions.UserUpdate.handle(newData);
async edit({ username, avatar }) {
const data = await this.client.api
.users('@me')
.patch({ data: { username, avatar: avatar && (await DataResolver.resolveImage(avatar)) } });
this.client.token = data.token;
const { updated } = this.client.actions.UserUpdate.handle(data);
return updated ?? this;
}
@@ -94,7 +96,8 @@ class ClientUser extends User {
/**
* Options for setting activities
* @typedef {Object} ActivitiesOptions
* @property {string} [name] Name of the activity
* @property {string} name Name of the activity
* @property {string} [state] State of the activity
* @property {ActivityType|number} [type] Type of the activity
* @property {string} [url] Twitch / YouTube stream URL
*/
@@ -145,7 +148,7 @@ class ClientUser extends User {
/**
* Options for setting an activity.
* @typedef {Object} ActivityOptions
* @property {string} [name] Name of the activity
* @property {string} name Name of the activity
* @property {string} [url] Twitch / YouTube stream URL
* @property {ActivityType|number} [type] Type of the activity
* @property {number|number[]} [shardId] Shard Id(s) to have the activity set on
@@ -153,7 +156,7 @@ class ClientUser extends User {
/**
* Sets the activity the client user is playing.
* @param {string|ActivityOptions} [name] Activity being played, or options for setting the activity
* @param {string|ActivityOptions} name Activity being played, or options for setting the activity
* @param {ActivityOptions} [options] Options for setting the activity
* @returns {ClientPresence}
* @example

View File

@@ -240,10 +240,19 @@ class CommandInteractionOptionResolver {
return option?.message ?? null;
}
/**
* The full autocomplete option object.
* @typedef {Object} AutocompleteFocusedOption
* @property {string} name The name of the option
* @property {ApplicationCommandOptionType} type The type of the application command option
* @property {string} value The value of the option
* @property {boolean} focused Whether this option is currently in focus for autocomplete
*/
/**
* Gets the focused option.
* @param {boolean} [getFull=false] Whether to get the full option object
* @returns {string|number|ApplicationCommandOptionChoice}
* @returns {string|AutocompleteFocusedOption}
* The value of the option, or the whole option if getFull is true
*/
getFocused(getFull = false) {
@@ -251,6 +260,17 @@ class CommandInteractionOptionResolver {
if (!focusedOption) throw new TypeError('AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION');
return getFull ? focusedOption : focusedOption.value;
}
/**
* Gets an attachment option.
* @param {string} name The name of the option.
* @param {boolean} [required=false] Whether to throw an error if the option is not found.
* @returns {?MessageAttachment} The value of the option, or null if not set and not required.
*/
getAttachment(name, required = false) {
const option = this._getTypedOption(name, 'ATTACHMENT', ['attachment'], required);
return option?.attachment ?? null;
}
}
module.exports = CommandInteractionOptionResolver;

View File

@@ -22,7 +22,7 @@ class ContextMenuInteraction extends BaseCommandInteraction {
);
/**
* The id of the target of the interaction
* The id of the target of this interaction
* @type {Snowflake}
*/
this.targetId = data.data.target_id;

View File

@@ -94,8 +94,16 @@ class DMChannel extends Channel {
createMessageComponentCollector() {}
awaitMessageComponent() {}
// Doesn't work on DM channels; bulkDelete() {}
// Doesn't work on DM channels; setRateLimitPerUser() {}
// Doesn't work on DM channels; setNSFW() {}
}
TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']);
TextBasedChannel.applyToClass(DMChannel, true, [
'bulkDelete',
'fetchWebhooks',
'createWebhook',
'setRateLimitPerUser',
'setNSFW',
]);
module.exports = DMChannel;

View File

@@ -0,0 +1,20 @@
'use strict';
const { Channel } = require('./Channel');
/**
* Represents a channel that displays a directory of guilds.
* @extends {Channel}
*/
class DirectoryChannel extends Channel {
_patch(data) {
super._patch(data);
/**
* The channel's name
* @type {string}
*/
this.name = data.name;
}
}
module.exports = DirectoryChannel;

View File

@@ -0,0 +1,264 @@
'use strict';
const GuildChannel = require('./GuildChannel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const GuildForumThreadManager = require('../managers/GuildForumThreadManager');
const { SortOrderTypes, ForumLayoutTypes } = require('../util/Constants');
const { transformAPIGuildForumTag, transformAPIGuildDefaultReaction } = require('../util/Util');
/**
* @typedef {Object} GuildForumTagEmoji
* @property {?Snowflake} id The id of a guild's custom emoji
* @property {?string} name The unicode character of the emoji
*/
/**
* @typedef {Object} GuildForumTag
* @property {Snowflake} id The id of the tag
* @property {string} name The name of the tag
* @property {boolean} moderated Whether this tag can only be added to or removed from threads
* by a member with the `ManageThreads` permission
* @property {?GuildForumTagEmoji} emoji The emoji of this tag
*/
/**
* @typedef {Object} GuildForumTagData
* @property {Snowflake} [id] The id of the tag
* @property {string} name The name of the tag
* @property {boolean} [moderated] Whether this tag can only be added to or removed from threads
* by a member with the `ManageThreads` permission
* @property {?GuildForumTagEmoji} [emoji] The emoji of this tag
*/
/**
* @typedef {Object} DefaultReactionEmoji
* @property {?Snowflake} id The id of a guild's custom emoji
* @property {?string} name The unicode character of the emoji
*/
/**
* Represents a channel that only contains threads
* @extends {GuildChannel}
* @implements {TextBasedChannel}
*/
class ForumChannel extends GuildChannel {
constructor(guild, data, client) {
super(guild, data, client, false);
/**
* A manager of the threads belonging to this channel
* @type {GuildForumThreadManager}
*/
this.threads = new GuildForumThreadManager(this);
this._patch(data);
}
_patch(data) {
super._patch(data);
if ('available_tags' in data) {
/**
* The set of tags that can be used in this channel.
* @type {GuildForumTag[]}
*/
this.availableTags = data.available_tags.map(tag => transformAPIGuildForumTag(tag));
} else {
this.availableTags ??= [];
}
if ('default_reaction_emoji' in data) {
/**
* The emoji to show in the add reaction button on a thread in a guild forum channel
* @type {?DefaultReactionEmoji}
*/
this.defaultReactionEmoji =
data.default_reaction_emoji && transformAPIGuildDefaultReaction(data.default_reaction_emoji);
} else {
this.defaultReactionEmoji ??= null;
}
if ('default_thread_rate_limit_per_user' in data) {
/**
* The initial rate limit per user (slowmode) to set on newly created threads in a channel.
* @type {?number}
*/
this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user;
} else {
this.defaultThreadRateLimitPerUser ??= null;
}
if ('rate_limit_per_user' in data) {
/**
* The rate limit per user (slowmode) for this channel.
* @type {?number}
*/
this.rateLimitPerUser = data.rate_limit_per_user;
} else {
this.rateLimitPerUser ??= null;
}
if ('default_auto_archive_duration' in data) {
/**
* The default auto archive duration for newly created threads in this channel.
* @type {?ThreadAutoArchiveDuration}
*/
this.defaultAutoArchiveDuration = data.default_auto_archive_duration;
} else {
this.defaultAutoArchiveDuration ??= null;
}
if ('nsfw' in data) {
/**
* If this channel is considered NSFW.
* @type {boolean}
*/
this.nsfw = data.nsfw;
} else {
this.nsfw ??= false;
}
if ('topic' in data) {
/**
* The topic of this channel.
* @type {?string}
*/
this.topic = data.topic;
}
if ('default_sort_order' in data) {
/**
* The default sort order mode used to order posts
* @type {?SortOrderType}
*/
this.defaultSortOrder = SortOrderTypes[data.default_sort_order];
} else {
this.defaultSortOrder ??= null;
}
/**
* The default layout type used to display posts
* @type {ForumLayoutType}
*/
this.defaultForumLayout = ForumLayoutTypes[data.default_forum_layout];
}
/**
* Sets the available tags for this forum channel
* @param {GuildForumTagData[]} availableTags The tags to set as available in this channel
* @param {string} [reason] Reason for changing the available tags
* @returns {Promise<ForumChannel>}
*/
setAvailableTags(availableTags, reason) {
return this.edit({ availableTags }, reason);
}
/**
* Sets the default reaction emoji for this channel
* @param {?DefaultReactionEmoji} defaultReactionEmoji The emoji to set as the default reaction emoji
* @param {string} [reason] Reason for changing the default reaction emoji
* @returns {Promise<ForumChannel>}
*/
setDefaultReactionEmoji(defaultReactionEmoji, reason) {
return this.edit({ defaultReactionEmoji }, reason);
}
/**
* Sets the default rate limit per user (slowmode) for new threads in this channel
* @param {number} defaultThreadRateLimitPerUser The rate limit to set on newly created threads in this channel
* @param {string} [reason] Reason for changing the default rate limit
* @returns {Promise<ForumChannel>}
*/
setDefaultThreadRateLimitPerUser(defaultThreadRateLimitPerUser, reason) {
return this.edit({ defaultThreadRateLimitPerUser }, reason);
}
/**
* Sets the default sort order mode used to order posts
* @param {?SortOrderType} defaultSortOrder The default sort order mode to set on this channel
* @param {string} [reason] Reason for changing the default sort order
* @returns {Promise<ForumChannel>}
*/
setDefaultSortOrder(defaultSortOrder, reason) {
return this.edit({ defaultSortOrder }, reason);
}
/**
* Sets the default forum layout type used to display posts
* @param {ForumLayoutType} defaultForumLayout The default forum layout type to set on this channel
* @param {string} [reason] Reason for changing the default forum layout
* @returns {Promise<ForumChannel>}
*/
setDefaultForumLayout(defaultForumLayout, reason) {
return this.edit({ defaultForumLayout }, reason);
}
/**
* Creates an invite to this guild channel.
* @param {CreateInviteOptions} [options={}] The options for creating the invite
* @returns {Promise<Invite>}
* @example
* // Create an invite to a channel
* channel.createInvite()
* .then(invite => console.log(`Created an invite with a code of ${invite.code}`))
* .catch(console.error);
*/
createInvite(options) {
return this.guild.invites.create(this.id, options);
}
/**
* Fetches a collection of invites to this guild channel.
* Resolves with a collection mapping invites by their codes.
* @param {boolean} [cache=true] Whether or not to cache the fetched invites
* @returns {Promise<Collection<string, Invite>>}
*/
fetchInvites(cache = true) {
return this.guild.invites.fetch({ channelId: this.id, cache });
}
/**
* Sets the default auto archive duration for all newly created threads in this channel.
* @param {ThreadAutoArchiveDuration} defaultAutoArchiveDuration The new default auto archive duration
* @param {string} [reason] Reason for changing the channel's default auto archive duration
* @returns {Promise<ForumChannel>}
*/
setDefaultAutoArchiveDuration(defaultAutoArchiveDuration, reason) {
return this.edit({ defaultAutoArchiveDuration }, reason);
}
/**
* Sets a new topic for the guild channel.
* @param {?string} topic The new topic for the guild channel
* @param {string} [reason] Reason for changing the guild channel's topic
* @returns {Promise<ForumChannel>}
* @example
* // Set a new channel topic
* channel.setTopic('needs more rate limiting')
* .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
* .catch(console.error);
*/
setTopic(topic, reason) {
return this.edit({ topic }, reason);
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
createWebhook() {}
fetchWebhooks() {}
setNSFW() {}
setRateLimitPerUser() {}
}
TextBasedChannel.applyToClass(ForumChannel, true, [
'send',
'lastMessage',
'lastPinAt',
'bulkDelete',
'sendTyping',
'createMessageCollector',
'awaitMessages',
'createMessageComponentCollector',
'awaitMessageComponent',
]);
module.exports = ForumChannel;

View File

@@ -10,6 +10,7 @@ const Integration = require('./Integration');
const Webhook = require('./Webhook');
const WelcomeScreen = require('./WelcomeScreen');
const { Error } = require('../errors');
const AutoModerationRuleManager = require('../managers/AutoModerationRuleManager');
const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager');
const GuildBanManager = require('../managers/GuildBanManager');
const GuildChannelManager = require('../managers/GuildChannelManager');
@@ -25,7 +26,6 @@ const VoiceStateManager = require('../managers/VoiceStateManager');
const {
ChannelTypes,
DefaultMessageNotificationLevels,
PartialTypes,
VerificationLevels,
ExplicitContentFilterLevels,
Status,
@@ -39,6 +39,7 @@ const Util = require('../util/Util');
let deprecationEmittedForSetChannelPositions = false;
let deprecationEmittedForSetRolePositions = false;
let deprecationEmittedForDeleted = false;
let deprecationEmittedForMe = false;
/**
* @type {WeakSet<Guild>}
@@ -117,6 +118,12 @@ class Guild extends AnonymousGuild {
*/
this.scheduledEvents = new GuildScheduledEventManager(this);
/**
* A manager of the auto moderation rules of this guild.
* @type {AutoModerationRuleManager}
*/
this.autoModerationRules = new AutoModerationRuleManager(this);
if (!data) return;
if (data.unavailable) {
/**
@@ -221,11 +228,15 @@ class Guild extends AnonymousGuild {
/**
* An array of enabled guild features, here are the possible values:
* * ANIMATED_ICON
* * AUTO_MODERATION
* * BANNER
* * COMMERCE
* * COMMUNITY
* * CREATOR_MONETIZABLE_PROVISIONAL
* * CREATOR_STORE_PAGE
* * DISCOVERABLE
* * FEATURABLE
* * INVITES_DISABLED
* * INVITE_SPLASH
* * MEMBER_VERIFICATION_GATE_ENABLED
* * NEWS
@@ -237,11 +248,16 @@ class Guild extends AnonymousGuild {
* * WELCOME_SCREEN_ENABLED
* * TICKETED_EVENTS_ENABLED
* * MONETIZATION_ENABLED
* <warn>`MONETIZATION_ENABLED` has been replaced.
* See [this pull request](https://github.com/discord/discord-api-docs/pull/5724) for more information.</warn>
* * MORE_STICKERS
* * THREE_DAY_THREAD_ARCHIVE
* * SEVEN_DAY_THREAD_ARCHIVE
* * PRIVATE_THREADS
* * ROLE_ICONS
* * RAID_ALERTS_DISABLED
* * ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE
* * ROLE_SUBSCRIPTIONS_ENABLED
* @typedef {string} Features
* @see {@link https://discord.com/developers/docs/resources/guild#guild-object-guild-features}
*/
@@ -286,14 +302,6 @@ class Guild extends AnonymousGuild {
this.premiumTier = PremiumTiers[data.premium_tier];
}
if ('premium_subscription_count' in data) {
/**
* The total number of boosts for this server
* @type {?number}
*/
this.premiumSubscriptionCount = data.premium_subscription_count;
}
if ('widget_enabled' in data) {
/**
* Whether widget images are enabled on this guild
@@ -371,6 +379,16 @@ class Guild extends AnonymousGuild {
this.maximumPresences ??= null;
}
if ('max_video_channel_users' in data) {
/**
* The maximum amount of users allowed in a video channel.
* @type {?number}
*/
this.maxVideoChannelUsers = data.max_video_channel_users;
} else {
this.maxVideoChannelUsers ??= null;
}
if ('approximate_member_count' in data) {
/**
* The approximate amount of members the guild has
@@ -419,12 +437,22 @@ class Guild extends AnonymousGuild {
if ('preferred_locale' in data) {
/**
* The preferred locale of the guild, defaults to `en-US`
* @type {string}
* @see {@link https://discord.com/developers/docs/dispatch/field-values#predefined-field-values-accepted-locales}
* @type {Locale}
* @see {@link https://discord.com/developers/docs/reference#locales}
*/
this.preferredLocale = data.preferred_locale;
}
if ('safety_alerts_channel_id' in data) {
/**
* The safety alerts channel's id for the guild
* @type {?Snowflake}
*/
this.safetyAlertsChannelId = data.safety_alerts_channel_id;
} else {
this.safetyAlertsChannelId ??= null;
}
if (data.channels) {
this.channels.cache.clear();
for (const rawChannel of data.channels) {
@@ -559,10 +587,19 @@ class Guild extends AnonymousGuild {
}
/**
* Widget channel for this guild
* Safety alerts channel for this guild
* @type {?TextChannel}
* @readonly
*/
get safetyAlertsChannel() {
return this.client.channels.resolve(this.safetyAlertsChannelId);
}
/**
* Widget channel for this guild
* @type {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel)}
* @readonly
*/
get widgetChannel() {
return this.client.channels.resolve(this.widgetChannelId);
}
@@ -588,15 +625,16 @@ class Guild extends AnonymousGuild {
/**
* The client user as a GuildMember of this guild
* @type {?GuildMember}
* @deprecated Use {@link GuildMemberManager#me} instead.
* @readonly
*/
get me() {
return (
this.members.resolve(this.client.user.id) ??
(this.client.options.partials.includes(PartialTypes.GUILD_MEMBER)
? this.members._add({ user: { id: this.client.user.id } }, true)
: null)
);
if (!deprecationEmittedForMe) {
process.emitWarning('Guild#me is deprecated. Use Guild#members#me instead.', 'DeprecationWarning');
deprecationEmittedForMe = true;
}
return this.members.me;
}
/**
@@ -698,9 +736,6 @@ class Guild extends AnonymousGuild {
* .catch(console.error);
*/
async fetchVanityData() {
if (!this.features.includes('VANITY_URL')) {
throw new Error('VANITY_URL');
}
const data = await this.client.api.guilds(this.id, 'vanity-url').get();
this.vanityURLCode = data.code;
this.vanityURLUses = data.uses;
@@ -773,7 +808,8 @@ class Guild extends AnonymousGuild {
/**
* Options used to fetch audit logs.
* @typedef {Object} GuildAuditLogsFetchOptions
* @property {Snowflake|GuildAuditLogsEntry} [before] Only return entries before this entry
* @property {Snowflake|GuildAuditLogsEntry} [before] Consider only entries before this entry
* @property {Snowflake|GuildAuditLogsEntry} [after] Consider only entries after this entry
* @property {number} [limit] The number of entries to return
* @property {UserResolvable} [user] Only return entries for actions made by this user
* @property {AuditLogAction|number} [type] Only return entries for this action type
@@ -789,18 +825,17 @@ class Guild extends AnonymousGuild {
* .then(audit => console.log(audit.entries.first()))
* .catch(console.error);
*/
async fetchAuditLogs(options = {}) {
if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id;
if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type];
async fetchAuditLogs({ before, after, limit, user, type } = {}) {
const data = await this.client.api.guilds(this.id)['audit-logs'].get({
query: {
before: options.before,
limit: options.limit,
user_id: this.client.users.resolveId(options.user),
action_type: options.type,
before: before?.id ?? before,
after: after?.id ?? after,
limit,
user_id: this.client.users.resolveId(user),
action_type: typeof type === 'string' ? GuildAuditLogs.Actions[type] : type,
},
});
return GuildAuditLogs.build(this, data);
}
@@ -808,24 +843,25 @@ class Guild extends AnonymousGuild {
* The data for editing a guild.
* @typedef {Object} GuildEditData
* @property {string} [name] The name of the guild
* @property {VerificationLevel|number} [verificationLevel] The verification level of the guild
* @property {ExplicitContentFilterLevel|number} [explicitContentFilter] The level of the explicit content filter
* @property {VoiceChannelResolvable} [afkChannel] The AFK channel of the guild
* @property {TextChannelResolvable} [systemChannel] The system channel of the guild
* @property {?(VerificationLevel|number)} [verificationLevel] The verification level of the guild
* @property {?(ExplicitContentFilterLevel|number)} [explicitContentFilter] The level of the explicit content filter
* @property {?VoiceChannelResolvable} [afkChannel] The AFK channel of the guild
* @property {?TextChannelResolvable} [systemChannel] The system channel of the guild
* @property {number} [afkTimeout] The AFK timeout of the guild
* @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the guild
* @property {GuildMemberResolvable} [owner] The owner of the guild
* @property {?(BufferResolvable|Base64Resolvable)} [splash] The invite splash image of the guild
* @property {?(BufferResolvable|Base64Resolvable)} [discoverySplash] The discovery splash image of the guild
* @property {?(BufferResolvable|Base64Resolvable)} [banner] The banner of the guild
* @property {DefaultMessageNotificationLevel|number} [defaultMessageNotifications] The default message notification
* level of the guild
* @property {?(DefaultMessageNotificationLevel|number)} [defaultMessageNotifications] The default message
* notification level of the guild
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild
* @property {TextChannelResolvable} [rulesChannel] The rules channel of the guild
* @property {TextChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild
* @property {string} [preferredLocale] The preferred locale of the guild
* @property {?TextChannelResolvable} [rulesChannel] The rules channel of the guild
* @property {?TextChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild
* @property {?string} [preferredLocale] The preferred locale of the guild
* @property {?TextChannelResolvable} [safetyAlertsChannel] The safety alerts channel of the guild
* @property {boolean} [premiumProgressBarEnabled] Whether the guild's premium progress bar is enabled
* @property {string} [description] The discovery description of the guild
* @property {?string} [description] The discovery description of the guild
* @property {Features[]} [features] The features of the guild
*/
@@ -906,7 +942,10 @@ class Guild extends AnonymousGuild {
if (typeof data.description !== 'undefined') {
_data.description = data.description;
}
if (data.preferredLocale) _data.preferred_locale = data.preferredLocale;
if (typeof data.preferredLocale !== 'undefined') _data.preferred_locale = data.preferredLocale;
if (typeof data.safetyAlertsChannel !== 'undefined') {
_data.safety_alerts_channel_id = this.client.channels.resolveId(data.safetyAlertsChannel);
}
if ('premiumProgressBarEnabled' in data) _data.premium_progress_bar_enabled = data.premiumProgressBarEnabled;
const newData = await this.client.api.guilds(this.id).patch({ data: _data, reason });
return this.client.actions.GuildUpdate.handle(newData).updated;
@@ -984,7 +1023,7 @@ class Guild extends AnonymousGuild {
/**
* Edits the level of the explicit content filter.
* @param {ExplicitContentFilterLevel|number} explicitContentFilter The new level of the explicit content filter
* @param {?(ExplicitContentFilterLevel|number)} explicitContentFilter The new level of the explicit content filter
* @param {string} [reason] Reason for changing the level of the guild's explicit content filter
* @returns {Promise<Guild>}
*/
@@ -995,7 +1034,7 @@ class Guild extends AnonymousGuild {
/* eslint-disable max-len */
/**
* Edits the setting of the default message notifications of the guild.
* @param {DefaultMessageNotificationLevel|number} defaultMessageNotifications The new default message notification level of the guild
* @param {?(DefaultMessageNotificationLevel|number)} defaultMessageNotifications The new default message notification level of the guild
* @param {string} [reason] Reason for changing the setting of the default message notifications
* @returns {Promise<Guild>}
*/
@@ -1031,7 +1070,7 @@ class Guild extends AnonymousGuild {
/**
* Edits the verification level of the guild.
* @param {VerificationLevel|number} verificationLevel The new verification level of the guild
* @param {?(VerificationLevel|number)} verificationLevel The new verification level of the guild
* @param {string} [reason] Reason for changing the guild's verification level
* @returns {Promise<Guild>}
* @example
@@ -1046,7 +1085,7 @@ class Guild extends AnonymousGuild {
/**
* Edits the AFK channel of the guild.
* @param {VoiceChannelResolvable} afkChannel The new AFK channel
* @param {?VoiceChannelResolvable} afkChannel The new AFK channel
* @param {string} [reason] Reason for changing the guild's AFK channel
* @returns {Promise<Guild>}
* @example
@@ -1061,7 +1100,7 @@ class Guild extends AnonymousGuild {
/**
* Edits the system channel of the guild.
* @param {TextChannelResolvable} systemChannel The new system channel
* @param {?TextChannelResolvable} systemChannel The new system channel
* @param {string} [reason] Reason for changing the guild's system channel
* @returns {Promise<Guild>}
* @example
@@ -1166,7 +1205,7 @@ class Guild extends AnonymousGuild {
/**
* Edits the rules channel of the guild.
* @param {TextChannelResolvable} rulesChannel The new rules channel
* @param {?TextChannelResolvable} rulesChannel The new rules channel
* @param {string} [reason] Reason for changing the guild's rules channel
* @returns {Promise<Guild>}
* @example
@@ -1181,7 +1220,7 @@ class Guild extends AnonymousGuild {
/**
* Edits the community updates channel of the guild.
* @param {TextChannelResolvable} publicUpdatesChannel The new community updates channel
* @param {?TextChannelResolvable} publicUpdatesChannel The new community updates channel
* @param {string} [reason] Reason for changing the guild's community updates channel
* @returns {Promise<Guild>}
* @example
@@ -1196,7 +1235,7 @@ class Guild extends AnonymousGuild {
/**
* Edits the preferred locale of the guild.
* @param {string} preferredLocale The new preferred locale of the guild
* @param {?string} preferredLocale The new preferred locale of the guild
* @param {string} [reason] Reason for changing the guild's preferred locale
* @returns {Promise<Guild>}
* @example
@@ -1209,6 +1248,21 @@ class Guild extends AnonymousGuild {
return this.edit({ preferredLocale }, reason);
}
/**
* Edits the safety alerts channel of the guild.
* @param {?TextChannelResolvable} safetyAlertsChannel The new safety alerts channel
* @param {string} [reason] Reason for changing the guild's safety alerts channel
* @returns {Promise<Guild>}
* @example
* // Edit the guild safety alerts channel
* guild.setSafetyAlertsChannel(channel)
* .then(updated => console.log(`Updated guild safety alerts channel to ${updated.safetyAlertsChannel.name}`))
* .catch(console.error);
*/
setSafetyAlertsChannel(safetyAlertsChannel, reason) {
return this.edit({ safetyAlertsChannel }, reason);
}
/**
* Edits the enabled state of the guild's premium progress bar
* @param {boolean} [enabled=true] The new enabled state of the guild's premium progress bar
@@ -1306,13 +1360,24 @@ class Guild extends AnonymousGuild {
return this;
}
/**
* Sets whether this guild's invites are disabled.
* @param {boolean} [disabled=true] Whether the invites are disabled
* @returns {Promise<Guild>}
*/
disableInvites(disabled = true) {
const features = this.features.filter(feature => feature !== 'INVITES_DISABLED');
if (disabled) features.push('INVITES_DISABLED');
return this.edit({ features });
}
/**
* Leaves the guild.
* @returns {Promise<Guild>}
* @example
* // Leave a guild
* guild.leave()
* .then(g => console.log(`Left the guild ${g}`))
* .then(guild => console.log(`Left the guild: ${guild.name}`))
* .catch(console.error);
*/
async leave() {

View File

@@ -1,13 +1,15 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const ApplicationCommand = require('./ApplicationCommand');
const AutoModerationRule = require('./AutoModerationRule');
const { GuildScheduledEvent } = require('./GuildScheduledEvent');
const Integration = require('./Integration');
const Invite = require('./Invite');
const { StageInstance } = require('./StageInstance');
const { Sticker } = require('./Sticker');
const Webhook = require('./Webhook');
const { OverwriteTypes, PartialTypes } = require('../util/Constants');
const { OverwriteTypes, PartialTypes, AutoModerationRuleTriggerTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');
const Util = require('../util/Util');
@@ -26,6 +28,8 @@ const Util = require('../util/Util');
* * STICKER
* * THREAD
* * GUILD_SCHEDULED_EVENT
* * APPLICATION_COMMAND
* * AUTO_MODERATION
* @typedef {string} AuditLogTargetType
*/
@@ -49,6 +53,8 @@ const Targets = {
STAGE_INSTANCE: 'STAGE_INSTANCE',
STICKER: 'STICKER',
THREAD: 'THREAD',
APPLICATION_COMMAND: 'APPLICATION_COMMAND',
AUTO_MODERATION: 'AUTO_MODERATION',
UNKNOWN: 'UNKNOWN',
};
@@ -102,6 +108,13 @@ const Targets = {
* * THREAD_CREATE: 110
* * THREAD_UPDATE: 111
* * THREAD_DELETE: 112
* * APPLICATION_COMMAND_PERMISSION_UPDATE: 121
* * AUTO_MODERATION_RULE_CREATE: 140
* * AUTO_MODERATION_RULE_UPDATE: 141
* * AUTO_MODERATION_RULE_DELETE: 142
* * AUTO_MODERATION_BLOCK_MESSAGE: 143
* * AUTO_MODERATION_FLAG_TO_CHANNEL: 144
* * AUTO_MODERATION_USER_COMMUNICATION_DISABLED: 145
* @typedef {?(number|string)} AuditLogAction
* @see {@link https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events}
*/
@@ -160,6 +173,13 @@ const Actions = {
THREAD_CREATE: 110,
THREAD_UPDATE: 111,
THREAD_DELETE: 112,
APPLICATION_COMMAND_PERMISSION_UPDATE: 121,
AUTO_MODERATION_RULE_CREATE: 140,
AUTO_MODERATION_RULE_UPDATE: 141,
AUTO_MODERATION_RULE_DELETE: 142,
AUTO_MODERATION_BLOCK_MESSAGE: 143,
AUTO_MODERATION_FLAG_TO_CHANNEL: 144,
AUTO_MODERATION_USER_COMMUNICATION_DISABLED: 145,
};
/**
@@ -193,13 +213,35 @@ class GuildAuditLogs {
}
}
/**
* Cached application commands, includes application commands from other applications
* @type {Collection<Snowflake, ApplicationCommand>}
* @private
*/
this.applicationCommands = new Collection();
if (data.application_commands) {
for (const command of data.application_commands) {
this.applicationCommands.set(command.id, new ApplicationCommand(guild.client, command, guild));
}
}
/**
* Cached auto moderation rules.
* @type {Collection<Snowflake, AutoModerationRule>}
* @private
*/
this.autoModerationRules = data.auto_moderation_rules.reduce(
(autoModerationRules, autoModerationRule) =>
autoModerationRules.set(autoModerationRule.id, guild.autoModerationRules._add(autoModerationRule)),
new Collection(),
);
/**
* The entries for this guild's audit logs
* @type {Collection<Snowflake, GuildAuditLogsEntry>}
*/
this.entries = new Collection();
for (const item of data.audit_log_entries) {
const entry = new GuildAuditLogsEntry(this, guild, item);
const entry = new GuildAuditLogsEntry(guild, item, this);
this.entries.set(entry.id, entry);
}
}
@@ -229,10 +271,12 @@ class GuildAuditLogs {
* * A sticker
* * A guild scheduled event
* * A thread
* * An application command
* * An auto moderation rule
* * An object with an id key if target was deleted
* * An object where the keys represent either the new value or the old value
* @typedef {?(Object|Guild|Channel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance|Sticker|
* GuildScheduledEvent)} AuditLogEntryTarget
* GuildScheduledEvent|ApplicationCommand|AutoModerationRule)} AuditLogEntryTarget
*/
/**
@@ -254,6 +298,8 @@ class GuildAuditLogs {
if (target < 100) return Targets.STICKER;
if (target < 110) return Targets.GUILD_SCHEDULED_EVENT;
if (target < 120) return Targets.THREAD;
if (target < 130) return Targets.APPLICATION_COMMAND;
if (target >= 140 && target < 150) return Targets.AUTO_MODERATION;
return Targets.UNKNOWN;
}
@@ -288,6 +334,8 @@ class GuildAuditLogs {
Actions.STICKER_CREATE,
Actions.GUILD_SCHEDULED_EVENT_CREATE,
Actions.THREAD_CREATE,
Actions.AUTO_MODERATION_RULE_CREATE,
Actions.AUTO_MODERATION_BLOCK_MESSAGE,
].includes(action)
) {
return 'CREATE';
@@ -313,6 +361,7 @@ class GuildAuditLogs {
Actions.STICKER_DELETE,
Actions.GUILD_SCHEDULED_EVENT_DELETE,
Actions.THREAD_DELETE,
Actions.AUTO_MODERATION_RULE_DELETE,
].includes(action)
) {
return 'DELETE';
@@ -335,6 +384,8 @@ class GuildAuditLogs {
Actions.STICKER_UPDATE,
Actions.GUILD_SCHEDULED_EVENT_UPDATE,
Actions.THREAD_UPDATE,
Actions.APPLICATION_COMMAND_PERMISSION_UPDATE,
Actions.AUTO_MODERATION_RULE_UPDATE,
].includes(action)
) {
return 'UPDATE';
@@ -352,7 +403,7 @@ class GuildAuditLogs {
* Audit logs entry.
*/
class GuildAuditLogsEntry {
constructor(logs, guild, data) {
constructor(guild, data, logs) {
const targetType = GuildAuditLogs.targetType(data.action_type);
/**
* The target type of this entry
@@ -378,6 +429,12 @@ class GuildAuditLogsEntry {
*/
this.reason = data.reason ?? null;
/**
* The id of the user that executed this entry
* @type {?Snowflake}
*/
this.executorId = data.user_id;
/**
* The user that executed this entry
* @type {?User}
@@ -385,7 +442,7 @@ class GuildAuditLogsEntry {
this.executor = data.user_id
? guild.client.options.partials.includes(PartialTypes.USER)
? guild.client.users._add({ id: data.user_id })
: guild.client.users.cache.get(data.user_id)
: guild.client.users.cache.get(data.user_id) ?? null
: null;
/**
@@ -398,9 +455,9 @@ class GuildAuditLogsEntry {
/**
* Specific property changes
* @type {?AuditLogChange[]}
* @type {AuditLogChange[]}
*/
this.changes = data.changes?.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) ?? null;
this.changes = data.changes?.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) ?? [];
/**
* The entry's id
@@ -429,7 +486,6 @@ class GuildAuditLogsEntry {
count: Number(data.options.count),
};
break;
case Actions.MESSAGE_PIN:
case Actions.MESSAGE_UNPIN:
this.extra = {
@@ -475,11 +531,30 @@ class GuildAuditLogsEntry {
channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id },
};
break;
case Actions.APPLICATION_COMMAND_PERMISSION_UPDATE:
this.extra = {
applicationId: data.options.application_id,
};
break;
case Actions.AUTO_MODERATION_BLOCK_MESSAGE:
case Actions.AUTO_MODERATION_FLAG_TO_CHANNEL:
case Actions.AUTO_MODERATION_USER_COMMUNICATION_DISABLED:
this.extra = {
autoModerationRuleName: data.options.auto_moderation_rule_name,
autoModerationRuleTriggerType: AutoModerationRuleTriggerTypes[data.options.auto_moderation_rule_trigger_type],
channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id },
};
break;
default:
break;
}
/**
* The id of the target of this entry
* @type {?Snowflake}
*/
this.targetId = data.target_id;
/**
* The target of this entry
* @type {?AuditLogEntryTarget}
@@ -495,12 +570,12 @@ class GuildAuditLogsEntry {
} else if (targetType === Targets.USER && data.target_id) {
this.target = guild.client.options.partials.includes(PartialTypes.USER)
? guild.client.users._add({ id: data.target_id })
: guild.client.users.cache.get(data.target_id);
: guild.client.users.cache.get(data.target_id) ?? null;
} else if (targetType === Targets.GUILD) {
this.target = guild.client.guilds.cache.get(data.target_id);
} else if (targetType === Targets.WEBHOOK) {
this.target =
logs.webhooks.get(data.target_id) ??
logs?.webhooks.get(data.target_id) ??
new Webhook(
guild.client,
this.changes.reduce(
@@ -535,10 +610,10 @@ class GuildAuditLogsEntry {
this.target =
data.action_type === Actions.MESSAGE_BULK_DELETE
? guild.channels.cache.get(data.target_id) ?? { id: data.target_id }
: guild.client.users.cache.get(data.target_id);
: guild.client.users.cache.get(data.target_id) ?? null;
} else if (targetType === Targets.INTEGRATION) {
this.target =
logs.integrations.get(data.target_id) ??
logs?.integrations.get(data.target_id) ??
new Integration(
guild.client,
this.changes.reduce(
@@ -603,6 +678,22 @@ class GuildAuditLogsEntry {
{ id: data.target_id, guild_id: guild.id },
),
);
} else if (targetType === Targets.APPLICATION_COMMAND) {
this.target = logs?.applicationCommands.get(data.target_id) ?? { id: data.target_id };
} else if (targetType === Targets.AUTO_MODERATION) {
this.target =
guild.autoModerationRules.cache.get(data.target_id) ??
new AutoModerationRule(
guild.client,
this.changes.reduce(
(o, c) => {
o[c.key] = c.new ?? c.old;
return o;
},
{ id: data.target_id, guild_id: guild.id },
),
guild,
);
} else if (data.target_id) {
this.target = guild[`${targetType.toLowerCase()}s`]?.cache.get(data.target_id) ?? { id: data.target_id };
}

View File

@@ -1,10 +1,9 @@
'use strict';
const { Channel } = require('./Channel');
const PermissionOverwrites = require('./PermissionOverwrites');
const { Error } = require('../errors');
const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager');
const { ChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
const { VoiceBasedChannelTypes } = require('../util/Constants');
const Permissions = require('../util/Permissions');
const Util = require('../util/Util');
@@ -16,6 +15,7 @@ const Util = require('../util/Util');
* - {@link NewsChannel}
* - {@link StoreChannel}
* - {@link StageChannel}
* - {@link ForumChannel}
* @extends {Channel}
* @abstract
*/
@@ -146,8 +146,21 @@ class GuildChannel extends Channel {
* @readonly
*/
get position() {
const sorted = this.guild._sortedChannels(this);
return [...sorted.values()].indexOf(sorted.get(this.id));
const selfIsCategory = this.type === 'GUILD_CATEGORY';
const types = Util.getSortableGroupTypes(this.type);
let count = 0;
for (const channel of this.guild.channels.cache.values()) {
if (!types.includes(channel.type)) continue;
if (!selfIsCategory && channel.parentId !== this.parentId) continue;
if (this.rawPosition === channel.rawPosition) {
if (BigInt(channel.id) < BigInt(this.id)) count++;
} else if (this.rawPosition > channel.rawPosition) {
count++;
}
}
return count;
}
/**
@@ -262,27 +275,6 @@ class GuildChannel extends Channel {
return this.guild.members.cache.filter(m => this.permissionsFor(m).has(Permissions.FLAGS.VIEW_CHANNEL, false));
}
/**
* The data for a guild channel.
* @typedef {Object} ChannelData
* @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 {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
* @property {?CategoryChannelResolvable} [parent] The parent of the channel
* @property {boolean} [lockPermissions]
* Lock the permissions of the channel to what the parent's permissions are
* @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites]
* Permission overwrites for the channel
* @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds
* @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration]
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The RTC region of the channel
*/
/**
* Edits the channel.
* @param {ChannelData} data The new data for the channel
@@ -294,64 +286,8 @@ class GuildChannel extends Channel {
* .then(console.log)
* .catch(console.error);
*/
async edit(data, reason) {
data.parent &&= this.client.channels.resolveId(data.parent);
if (typeof data.position !== 'undefined') {
const updatedChannels = await Util.setPosition(
this,
data.position,
false,
this.guild._sortedChannels(this),
this.client.api.guilds(this.guild.id).channels,
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
}
let permission_overwrites;
if (data.permissionOverwrites) {
permission_overwrites = data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
if (data.lockPermissions) {
if (data.parent) {
const newParent = this.guild.channels.resolve(data.parent);
if (newParent?.type === 'GUILD_CATEGORY') {
permission_overwrites = newParent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
} else if (this.parent) {
permission_overwrites = this.parent.permissionOverwrites.cache.map(o =>
PermissionOverwrites.resolve(o, this.guild),
);
}
}
const newData = await this.client.api.channels(this.id).patch({
data: {
name: (data.name ?? this.name).trim(),
type: ChannelTypes[data.type],
topic: data.topic,
nsfw: data.nsfw,
bitrate: data.bitrate ?? this.bitrate,
user_limit: data.userLimit ?? this.userLimit,
rtc_region: data.rtcRegion ?? this.rtcRegion,
parent_id: data.parent,
lock_permissions: data.lockPermissions,
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: data.defaultAutoArchiveDuration,
permission_overwrites,
},
reason,
});
return this.client.actions.ChannelUpdate.handle(newData).updated;
edit(data, reason) {
return this.guild.channels.edit(this, data, reason);
}
/**
@@ -415,30 +351,10 @@ class GuildChannel extends Channel {
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
* .catch(console.error);
*/
async setPosition(position, { relative, reason } = {}) {
const updatedChannels = await Util.setPosition(
this,
position,
relative,
this.guild._sortedChannels(this),
this.client.api.guilds(this.guild.id).channels,
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: updatedChannels,
});
return this;
setPosition(position, options = {}) {
return this.guild.channels.setPosition(this, position, options);
}
/**
* Data that can be resolved to an Application. This can be:
* * An Application
* * An Activity with associated Application
* * A Snowflake
* @typedef {Application|Snowflake} ApplicationResolvable
*/
/**
* Options used to clone a guild channel.
* @typedef {GuildChannelCreateOptions} GuildChannelCloneOptions
@@ -513,7 +429,7 @@ class GuildChannel extends Channel {
// This flag allows managing even if timed out
if (permissions.has(Permissions.FLAGS.ADMINISTRATOR, false)) return true;
if (this.guild.me.communicationDisabledUntilTimestamp > Date.now()) return false;
if (this.guild.members.me.communicationDisabledUntilTimestamp > Date.now()) return false;
const bitfield = VoiceBasedChannelTypes.includes(this.type)
? Permissions.FLAGS.MANAGE_CHANNELS | Permissions.FLAGS.CONNECT
@@ -544,7 +460,7 @@ class GuildChannel extends Channel {
* .catch(console.error);
*/
async delete(reason) {
await this.client.api.channels(this.id).delete({ reason });
await this.guild.channels.delete(this.id, reason);
return this;
}
}

View File

@@ -55,8 +55,8 @@ class GuildEmoji extends BaseGuildEmoji {
* @readonly
*/
get deletable() {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
return !this.managed && this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS);
if (!this.guild.members.me) throw new Error('GUILD_UNCACHED_ME');
return !this.managed && this.guild.members.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS);
}
/**
@@ -72,18 +72,8 @@ class GuildEmoji extends BaseGuildEmoji {
* Fetches the author for this emoji
* @returns {Promise<User>}
*/
async fetchAuthor() {
if (this.managed) {
throw new Error('EMOJI_MANAGED');
} else {
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS_AND_STICKERS)) {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
}
}
const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get();
this._patch(data);
return this.author;
fetchAuthor() {
return this.guild.emojis.fetchAuthor(this);
}
/**
@@ -137,7 +127,7 @@ class GuildEmoji extends BaseGuildEmoji {
* @returns {Promise<GuildEmoji>}
*/
async delete(reason) {
await this.client.api.guilds(this.guild.id).emojis(this.id).delete({ reason });
await this.guild.emojis.delete(this, reason);
return this;
}

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