Compare commits

...

262 commits

Author SHA1 Message Date
Daniel Bulant
2eaafaf1d3
Fix options 2020-12-16 18:00:47 +01:00
Daniel Bulant
c513df444e
Attempt to fix the create command function 2020-12-16 17:54:46 +01:00
Daniel Bulant
4f5cdf2814
Update README.md 2020-12-16 16:51:40 +01:00
Daniel Bulant
0ca50889ae
Merge pull request #2 from devsnek/interactions
Interactions
2020-12-16 15:15:31 +01:00
Gus Caplan
53412f6b9f
various stuff 2020-12-11 17:14:32 -06:00
Gus Caplan
2b8a6294af
fixes 2020-12-11 15:10:12 -06:00
Gus Caplan
8c65961a07
add ack api 2020-12-11 15:05:25 -06:00
Gus Caplan
4599ef954f
lint 2020-12-11 14:46:28 -06:00
Gus Caplan
790b6b3b5c
better command api 2020-12-11 14:44:58 -06:00
Gus Caplan
5be32161b9
basic working 2020-12-11 14:22:49 -06:00
Gus Caplan
db6d1b3ba9
fix webhook 2020-12-11 14:22:48 -06:00
Gus Caplan
21bfe3da7c
middleware model 2020-12-11 14:22:48 -06:00
Gus Caplan
a626dc8c41
more 2020-12-11 14:22:48 -06:00
Gus Caplan
c6f87aa32d
interactions wip 2020-12-11 14:22:46 -06:00
Advaith
2685b960d7
docs(Client): #emojis is a BaseGuildEmojiManager (#5048) 2020-12-08 22:07:06 +01:00
monbrey
60e5a0e46f
feat(Message|TextChannel): Inline replies (#4874)
* feat(Message): remove reply functionality

* feat(InlineReplies): add INLINE_REPLY constant/typing

* feat(InlineReplies): add Message#replyReference property

* feat(InlineReplies): add typings for sending inline replies

* feat(InlineReplies): provide support for inline-replying to messages

* feat(Message): add referencedMessage getter

* fix: check that Message#reference is defined in referencedMessage

* refactor(InlineReplies): rename property, rework Message resolution

* docs: update jsdoc for inline replies

* feat(Message): inline reply method

* fix(ApiMessage): finish renaming replyTo

* fix: jsdocs for Message#referencedMessage

Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>

* fix: restore reply typings

* fix: dont pass channel_id to API when replying

* chore: update jsdocs

* chore: more jsdoc updates

* feat(AllowedMentions): add typings for replied_user

* fix: naming conventions

* fix(Message): referenced_message is null, not undefined

* fix(MessageMentionOptions): repliedUser should be optional

* chore: get this back to the right state

* fix(ApiMessage): pass allowed_mentions when replying without content

* fix(ApiMessage): prevent mutation of client options

Co-authored-by: almostSouji <timoqueezle@gmail.com>
Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>
2020-12-08 21:08:26 +01:00
SpaceEEC
7365f40300
fix(Collector): throw an error if a non-function was provided as filter (#5034) 2020-12-08 20:11:44 +01:00
Carter
09d07553ab
docs(User): fix typos in jsdoc (#5060) 2020-12-06 18:03:39 +01:00
BannerBomb
e272fd6909
fix(BaseGuildEmoji): typo in requiresColons (#5076) 2020-12-06 17:59:12 +01:00
Antonio Román
90d458820b
chore(Engine): bump Node.js to v14.0.0 (#5067)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-12-06 17:58:08 +01:00
Advaith
9f3c3e0918
docs(WebSocketManager): fix type of status (#5059) 2020-11-30 01:08:54 +01:00
Daniel Bulant
ec560b8107
Merge pull request #1 from monbrey/inline-replies
Inline replies
2020-11-29 19:32:58 +01:00
monbrey
fbb1c93454 fix(ApiMessage): prevent mutation of client options 2020-11-27 12:43:26 +11:00
monbrey
6b322f47a0
fix(MessageReaction): set MessageReaction#me in patch method (#5047) 2020-11-25 23:55:29 +01:00
Amish Shah
4fcb9ebf30
fix(Voice*): filter out silent audio from video users (#5035) 2020-11-25 23:51:16 +01:00
izexi
53529bd05d
fix(GuildTemplate): 'guild' getter (#5040) 2020-11-25 23:50:28 +01:00
monbrey
88625a5b7d fix(ApiMessage): pass allowed_mentions when replying without content 2020-11-24 09:01:24 +11:00
monbrey
e43ad1eea9 chore: get this back to the right state 2020-11-24 08:29:40 +11:00
Jan
8d650a7250
feat: BaseGuildEmojiManager (#4934)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2020-11-22 19:48:30 +01:00
Jan
12a096b5f1
fix(RoleManager): fix ID return value, change return type to collection (#4935)
Co-authored-by: Ishmaam Khan <ishmaamk@gmail.com>
2020-11-22 19:39:19 +01:00
Advaith
6f3076325e
remove User#locale (#4932) 2020-11-22 19:39:06 +01:00
anandre
8c8883ef26
Remove Guild#member (#4890) 2020-11-22 19:21:01 +01:00
Junseo Park
4b555fdf4c
feat(Message): added string type for message nonce (#4782)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-11-22 19:13:38 +01:00
monbrey
863734aba4
feat(GuildMemberManager): throw TypeError on incorrect GuildMemberManager#ban params (#4816)
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-11-22 19:13:07 +01:00
iCrawl
1f4b9fe749
chore(Release): version up 2020-11-22 14:03:20 +01:00
Matt (IPv4) Cowley
2a6c363a8a
feat(Shard): shard-specific broadcastEval/fetchClientValues + shard Id util (#4991) 2020-11-22 13:35:18 +01:00
SpaceEEC
643f96c79b
fix(Guild): fetch member if already in the guild (#4967) 2020-11-21 17:16:22 +01:00
izexi
2b2994badc
feat: add support for guild templates (#4907)
Co-authored-by: Noel <buechler.noel@outlook.com>
2020-11-21 15:09:56 +01:00
SpaceEEC
eaecd0e8b7
fix(User): only assign to bot initially or if info is actually present (#4990) 2020-11-20 16:48:05 +01:00
SpaceEEC
2e940e635d
fix(GuildMemberUpdate): cache incoming members & use partials if enabled (#4986)
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-11-20 16:20:47 +01:00
SpaceEEC
8b91ac5d7e
fix(MessageReaction*Action): correctly cache incoming members and users (#4969) 2020-11-20 16:19:18 +01:00
Sugden
7faa73a561
feat: add missing error codes (#5008)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-11-20 16:18:13 +01:00
monbrey
eadeeed72e fix(MessageMentionOptions): repliedUser should be optional 2020-11-19 18:56:09 +11:00
monbrey
939c495ebb fix(Message): referenced_message is null, not undefined 2020-11-18 08:16:20 +11:00
Ashley Meadows
042e071a64
fix(MessageReaction): add client property to typings (#5003)
close #5002

Co-authored-by: Ashley Meadows <itsa-sh@users.noreply.github.com>
2020-11-17 22:05:03 +01:00
monbrey
065e89337e fix: naming conventions 2020-11-17 13:33:59 +11:00
monbrey
1028183c23 Merge remote-tracking branch 'upstream/master' into inline-replies 2020-11-17 10:40:31 +11:00
monbrey
b61a367392 feat(AllowedMentions): add typings for replied_user 2020-11-17 10:39:37 +11:00
monbrey
ff2dbfa52d chore: more jsdoc updates 2020-11-17 08:30:10 +11:00
monbrey
3463acafca chore: update jsdocs 2020-11-17 08:29:50 +11:00
monbrey
274ae9935e fix: dont pass channel_id to API when replying 2020-11-17 08:29:18 +11:00
monbrey
2eafeeca55 fix: restore reply typings 2020-11-17 08:24:16 +11:00
HarmoGlace
b8fd3f65d9
feat(Message): add crosspostable property (#4903)
Co-authored-by: Advaith <advaithj1@gmail.com>
Co-authored-by: Alex Hîncu <teesealz@gmail.com>
2020-11-01 12:32:20 +01:00
Christopher Bradshaw
efd7849ed0
docs: use npm ci instead of npm install (#4928)
Use `npm ci` instead of `npm install` after cloning the repository.
2020-11-01 12:30:25 +01:00
Matt (IPv4) Cowley
adf2e872f8
fix(Shard): don't pass event arguments to exit handler (#4957) 2020-11-01 12:29:29 +01:00
Matt (IPv4) Cowley
ed8b3cc9ea
fix(PackageLock): reinstall GitHub docgen dev dependency (#4958) 2020-11-01 12:29:00 +01:00
iCrawl
7ec0bd93b0
chore(Release): version upgrade 2020-10-24 06:27:55 +02:00
monbrey
3d158f4448
fix(Action): attempt to get a User if GuildMember not returned (#4922) 2020-10-24 06:25:35 +02:00
Sugden
250c3ae3c1
fix(GuildChannel): parentID shouldn't be set in the constructor (#4919) 2020-10-19 22:24:18 +02:00
iCrawl
94c9cc2300
fix(Webpack): revert webpack upgrade 2020-10-19 18:46:49 +02:00
iCrawl
e9f36b5041
chore(Release): version upgrade 2020-10-19 18:27:26 +02:00
Souji
30808f9f0b
feat(Message): allow custom emoji format for react (#4895) 2020-10-17 15:54:22 +02:00
Sugden
af670fc718
refactor: improve the accuracy of docs/improve docs (#4845)
Co-authored-by: Noel <icrawltogo@gmail.com>
2020-10-17 15:53:02 +02:00
Jan
4bbe716aa0
fix(esm): add missing exports (#4911) 2020-10-17 15:47:56 +02:00
Jan
a7af4a8837
docs(PresenceData): add YouTube and remove application (#4910) 2020-10-17 15:47:49 +02:00
Noel
89feedad98
revert: "fix(GuildEmojiManager): check for guild in methods that use it" (#4912)
This reverts commit 728b3f939c.
2020-10-17 15:46:10 +02:00
Jan
728b3f939c
fix(GuildEmojiManager): check for guild in methods that use it (#4886) 2020-10-17 15:40:39 +02:00
Sugden
7db6978012
fix(GuildMember): properly check permissions for hasPermissions (#4677) 2020-10-17 15:40:23 +02:00
Jiralite
6261dd65d3
fix(GuildEmojiCreate): Prevent double fire from emoji creation (#4863) 2020-10-17 15:40:04 +02:00
izexi
a45cc112e5
fix(GuildMemberManager): options.roles on 'prune' (#4838) 2020-10-17 15:39:29 +02:00
Constantinos
b8aa967226
ci: use npm ci instead of npm install (#4877)
Use npm ci instead of npm install when installing dependencies in CI.
2020-10-17 15:38:53 +02:00
Adrian Paschkowski
6e4308bfde
fix(GuildChannel): Default parentID to null (#4881) 2020-10-17 15:36:16 +02:00
Adrian Paschkowski
dd12912124
fix(Actions): Avoid crash in InviteCreate with unknown channel (#4882) 2020-10-17 15:36:02 +02:00
Adrian Paschkowski
937153a92f
fix(GuildMemberManager): Use actually random nonce in fetch (#4884)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2020-10-17 15:35:22 +02:00
Matt (IPv4) Cowley
c412cd7521
feat(Message): add messageEditHistoryMaxSize to limit stored msg edits (#4867) 2020-10-17 15:34:49 +02:00
cherryblossom000
4a6fb9a7d4
types(TextBasedChannel): make lastPinAt nullable (#4842)
This commit makes `TextBasedChannelFields#lastPinAt` nullable in the
typings.
2020-10-17 15:33:57 +02:00
cherryblossom000
824e92229d
types(Activity): move flags from Presence to Activity (#4843)
This commit moves the `flags` property from `Presence` to `Activity` in
the typings.
2020-10-17 15:33:37 +02:00
Jiralite
0b59141054
types(GuildPreview): Make description nullable (#4854)
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
2020-10-17 15:33:08 +02:00
yuta0801
b9ad51049e
fix(GuildChannel): make setTopic argument nullable (#4875) 2020-10-17 15:32:32 +02:00
monbrey
08286459cb
fix: jsdocs for Message#referencedMessage
Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>
2020-10-08 08:15:08 +11:00
monbrey
2e96b9a606 fix(ApiMessage): finish renaming replyTo 2020-10-07 21:39:01 +11:00
monbrey
ef1856bb1f Merge remote-tracking branch 'souji/maj/remove-msg-reply' into inline-replies 2020-10-07 21:09:05 +11:00
monbrey
4f7c207c99 feat(Message): inline reply method 2020-10-07 20:58:50 +11:00
monbrey
975b6cbd94 docs: update jsdoc for inline replies 2020-10-07 14:42:20 +11:00
monbrey
67c2e56647 refactor(InlineReplies): rename property, rework Message resolution 2020-10-07 14:18:17 +11:00
monbrey
ab0d6fc5c9 fix: check that Message#reference is defined in referencedMessage 2020-10-05 18:19:39 +11:00
monbrey
b8f50a09d2 feat(Message): add referencedMessage getter 2020-10-05 17:40:05 +11:00
monbrey
9f3108052c feat(InlineReplies): provide support for inline-replying to messages 2020-10-02 16:10:36 +10:00
monbrey
a5ce6cfa9a feat(InlineReplies): add typings for sending inline replies 2020-10-02 16:10:36 +10:00
monbrey
ab5ee838a3 feat(InlineReplies): add Message#replyReference property 2020-10-02 16:10:27 +10:00
monbrey
0ed281888d feat(InlineReplies): add INLINE_REPLY constant/typing 2020-10-02 14:57:40 +10:00
almostSouji
bc4dc22c1f
feat(Message): remove reply functionality 2020-10-01 14:10:36 +02:00
Antonio Román
d2341654fe
fix(Rest): resolved a regression, added retried AbortError (#4852) 2020-09-29 18:05:54 +02:00
Jan
169d4c3bff
refactor(ReactionUserManager): use client property (#4829) 2020-09-25 23:46:31 +02:00
monbrey
13d64e6fa6
fix(MessageManager): throw if delete param is not MessageResolvable (#4825) 2020-09-25 23:46:06 +02:00
Jan
f83b3d7fc1
feat(NewsChannel): add support for following (#4805)
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-09-25 23:45:47 +02:00
cherryblossom000
f2bbad36d5
feat(GuildManager): add AFK and system channel options in create (#4837)
This commit adds support for the `afk_channel_id`, `afk_timeout`, and
`system_channel_id` parameters in the
[create guild](https://discord.com/developers/docs/resources/guild#create-guild-json-params)
endpoint by adding the `afkChannelID`, `afkTimeout`, and
`systemChannelID` options in `GuildManager#create`.

This commit also fixes a minor bug in `create` when specifying types for
the channels due to the channel types not being changed from `'text'`,
`'voice'` etc to the corresponding numbers, so Discord would return an
error.
2020-09-25 23:44:32 +02:00
Matt (IPv4) Cowley
77c0788b2c
fix(Shard): avoid caching null child in eval/fetchClientValue (#4823) 2020-09-25 23:43:32 +02:00
MrWasdennnoch
4e79e39e22
fix(Action): Sanity-Check if Discord includes all required data (#4841) 2020-09-25 23:42:49 +02:00
Antonio Román
32fe72f909
feat(Rest): switch queue to AsyncQueue (#4835)
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-09-25 23:42:24 +02:00
Ryan Munro
1e63f3756e
fix(Message): use Promise#reject instead of Throw on Message#delete (#4818) 2020-09-15 18:35:54 +02:00
MrWasdennnoch
8fa3a89482
fix(Action): Don't crash when partials are disabled (#4822) 2020-09-15 18:35:20 +02:00
Advaith
9c76129a23
feat(ActivityTypes): add Competing (type 5) (#4824) 2020-09-15 18:33:52 +02:00
Noel
01ceda5b0c
chore(Deps): bl vulnerability (#4813) 2020-09-13 12:48:53 +02:00
MrWasdennnoch
eeb4c14754
fix(Partials): Use more user objects available from the gateway (#4791) 2020-09-13 12:09:12 +02:00
Johnson Chen
bcb7c721dc
feat(Message): add support for crossposting (#4105)
Co-authored-by: Advaith <advaithj1@gmail.com>
Co-authored-by: Joe <56809242+Jo3-L@users.noreply.github.com>
Co-authored-by: Jan <66554238+Vaporox@users.noreply.github.com>
Co-authored-by: Noel <icrawltogo@gmail.com>
2020-09-13 12:07:56 +02:00
dependabot[bot]
0da65becd3
chore(deps): bump node-fetch from 2.6.0 to 2.6.1 (#4812)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-13 11:57:44 +02:00
Sugden
422a4dda68
typings(Guild): document RELAY_ENABLED feature (#4788) 2020-09-08 09:58:56 +02:00
anandre
222137dcd1
docs(Role): Update various Role method descriptions (#4798)
Co-authored-by: Papaia <43409674+Papaia@users.noreply.github.com>
2020-09-08 09:58:11 +02:00
Quentin
372a405926
docs(ReactionCollector): Revise JSDoc for ReactionCollector#dispose and #remove (#4709)
Co-authored-by: Amish Shah <amishshah.2k@gmail.com>
Co-authored-by: uhKevinMC <plainkevin123@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Noel <icrawltogo@gmail.com>
2020-09-08 09:57:39 +02:00
anandre
dfd63bdb6b
docs(Guild): Guild.setName() example (#4797)
The docs example was incorrect, as the parameter is called `updated` but was later referenced as `guild`.  This PR fixes that by changing it to `updated` to match other examples, such as `setRegion()`
2020-09-05 20:18:00 +02:00
Papaia
5b39737d49
fix(lint): RESTManager warning (#4796)
Co-authored-by: Papaia <43409674+ItsPapaia@users.noreply.github.com>
2020-09-05 20:16:09 +02:00
cherryblossom000
904aecfdb7
types: don't use readonly arrays in interfaces (#4794)
This reverts some of the changes in f451be05 so that this works:

```ts
const embed: MessageEmbedOptions = {
  fields: [{
    // fixed stuff
  }],
};
if (/* condition */) {
  embed.fields.push({
    // conditional stuff
  });
}
```

See https://github.com/discordjs/discord.js/pull/4692#issuecomment-687252066.
2020-09-05 20:14:39 +02:00
MrWasdennnoch
a28754b892
fix(Typings): remove Partial types from some events (#4781) 2020-09-05 20:13:59 +02:00
Sugden
b43e742503
docs(GuildChannel): ThisType should be this (#4793) 2020-09-05 10:20:32 +02:00
Darqam
8ac25d37d9
docs(MessageManager): Update example for fetchPinned (#4785)
Example showed the method for channel and not messageManager
2020-09-04 20:19:51 +02:00
Souji
77b6a7d5bd
fix(Util): throw token invalid for fetching rec. shard amount (#4779) 2020-09-04 12:51:15 +02:00
MrWasdennnoch
aa25608c52
typings(PartialUser): fix PartialUser remove deleted property (#4773) 2020-09-03 09:53:44 +02:00
Jan
b0ab37ddc0
feat(Channel): add isText() type guard (#4745) 2020-08-31 09:59:17 +02:00
Sugden
3141f7cb04
feat(Guild): add includeApplications option for fetchIntegrations (#4762) 2020-08-31 09:17:53 +02:00
Sugden
7ba9440053
fix(Guild): cache fetched widget data (#4760) 2020-08-31 09:16:53 +02:00
Sugden
f97316319f
feat(UserFlags): add renamed UserFlags (#4761) 2020-08-30 00:34:37 +02:00
Tristan Guichaoua
405b487dc3
fix(Typing): change NodeJS.Timer into NodeJS.Timeout (#4755) 2020-08-29 18:54:39 +02:00
Tristan Guichaoua
b48b782c87
chore(Prettier): add settings for prettier plugin (#4756)
Co-authored-by: Papaia <43409674+Papaia@users.noreply.github.com>
2020-08-29 12:08:47 +02:00
cherryblossom000
74763ef3fb
types: don't allow any object in the first parameter if second parameter is not given in TextBasedChannel#send (#4736) 2020-08-29 12:08:04 +02:00
Carter
74ebb650df
style: remove unnecessary eslint comment (#4758) 2020-08-29 12:05:33 +02:00
Louis
a363b90fa5
docs(BaseGuildEmoji): account for optional properties (#4723) 2020-08-28 14:19:53 +02:00
Sugden
6aab9c3d64
fix: correctly import extendable classes (#4744) 2020-08-28 14:19:20 +02:00
Tristan Guichaoua
2dc70af717
types: add all types for GuildAuditLogsEntry#target (#4738)
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-08-28 14:18:45 +02:00
Sugden
46acfac327
refactor(Client): remove non-existant property from toJSON (#4750) 2020-08-28 14:18:17 +02:00
Sugden
727b29c85d
feat(Client): allow options for generateInvite (#4741) 2020-08-28 14:17:37 +02:00
Sugden
e0e271162c
fix(typings): bot cannot be null (#4719) 2020-08-27 16:40:36 +02:00
InkoHX
cfc68677ee
docs(ClientOptions): fix typo (#4730) 2020-08-27 16:39:55 +02:00
Noel
43c4d80b12
ci(CodeScanning): add CodeQL code scanning workflow 2020-08-24 19:27:00 +02:00
Carter
05c9e30163
docs(APIMessage): fix wording on comment (#4717) 2020-08-17 09:56:18 +02:00
Carter
b6167d8c3b
docs: update jsdoc type for User#bot (#4716) 2020-08-17 09:45:57 +02:00
iCrawl
56e8ef2d38
chore(Release): version upgrade 2020-08-15 20:38:09 +02:00
Jan
db512d8f62
fix(User): set User#bot to false if not partial (#4706)
Co-authored-by: Noel <icrawltogo@gmail.com>
2020-08-15 20:04:32 +02:00
Noel
5249cf33e5
revert(Shard): "fix missing child_process silent option of Shard to allow listening to output" (#4707)
This reverts commit 58d1589a55.
2020-08-15 12:50:05 +02:00
Jan
09bde74e43
chore: bump version in package-lock.json (#4705) 2020-08-15 12:38:40 +02:00
iCrawl
a4dbfdce59
chore(Release): version upgrade 2020-08-14 21:56:11 +02:00
Noel
dea48d64a5
chore(Deps): upgrade deps (#4701) 2020-08-14 21:46:23 +02:00
Advaith
178439ef8c
feat: trigger userUpdate on GUILD_MEMBER_UPDATE (#4697) 2020-08-14 20:49:44 +02:00
Carter
f1194afd7c
feat(GuildMemberManager#prune): roles query param (#4142)
Co-authored-by: izexi <43889168+izexi@users.noreply.github.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: kyranet <kyradiscord@gmail.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Noel <icrawltogo@gmail.com>
2020-08-14 20:14:31 +02:00
Carter
2742923df4
feat(GuildManager): adds GuildManager#fetch (#4086)
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-08-14 19:34:19 +02:00
Human
0b38c5d8b3
docs: updated applications URL (#4699) 2020-08-14 19:33:11 +02:00
cherryblossom000
f451be0519
feat(typings): use readonly arrays in parameters (#4692)
Co-authored-by: Noel <icrawltogo@gmail.com>
2020-08-13 20:43:51 +02:00
Advaith
f991bd46f3
chore(Constants): update large_threshold default (#4698) 2020-08-13 20:38:59 +02:00
Quentin
139e56c774
docs(ReactionCollector): update remove and dispose events (#4136)
Co-authored-by: Amish Shah <amishshah.2k@gmail.com>
Co-authored-by: uhKevinMC <plainkevin123@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: Noel <icrawltogo@gmail.com>
2020-08-12 23:22:45 +02:00
Androz
e7eda72c9d
feat(typings): add number type for setExplicitContentFilter method (#4694) 2020-08-12 23:18:58 +02:00
Sugden
980243f2d5
fix(Partials): correctly set properties as nullable (#4636) 2020-08-12 21:26:59 +02:00
Sugden
b6ddd4ce41
fix(APIMessage): add reply user to allowedMentions (#4591) 2020-08-12 21:25:38 +02:00
Arthur
6caeaeb391
fix(MessageReactionAdd): prevent double messageReactionAdd triggering (#4682)
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-08-12 21:24:45 +02:00
Carter
290938bf80
feat: bypass cache check with forceFetch param (#4592) 2020-08-12 21:23:04 +02:00
Souji
0225851e40
feat(BitField): add problematic bit to error (#4617) 2020-08-12 12:37:01 +02:00
Sardonyx
2a7f749d5a
docs(Embeds): Added descriptions to the typedefs (#4303)
Co-authored-by: RDambrosio <rdambrosio016@gmail.com>
2020-08-12 12:29:02 +02:00
Advaith
57ca3d7843
feat(Guild): updates for Community guilds (#4377)
Co-authored-by: SpaceEEC <spaceeec@users.noreply.github.com>
2020-08-12 12:21:17 +02:00
Advaith
de8d26d791
docs(Constants): Improve large_threshold description (#3744)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Crawl <icrawltogo@gmail.com>
2020-08-12 12:17:13 +02:00
Advaith
5be6630843
feat(Guild): discovery splash (#4619)
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
2020-08-12 11:09:18 +02:00
Advaith
446bbfe9eb
docs(Ban): days must be 0-7 (#4693)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Noel <icrawltogo@gmail.com>
2020-08-12 09:35:33 +02:00
Tristan Guichaoua
f2f31a14c9
feat(types): BitFieldResolvable use ReadonlyArray (#4604) 2020-08-12 09:34:24 +02:00
Sugden
e92cbc444b
feat: deprecate GuildEmbed methods and properties in favour of GuildWidget (#4121) 2020-08-12 09:33:00 +02:00
Advaith
baffbdb541
fix(Integration): user might not be present (#4691)
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
2020-08-12 09:30:34 +02:00
Bence
b7740d4859
feat(GuildEmoji): cache the author (#4334)
Co-authored-by: Papaia <43409674+Papaia@users.noreply.github.com>
2020-08-12 09:27:00 +02:00
Souji
599cde3627
fix(GuildChannel): make lockPermissions use parent overwrites (#4627)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2020-08-12 09:23:31 +02:00
Advaith
bd2bb0e1c7
docs(Welcome): change discord badge to shields.io for consistency (#4633) 2020-08-12 09:22:44 +02:00
samsamson33
03580b23a3
feat(Util): add missing colors to docs (#3843)
Co-authored-by: Crawl <icrawltogo@gmail.com>
2020-08-11 23:40:07 +02:00
Jan
9d747d14c5
docs(Client): fix docs for login method (#4350) 2020-08-11 23:36:25 +02:00
Nathan Franke
124afeb843
fix(Collector): support async (#4123) 2020-08-11 23:34:47 +02:00
Hayden Andreyka
05cbf70486
docs(Guild): clarify vanity URL documentation (#4125)
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Crawl <icrawltogo@gmail.com>
2020-08-11 23:05:03 +02:00
Jared Gesser
58d1589a55
fix missing child_process silent option of Shard to allow listening to output (#4308) 2020-08-11 23:01:19 +02:00
Sugden
49ad279c52
refactor(GuildMemberManager): use data instead of query (#4370) 2020-08-11 23:01:02 +02:00
Erwann Hilley
1fbaf8816e
feat: add Blob support for browser (#4338)
Co-authored-by: Papaia <43409674+Papaia@users.noreply.github.com>
2020-08-11 23:00:12 +02:00
Souji
b4d651055a
fix(BaseManager): properly type valueOf (#4594) 2020-08-11 22:59:44 +02:00
Sugden
c5b6c4da43
fix: correctly import VoiceState (#4616) 2020-08-11 22:59:03 +02:00
Jan
f628981f42
docs(CategoryChannel): Fix children being incorrectly marked as nullable (#4620) 2020-08-11 22:58:30 +02:00
Souji
a663ea4d2c
fix(ApiMessage): respect allowedMentions with split (#4588) 2020-08-11 22:57:12 +02:00
Zaid - Nico
2be68e4125
fix(Message): Message#createdTimestamp uses deconstructed message id to get timestamp (#4632) 2020-08-11 21:02:15 +02:00
Souji
317f24076e
fix(Util): support empty array for flatten (#4590) 2020-08-11 21:01:29 +02:00
Souji
fab3153de6
fix: consider #nsfw false if not present in data (#4593) 2020-08-11 21:00:29 +02:00
Souji
276dddcbfb
fix(PresenceStatus): include invisible in typings (#4585) 2020-08-11 20:59:47 +02:00
Souvik
2adb5815bf
fix: set #nickname to null as the default value (#4641) 2020-08-11 20:58:52 +02:00
tiehm
3df99930e8
fix(Typings): Channel#delete returns bad type (#4118) 2020-08-11 20:58:12 +02:00
Matthew Stead
e54c21bc65
feat(typings): TypeScript support for changing $browser (#4667) 2020-08-11 20:57:42 +02:00
Souji
bbfc715821
fix(Message): include MessageEmbed type (#4675) 2020-08-11 20:56:01 +02:00
Sanskar Jha
755f3798d1
docs(examples): fix example img (#4678) 2020-08-11 20:55:05 +02:00
Arthur
5b716c5b0c
docs(ReactionManager): clarify cache Collection keys type (#4683) 2020-08-11 20:54:14 +02:00
Jan
b0e53e9c6d
docs(Message): add NewsChannel type to Message#channel (#4680) 2020-08-11 20:53:29 +02:00
Androz
c55b5c8c19
fix(typings): correct spelling of APIError (#4687) 2020-08-11 20:52:10 +02:00
모메MoMe
fb1dd6b53a
fix(Util): Fix cleanContent mention exploit (#4663) 2020-07-29 12:47:20 +02:00
Jan
0e61fca974
docs: make use of MessageResolvable type for bulkDelete (#4661) 2020-07-29 12:15:23 +02:00
camc
2b6e6d8631
feat(Module): add ReactionManager to exports (#4372)
add ReactionManager to the manager exports in src/index.js

closes #4363
2020-07-17 10:20:51 +02:00
Tenpi
47151fc2a9
fix(typings): allow custom events (#4162) 2020-07-17 10:17:44 +02:00
Souji
5027787aec
chore: add relevant client options section to issue template (#4587) 2020-07-17 10:16:37 +02:00
Souji
c79ac4d9fc
feat(Message): support pin and unpin with reason (#4586)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-07-17 10:13:30 +02:00
Souji
f9f3661090
fix(User): type dmChannel as nullable (#4609) 2020-07-06 19:07:33 +02:00
Crawl
ae716872b9
chore(deps): remove peer-deps as per npm@7 (#4272) 2020-06-19 11:48:01 +02:00
Phineas
ea19faa411
Change domain to discord.com (#4160) 2020-06-19 11:46:59 +02:00
Papaia
9a1c56c5b9
docs(erlpack): discordapp to discord (#4288)
Co-authored-by: Papaia <43409674+ItsPapaia@users.noreply.github.com>
2020-06-19 11:46:29 +02:00
Jan
214981f0b1
feat(MessageMentions): fix typings/docs, add resolvables support (#4339) 2020-06-19 11:43:19 +02:00
Souji
16847a3c13
fix: typing start event emitting on non text based channels (#4349) 2020-06-19 11:42:06 +02:00
uhKevinMC
54a7fdadda
feat(voiceState): add self_video property (#4346) 2020-06-19 11:41:11 +02:00
Papaia
1c275afd7c
fix(Guild): fix vanityURLUses desc, internally use fetchVanityData (#4335)
* docs(vanityURLUses): use fetchVanityData

* feat(fetchVanityCode): internally call fetchVanityData

* Update src/structures/Guild.js
2020-06-04 19:17:18 +02:00
Johnson Chen
8030612e52
feat(Guild): add fetchVanityData (#4103)
* chore: deprecate Guild.fetchVanityCode()

* feat: add Guild.fetchVanityData()

* chore: update typings

* fix: remove redundant .then()

Co-Authored-By: Antonio Román <kyradiscord@gmail.com>

* chore: fix lint

* chore: util.deprecate fetchVanityCode

* feat: add VanityData typedef and populate vanityURLUses

* chore: update typings

* chore: properly deprecate fetchVanityCode

* chore: fix jsdoc description for fetchVanityData

* feat: make fetchVanityData an async function

* chore: update Vanity typedef

* docs: update jsdoc

* feat: throw vanity url error instead of returning rejected promise

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

* docs: disable max-len rule and add info about receiving parameter

* fix: throw Error instead of rejecting Promise

* revert: revert "fix: throw Error instead of rejecting Promise"

This reverts commit 7ffd53eba40ff7261a36372935c3017576518a56.

* fix: require DJSError to fix throwing VANITY_URL error

* nitpick: re-add TypeError to the import

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

Co-authored-by: Antonio Román <kyradiscord@gmail.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2020-06-04 13:41:20 +02:00
Carter
257371da28
feat(REST): allow options.query as URLSearchParams (#4143)
* feat: support query as URLSearchParams

* style: remove unnecessary comment

* patch: use const

Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>

* feat: not reconstructing the search params

Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-06-04 13:34:29 +02:00
nolan
5955498aca
fix(DataResolver): resolveInviteCode to support new domain (#4281)
Update resolveInviteCode method to support "discord.com/invite" links.
2020-06-04 13:22:26 +02:00
Papaia
fc4bddf82a
fix: grammatical errors in INVALID_TYPE errors (#4289)
* fix(GuildMemberRoleManager): grammatical error

* fix: eslint

* fix(GuildChannel): an

* fix(PermissionOverwrites): an

Co-authored-by: Papaia <43409674+ItsPapaia@users.noreply.github.com>
2020-06-04 13:02:28 +02:00
Souji
bd349650a7
docs(Guild): description of Guild#premiumSubscriptionCount (#4324) 2020-06-04 12:56:48 +02:00
Sugden
88a62d5fea
docs(GuildManager): resolve returns a GuildChannel (#4333) 2020-06-04 12:52:43 +02:00
Evan
b0b62d63cf
chore(docs): remove comment about type 13 (#4159) 2020-05-21 13:18:06 +02:00
Sugden
15b53509da
fix(APIMessage): only pass allowedMentions if content is defined (#4269) 2020-05-21 13:17:00 +02:00
Schuyler Cebulskie
153a030c1f
Improve text for Discord server link in issues 2020-05-17 02:51:28 -04:00
Papaia
2583ad5da7
docs(WebSocketShard): add missing properties (#4268) 2020-05-09 17:23:32 +02:00
SpaceEEC
a6510d6a61
revert "chore(docs): example for timeout in message.delete()" (#4167) 2020-05-07 23:39:45 +02:00
SpaceEEC
407bc77d34
fix: in/de-crement max listener for client events (#4168) 2020-05-07 23:39:23 +02:00
Alexander Kashev
766b91d306
docs(ShardingManager): fix typo in JSDoc (#4158)
Fix typo introduced in PR #4157 in the link to Node docs
2020-05-05 22:45:30 +02:00
anandre
b385aedf36
chore(docs): example for timeout in message.delete() (#4165) 2020-05-05 22:41:41 +02:00
Alexander Kashev
99612ba14d
docs(ShardingManager): remove experimental status of Worker threads (#4157) 2020-05-04 13:08:52 +02:00
Kevin
ec0227a476
feat(GuildMemberManager): nonce and chunk_count for _fetchMany (#4130)
Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com>
2020-05-04 12:48:33 +02:00
anandre
2617d3c9f3
docs(VoiceState): remove permissions required from description (#4156) 2020-05-04 12:46:58 +02:00
Carter
6367f603f6
typings: Add User#fetchFlags (#4138) 2020-05-01 14:45:14 +01:00
sillyfrog
d3c9384c9c
fix(Voice): correctly set speaking data in the voice ssrcMap
Co-authored-by: Sillyfrog <sillyfrog@users.noreply.github.com>
2020-04-30 17:21:29 +01:00
Carter
026691702d
feat(Guild#fetch): withCount param (#4111) 2020-04-27 09:05:39 +02:00
Corentin Poupry
605ee8587b
fix(MessageEmbed): explicitly mark proxyIconURL as undefined (#4097) 2020-04-26 17:02:45 +02:00
Alon L
819e04a7ab
fix(Typing): dmChannel bulkDelete (#4115)
Co-Authored-By: Sugden <28943913+NotSugden@users.noreply.github.com>
2020-04-26 15:59:30 +02:00
Sugden
46b9e25190
typings(User): mark locale and flags as optional (#4127) 2020-04-26 15:58:53 +02:00
Nathan Franke
1726651c71
chore(tooling): include mention of commit convention (#4124) 2020-04-24 12:02:04 +02:00
Alon L
e3303ac3a2
fix(Typing): setSpeaking public (#4109) 2020-04-24 08:44:37 +01:00
Crawl
67a74c33e1
fix(Webpack): add Buffer polyfill in browser (#4102) 2020-04-20 21:15:26 +02:00
Papaia
97cbbb176b
fix(Guild): name acronym (#4104) 2020-04-20 21:15:14 +02:00
RDambrosio
5af1a552bc
fix(PacketHandler): guild members chunk packet handler should… (#4092) 2020-04-19 12:25:32 +02:00
SpaceEEC
97d23de247
fix(Typings): add optional Set<Snowflake> to shardReady event (#4099) 2020-04-19 12:24:58 +02:00
Souji
863a70918a
docs(Message): add timeout to Message#delete example (#4090) 2020-04-18 12:16:19 +02:00
Jyguy
6fbaf0a036
fix(User): jsdoc for User#flags (#4094) 2020-04-18 12:15:30 +02:00
iCrawl
d827544fbd
chore(Release): version 2020-04-17 12:58:26 +02:00
thepheer
12187efdbd
feat(DataResolver): prefer streams over buffers (#4075)
* feat(DataResolver): prefer streams over buffers

* feat(DataResolver): add `resolveFileAsBuffer`

Add `resolveFileAsBuffer` to use it in `resolveImage` which still requires Buffers to work.

* fix(DataResolver): make sure `resolveFile` always returns a Promise

* refactor(DataResolver): use for-await-of

* fix(DataResolver): use forked form-data which supports custom streams

* fix(APIRequest): use forked form-data in code too

Co-authored-by: - <5144598+-@users.noreply.github.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-04-17 12:03:50 +02:00
Ryan Munro
7c6000c5e3
feat(ClientOptions): allow setting default allowedMentions (#4085)
* feat(ClientOptions): add default allowedMentions

* feat(ClientOptions): use default allowedMentions when not provided

* Update src/structures/APIMessage.js

Co-Authored-By: SpaceEEC <spaceeec@yahoo.com>

* Update src/structures/APIMessage.js

Co-Authored-By: SpaceEEC <spaceeec@yahoo.com>

* fix(ClientOptions): default allowedMentions should be undefined

Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-04-17 11:23:31 +02:00
HarmoGlace
a88b7239b5
docs(RoleManager): everyone role can't be null (#3995)
* docs(RoleManager) : fix jsdoc everyone role can't be null

It fixes the jsdoc of RoleManager ; the everyone role of a guild can't be null

* Everyone role can't be null

* fix(typings): mark RoleManager#everyone as non-null

Co-authored-by: Crawl <icrawltogo@gmail.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-04-16 12:18:43 +02:00
Duncan Sterken
0a3759f683
feat(ESModules): importing for esm modules (#3998)
* fix: importing for esm modules

* style: use single quotes

* refactor: remove 'use strict'
2020-04-16 12:11:24 +02:00
Sardonyx
da5d92812e
docs(Webhook): id and token information (#3962)
* Documented how to get ID and Token of a webhook

* Update docs/examples/webhook.js

Co-Authored-By: Sugden <28943913+NotSugden@users.noreply.github.com>

* Explained whats the response body

* Update docs/examples/webhook.js

Co-Authored-By: Crawl <icrawltogo@gmail.com>

* Update docs/examples/webhook.js

Co-Authored-By: Crawl <icrawltogo@gmail.com>

* Update webhook.js

* Capitilized ID

Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: Crawl <icrawltogo@gmail.com>
2020-04-16 12:09:26 +02:00
Kevin
ff3454ef89
feat(GuildMemberManager): customisable timeout for _fetchMany (#4081) 2020-04-16 12:07:32 +02:00
SpaceEEC
4625881c54
docs(MessageEmbed): document the constructor (#4077) 2020-04-16 12:07:08 +02:00
SpaceEEC
4c2308b4c6
docs(MessageManager): document return type of delete (#4012)
* docs(MessageManager): document return type of delete

* docs(MessageManager): use Promise<void> over Promise

Co-Authored-By: Papaia <43409674+ItsPapaia@users.noreply.github.com>

Co-authored-by: Papaia <43409674+ItsPapaia@users.noreply.github.com>
2020-04-16 11:57:03 +02:00
SpaceEEC
7ce58dbd4a
docs(ShardClientUtil): link Shard#message from send method (#4028)
* docs(ShardClientUtil): link Shard#message from send method

* docs(ShardClientUtil): use @ emits instead of @ link
2020-04-16 11:56:42 +02:00
Ron B
2388467bd3
chore(Typings): stricter def for Client#emit (#4087) 2020-04-16 11:52:52 +02:00
withmask
d7096569c8
fix(PermissionOverwrites): resolveOverwriteOptions description (#4088)
smoll update
2020-04-16 11:52:06 +02:00
Advaith
fcacf1bc0d
fix(Guild): sort text, news, and store channels together (#4070) 2020-04-16 10:35:19 +02:00
Carter
2e5a6476d5
feat: User#flags (#4060)
* feat: user flags

* fix: unnecessary negated statement

* fix: wording for description

* fix: an vs. a

* feat: add verified bot and dev flags

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

* typings :verified bot and dev flags

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

* feat: mon's suggestion, async fetchFlags & jsdoc

* feat: added to index.js

* fix: typo

* style: leveled flags

* typings: update leveled flags

Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2020-04-16 10:32:15 +02:00
cherryblossom000
72a33cb8c2
fix(Typings): GuildPreview#features and Integration#type (#4080)
* fix(Typings): make GuildPreview#features an array

* fix(Typings): make Integration#type a string
2020-04-16 10:31:40 +02:00
SpaceEEC
72a7f2b3ed
fix(ClientApplication): type fetchAssets as resolving with an a… (#4078) 2020-04-16 09:27:52 +02:00
SpaceEEC
a8db9884d5
feat(Message): add allowedMentions to MessageEditOptions (#4071) 2020-04-16 09:27:19 +02:00
Roki
1330e2d246
feat: add supported 4096 image size and jpeg format (#4031)
* add 4096 avatar size that discord supports

* jpeg is also a thing

* update jsdocs

* update typings and remove duplicate type
2020-04-12 22:20:31 +02:00
Jyguy
9ba4eff279
fix(StreamDispatcher): correct property types (#4059)
* typings(StreamDispatcher): correct property types

* typings(StreamDispatcher): order methods alphabetically
2020-04-12 20:58:53 +02:00
Vlad Frangu
e5fac8c32f
chore(WebSocketShard): log Discord requested reconnects (#4066) 2020-04-12 20:57:50 +02:00
Quentin
a07c3c2f94
fix(BaseManager): remove declaration of remove method (#4069)
The BaseManager#remove method doesn't exist, but was in the BaseManager typings.
2020-04-12 20:54:34 +02:00
SpaceEEC
828640ca26
ci(Testing): add TypeScript test job (#4002)
* ci(Testing): add TypeScript job

* chore: add eol before eof
2020-04-04 14:11:59 +02:00
SpaceEEC
9e4c39ae53
fix(Message): update MessageMention's roles on message edit (#4016) 2020-04-03 21:30:49 +02:00
Amish Shah
0e44ecd420 chore: fix typings/docs for VoiceBroadcast (#4014) 2020-04-03 11:19:24 +01:00
Advaith
849c6324d3
feat(Guild): PUBLIC_DISABLED and WELCOME_SCREEN_ENABLED features (#4009) 2020-04-03 11:59:51 +02:00
Syntle
691e96c5cf
fix(Presence): add missing userID property to declarations (#4013)
* Added `userID` property to `Presence` class

userID property exists in docs but not in typings

* fix(Presence): userID should be typed as Snowflake

Co-Authored-By: BorgerKing <38166539+RDambrosio016@users.noreply.github.com>

Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
Co-authored-by: BorgerKing <38166539+RDambrosio016@users.noreply.github.com>
2020-04-03 11:57:50 +02:00
iCrawl
6544d22338
chore(Release): version 2020-03-27 22:25:33 +01:00
SpaceEEC
5e491260a1
fix(Typings): use Channel instead of *ChannelTypes in ClientEve… (#4001) 2020-03-27 22:23:46 +01:00
137 changed files with 15295 additions and 1447 deletions

View file

@ -1,11 +1,12 @@
{
"root": true,
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"plugins": ["import"],
"parserOptions": {
"ecmaVersion": 2019
"ecmaVersion": 2020
},
"env": {
"es6": true,
"es2020": true,
"node": true
},
"overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }],
@ -26,7 +27,8 @@
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"endOfLine": "lf"
"endOfLine": "lf",
"arrowParens": "avoid"
}
],
"strict": ["error", "global"],

View file

@ -11,8 +11,8 @@ is a great boon to your development process.
To get ready to work on the codebase, please do the following:
1. Fork & clone the repository, and make sure you're on the **master** branch
2. Run `npm install`
2. Run `npm ci`
3. If you're working on voice, also run `npm install @discordjs/opus` or `npm install opusscript`
4. Code your heart out!
5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare)
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare) (Make sure you follow the [conventional commit format](https://github.com/discordjs/discord.js-next/blob/master/.github/COMMIT_CONVENTION.md))

View file

@ -28,6 +28,12 @@ You won't receive any basic help here.
- Operating system:
- Priority this issue should have please be realistic and elaborate if possible:
**Relevant client options:**
- partials: none
- gateway intents: none
- other: none
<!--
If this applies to you, please check the respective checkbox: [ ] becomes [x].
You don't have to modify the text to suit your particular situation if you want to

View file

@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: discord.js discord server
- name: Discord server
url: https://discord.gg/bRCvFy9
about: Please use this Discord Server to ask questions and get support. We don't typically answer questions here and they will likely be closed and redirected to the Discord server.
about: Have questions or need support? Please go to the Discord server, as issues that are just support-related will be closed and redirected there.

18
.github/tsc.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
"problemMatcher": [
{
"owner": "tsc",
"pattern": [
{
"regexp": "^(?:\\s+\\d+\\>)?([^\\s].*)\\((\\d+),(\\d+)\\)\\s*:\\s+(error|warning|info)\\s+(\\w{1,2}\\d+)\\s*:\\s*(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}

27
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: "CodeQL"
on:
push:
pull_request:
schedule:
- cron: '0 */12 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 2
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View file

@ -15,13 +15,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@master
- name: Install Node v12
- name: Install Node v14
uses: actions/setup-node@master
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm install
run: npm ci
- name: Build and deploy documentation
uses: discordjs/action-docs@v1
@ -35,13 +35,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@master
- name: Install Node v12
- name: Install Node v14
uses: actions/setup-node@master
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm install
run: npm ci
- name: Build and deploy webpack
uses: discordjs/action-webpack@v1

View file

@ -10,13 +10,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v12
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm install
run: npm ci
- name: Run ESLint
uses: icrawl/action-eslint@v1
@ -28,17 +28,38 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v12
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm install
run: npm ci
- name: Run TSLint
run: npm run lint:typings
typescript:
name: TypeScript
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm ci
- name: Register Problem Matcher
run: echo "##[add-matcher].github/tsc.json"
- name: Run TypeScript compiler
run: npm run test:typescript
docs:
name: Documentation
runs-on: ubuntu-latest
@ -46,13 +67,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v1
- name: Install Node v12
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm install
run: npm ci
- name: Test documentation
run: npm run docs:test

View file

@ -8,13 +8,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v12
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm install
run: npm ci
- name: Run ESLint
uses: icrawl/action-eslint@v1
@ -26,17 +26,38 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v12
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm install
run: npm ci
- name: Run TSLint
run: npm run lint:typings
typescript:
name: TypeScript
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
run: npm ci
- name: Register Problem Matcher
run: echo "##[add-matcher].github/tsc.json"
- name: Run TypeScript compiler
run: npm run test:typescript
docs:
name: Documentation
runs-on: ubuntu-latest
@ -44,13 +65,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Node v12
- name: Install Node v14
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 14
- name: Install dependencies
run: npm install
run: npm ci
- name: Test documentation
run: npm run docs:test

3
.gitignore vendored
View file

@ -1,7 +1,6 @@
# Packages
node_modules/
yarn.lock
package-lock.json
# Log files
logs/
@ -18,5 +17,7 @@ deploy/deploy_key.pub
# Miscellaneous
.tmp/
.vscode/
.idea/
docs/docs.json
typings/index.js
webpack/

1
.npmrc
View file

@ -1 +0,0 @@
package-lock=false

View file

@ -5,7 +5,7 @@
</p>
<br />
<p>
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://discord.gg/bRCvFy9"><img src="https://img.shields.io/discord/222078108977594368?color=7289da&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="Build status" /></a>
@ -19,6 +19,7 @@
## Table of contents
- [Changes](#changes)
- [About](#about)
- [Installation](#installation)
- [Audio engines](#audio-engines)
@ -29,10 +30,14 @@
- [Contributing](#contributing)
- [Help](#help)
## Changes
This fork has the inline replies and slash commands for **testing** purposes.
## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the
[Discord API](https://discordapp.com/developers/docs/intro).
[Discord API](https://discord.com/developers/docs/intro).
- Object-oriented
- Predictable abstractions
@ -41,7 +46,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation
**Node.js 12.0.0 or newer is required.**
**Node.js 14.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js`
@ -57,7 +62,7 @@ For production bots, using @discordjs/opus should be considered a necessity, esp
### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`)
- [erlpack](https://github.com/discord/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discord/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
@ -76,7 +81,7 @@ client.on('ready', () => {
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
msg.channel.send('pong');
}
});

View file

@ -33,7 +33,7 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');
```
@ -68,7 +68,7 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');
```
@ -105,13 +105,13 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');
```
The results are the same as the URL examples:
![Image showing result](/static/attachment-example1.png)
![Image showing result](/static/attachment-example2.png)
But what if you have a buffer from an image? Or a text document? Well, it's the same as sending a local file or a URL!
@ -154,7 +154,7 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');
```

View file

@ -23,9 +23,9 @@ client.on('message', message => {
// If the message is "what is my avatar"
if (message.content === 'what is my avatar') {
// Send the user's avatar URL
message.reply(message.author.displayAvatarURL());
message.channel.send(message.author.displayAvatarURL());
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');

View file

@ -36,5 +36,5 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');

View file

@ -28,5 +28,5 @@ client.on('guildMemberAdd', member => {
channel.send(`Welcome to the server, ${member}`);
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');

View file

@ -33,7 +33,7 @@ client.on('message', message => {
// If we have a user mentioned
if (user) {
// Now we get the member from the user
const member = message.guild.member(user);
const member = message.guild.members.resolve(user);
// If the member is in the guild
if (member) {
/**
@ -45,28 +45,28 @@ client.on('message', message => {
.kick('Optional reason that will display in the audit logs')
.then(() => {
// We let the message author know we were able to kick the person
message.reply(`Successfully kicked ${user.tag}`);
message.channel.send(`Successfully kicked ${user.tag}`);
})
.catch(err => {
// An error happened
// This is generally due to the bot not being able to kick the member,
// either due to missing permissions or role hierarchy
message.reply('I was unable to kick the member');
message.channel.send('I was unable to kick the member');
// Log the error
console.error(err);
});
} else {
// The mentioned user isn't in this guild
message.reply("That user isn't in this guild!");
message.channel.send("That user isn't in this guild!");
}
// Otherwise, if no user was mentioned
} else {
message.reply("You didn't mention the user to kick!");
message.channel.send("You didn't mention the user to kick!");
}
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');
```
@ -105,7 +105,7 @@ client.on('message', message => {
// If we have a user mentioned
if (user) {
// Now we get the member from the user
const member = message.guild.member(user);
const member = message.guild.members.resolve(user);
// If the member is in the guild
if (member) {
/**
@ -121,28 +121,28 @@ client.on('message', message => {
})
.then(() => {
// We let the message author know we were able to ban the person
message.reply(`Successfully banned ${user.tag}`);
message.channel.send(`Successfully banned ${user.tag}`);
})
.catch(err => {
// An error happened
// This is generally due to the bot not being able to ban the member,
// either due to missing permissions or role hierarchy
message.reply('I was unable to ban the member');
message.channel.send('I was unable to ban the member');
// Log the error
console.error(err);
});
} else {
// The mentioned user isn't in this guild
message.reply("That user isn't in this guild!");
message.channel.send("That user isn't in this guild!");
}
} else {
// Otherwise, if no user was mentioned
message.reply("You didn't mention the user to ban!");
message.channel.send("You didn't mention the user to ban!");
}
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');
```

View file

@ -27,5 +27,5 @@ client.on('message', message => {
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
// Log our bot in using the token from https://discord.com/developers/applications
client.login('your token here');

View file

@ -6,8 +6,13 @@
// Import the discord.js module
const Discord = require('discord.js');
// Create a new webhook
/*
* Create a new webhook
* The Webhooks ID and token can be found in the URL, when you request that URL, or in the response body.
* https://discord.com/api/webhooks/12345678910/T0kEn0fw3Bh00K
* ^^^^^^^^^^ ^^^^^^^^^^^^
* Webhook ID Webhook Token
*/
const hook = new Discord.WebhookClient('webhook id', 'webhook token');
// Send a message using the webhook

View file

@ -4,7 +4,7 @@ These questions are some of the most frequently asked.
## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`
Update to Node.js 12.0.0 or newer.
Update to Node.js 14.0.0 or newer.
## How do I get voice working?

View file

@ -5,7 +5,7 @@
</p>
<br />
<p>
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://discord.gg/bRCvFy9"><img src="https://img.shields.io/discord/222078108977594368?color=7289da&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://travis-ci.org/discordjs/discord.js"><img src="https://travis-ci.org/discordjs/discord.js.svg" alt="Build status" /></a>
@ -24,7 +24,7 @@ Welcome to the discord.js v12 documentation.
## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the
[Discord API](https://discordapp.com/developers/docs/intro).
[Discord API](https://discord.com/developers/docs/intro).
- Object-oriented
- Predictable abstractions
@ -33,7 +33,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation
**Node.js 12.0.0 or newer is required.**
**Node.js 14.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js`
@ -49,7 +49,7 @@ For production bots, using @discordjs/opus should be considered a necessity, esp
### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`)
- [erlpack](https://github.com/discord/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discord/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
@ -68,7 +68,7 @@ client.on('ready', () => {
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
msg.channel.send('pong');
}
});

View file

@ -37,7 +37,7 @@ client.on('message', async message => {
if (message.member.voice.channel) {
const connection = await message.member.voice.channel.join();
} else {
message.reply('You need to join a voice channel first!');
message.channel.send('You need to join a voice channel first!');
}
}
});

96
esm/discord.mjs Normal file
View file

@ -0,0 +1,96 @@
import Discord from '../src/index.js';
export default Discord;
export const {
BaseClient,
Client,
Shard,
ShardClientUtil,
ShardingManager,
WebhookClient,
ActivityFlags,
BitField,
Collection,
Constants,
DataResolver,
BaseManager,
DiscordAPIError,
HTTPError,
MessageFlags,
Intents,
Permissions,
Speaking,
Snowflake,
SnowflakeUtil,
Structures,
SystemChannelFlags,
UserFlags,
Util,
version,
BaseGuildEmojiManager,
ChannelManager,
GuildChannelManager,
GuildEmojiManager,
GuildEmojiRoleManager,
GuildMemberManager,
GuildMemberRoleManager,
GuildManager,
ReactionManager,
ReactionUserManager,
MessageManager,
PresenceManager,
RoleManager,
UserManager,
discordSort,
escapeMarkdown,
fetchRecommendedShards,
resolveColor,
resolveString,
splitMessage,
Application,
Base,
Activity,
APIMessage,
BaseGuildEmoji,
CategoryChannel,
Channel,
ClientApplication,
ClientUser,
Collector,
DMChannel,
Emoji,
Guild,
GuildAuditLogs,
GuildChannel,
GuildEmoji,
GuildMember,
GuildPreview,
GuildTemplate,
Integration,
Invite,
Message,
MessageAttachment,
MessageCollector,
MessageEmbed,
MessageMentions,
MessageReaction,
NewsChannel,
PermissionOverwrites,
Presence,
ClientPresence,
ReactionCollector,
ReactionEmoji,
RichPresenceAssets,
Role,
StoreChannel,
Team,
TeamMember,
TextChannel,
User,
VoiceChannel,
VoiceRegion,
VoiceState,
Webhook,
WebSocket
} = Discord;

11518
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,28 @@
{
"name": "discord.js",
"version": "12.1.0",
"version": "12.5.0",
"description": "A powerful library for interacting with the Discord API",
"main": "./src/index",
"types": "./typings/index.d.ts",
"exports": {
".": [
{
"require": "./src/index.js",
"import": "./esm/discord.mjs"
},
"./src/index.js"
],
"./esm": "./esm/discord.mjs"
},
"scripts": {
"test": "npm run lint && npm run docs:test && npm run lint:typings",
"test:typescript": "tsc",
"docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --source src --custom docs/index.yml",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint:typings": "tslint typings/index.d.ts",
"prettier": "prettier --write --single-quote --print-width 120 --trailing-comma all --end-of-line lf src/**/*.js typings/**/*.ts",
"prettier": "prettier --write src/**/*.js typings/**/*.ts",
"build:browser": "webpack",
"prepublishOnly": "npm run test && cross-env NODE_ENV=production npm run build:browser"
},
@ -36,68 +47,40 @@
"runkitExampleFilename": "./docs/examples/ping.js",
"unpkg": "./webpack/discord.min.js",
"dependencies": {
"@discordjs/collection": "^0.1.5",
"@discordjs/collection": "^0.1.6",
"@discordjs/form-data": "^3.0.1",
"abort-controller": "^3.0.0",
"form-data": "^3.0.0",
"node-fetch": "^2.6.0",
"prism-media": "^1.2.0",
"node-fetch": "^2.6.1",
"prism-media": "^1.2.2",
"setimmediate": "^1.0.5",
"tweetnacl": "^1.0.3",
"ws": "^7.2.1"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"erlpack": "discordapp/erlpack",
"libsodium-wrappers": "^0.7.6",
"sodium": "^3.0.2",
"utf-8-validate": "^5.0.2",
"zlib-sync": "^0.1.6"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"erlpack": {
"optional": true
},
"libsodium-wrappers": {
"optional": true
},
"sodium": {
"optional": true
},
"utf-8-validate": {
"optional": true
},
"zlib-sync": {
"optional": true
}
"ws": "^7.3.1"
},
"devDependencies": {
"@commitlint/cli": "^8.3.5",
"@commitlint/config-angular": "^8.3.4",
"@types/node": "^10.12.24",
"@types/ws": "^7.2.1",
"@commitlint/cli": "^11.0.0",
"@commitlint/config-angular": "^11.0.0",
"@types/node": "^12.12.6",
"@types/ws": "^7.2.7",
"cross-env": "^7.0.2",
"discord.js-docgen": "discordjs/docgen",
"dtslint": "^3.0.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-prettier": "^3.1.2",
"husky": "^4.2.3",
"jest": "^25.1.0",
"discord.js-docgen": "git+https://github.com/discordjs/docgen.git",
"dtslint": "^4.0.4",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.13.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.3.0",
"jest": "^26.6.0",
"json-filter-loader": "^1.0.0",
"lint-staged": "^10.0.8",
"prettier": "^1.19.1",
"terser-webpack-plugin": "^1.2.2",
"tslint": "^6.0.0",
"typescript": "^3.8.2",
"webpack": "^4.41.6",
"webpack-cli": "^3.3.11"
"lint-staged": "^10.4.2",
"prettier": "^2.1.2",
"terser-webpack-plugin": "^4.2.3",
"tslint": "^6.1.3",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
},
"browser": {
"@discordjs/opus": false,
@ -127,9 +110,9 @@
"src/client/voice/receiver/PacketHandler.js": false,
"src/client/voice/receiver/Receiver.js": false,
"src/client/voice/util/PlayInterface.js": false,
"src/client/voice/util/Secretbox.js": false,
"src/client/voice/util/Silence.js": false,
"src/client/voice/util/VolumeInterface.js": false
"src/client/voice/util/VolumeInterface.js": false,
"src/util/Sodium.js": false
},
"husky": {
"hooks": {
@ -139,7 +122,7 @@
},
"lint-staged": {
"*.js": "eslint --fix",
"*.ts": "prettier --write --single-quote --print-width 120 --trailing-comma all --end-of-line lf"
"*.ts": "prettier --write"
},
"commitlint": {
"extends": [
@ -169,5 +152,12 @@
]
]
}
},
"prettier": {
"singleQuote": true,
"printWidth": 120,
"trailingComma": "all",
"endOfLine": "lf",
"arrowParens": "avoid"
}
}

View file

@ -139,6 +139,28 @@ class BaseClient extends EventEmitter {
this._immediates.delete(immediate);
}
/**
* Increments max listeners by one, if they are not zero.
* @private
*/
incrementMaxListeners() {
const maxListeners = this.getMaxListeners();
if (maxListeners !== 0) {
this.setMaxListeners(maxListeners + 1);
}
}
/**
* Decrements max listeners by one, if they are not zero.
* @private
*/
decrementMaxListeners() {
const maxListeners = this.getMaxListeners();
if (maxListeners !== 0) {
this.setMaxListeners(maxListeners - 1);
}
}
toJSON(...props) {
return Util.flatten(this, { domain: false }, ...props);
}

View file

@ -1,17 +1,19 @@
'use strict';
const BaseClient = require('./BaseClient');
const InteractionClient = require('./InteractionClient');
const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
const { Error, TypeError, RangeError } = require('../errors');
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
const ChannelManager = require('../managers/ChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildManager = require('../managers/GuildManager');
const UserManager = require('../managers/UserManager');
const ShardClientUtil = require('../sharding/ShardClientUtil');
const ClientApplication = require('../structures/ClientApplication');
const GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite');
const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook');
@ -102,6 +104,12 @@ class Client extends BaseClient {
? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
: null;
/**
* The interaction client.
* @type {InteractionClient}
*/
this.interactionClient = new InteractionClient(options, this);
/**
* All of the {@link User} objects that have been cached at any point, mapped by their IDs
* @type {UserManager}
@ -135,7 +143,8 @@ class Client extends BaseClient {
Object.defineProperty(this, 'token', { writable: true });
if (!browser && !this.token && 'DISCORD_TOKEN' in process.env) {
/**
* Authorization token for the logged in bot
* Authorization token for the logged in bot.
* If present, this defaults to `process.env.DISCORD_TOKEN` when instantiating the client
* <warn>This should be kept private at all times.</warn>
* @type {?string}
*/
@ -164,11 +173,11 @@ class Client extends BaseClient {
/**
* All custom emojis that the client has access to, mapped by their IDs
* @type {GuildEmojiManager}
* @type {BaseGuildEmojiManager}
* @readonly
*/
get emojis() {
const emojis = new GuildEmojiManager({ client: this });
const emojis = new BaseGuildEmojiManager(this);
for (const guild of this.guilds.cache.values()) {
if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji);
}
@ -195,7 +204,7 @@ class Client extends BaseClient {
/**
* Logs the client in, establishing a websocket connection to Discord.
* @param {string} token Token of the account to log in with
* @param {string} [token=this.token] Token of the account to log in with
* @returns {Promise<string>} Token of the account used
* @example
* client.login('my token');
@ -253,6 +262,23 @@ class Client extends BaseClient {
.then(data => new Invite(this, data));
}
/**
* Obtains a template from Discord.
* @param {GuildTemplateResolvable} template Template code or URL
* @returns {Promise<GuildTemplate>}
* @example
* client.fetchGuildTemplate('https://discord.new/FKvmczH2HyUf')
* .then(template => console.log(`Obtained template with code: ${template.code}`))
* .catch(console.error);
*/
fetchGuildTemplate(template) {
const code = DataResolver.resolveGuildTemplateCode(template);
return this.api.guilds
.templates(code)
.get()
.then(data => new GuildTemplate(this, data));
}
/**
* Obtains a webhook from Discord.
* @param {Snowflake} id ID of the webhook
@ -340,7 +366,7 @@ class Client extends BaseClient {
}
/**
* Obtains a guild preview from Discord, only available for public guilds.
* Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds.
* @param {GuildResolvable} guild The guild to fetch the preview for
* @returns {Promise<GuildPreview>}
*/
@ -355,28 +381,43 @@ class Client extends BaseClient {
/**
* Generates a link that can be used to invite the bot to a guild.
* @param {PermissionResolvable} [permissions] Permissions to request
* @param {InviteGenerationOptions|PermissionResolvable} [options] Permissions to request
* @returns {Promise<string>}
* @example
* client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'])
* client.generateInvite({
* permissions: ['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'],
* })
* .then(link => console.log(`Generated bot invite link: ${link}`))
* .catch(console.error);
*/
async generateInvite(permissions) {
permissions = Permissions.resolve(permissions);
async generateInvite(options = {}) {
if (Array.isArray(options) || ['string', 'number'].includes(typeof options) || options instanceof Permissions) {
process.emitWarning(
'Client#generateInvite: Generate invite with an options object instead of a PermissionResolvable',
'DeprecationWarning',
);
options = { permissions: options };
}
const application = await this.fetchApplication();
const query = new URLSearchParams({
client_id: application.id,
permissions: permissions,
permissions: Permissions.resolve(options.permissions),
scope: 'bot',
});
if (typeof options.disableGuildSelect === 'boolean') {
query.set('disable_guild_select', options.disableGuildSelect.toString());
}
if (typeof options.guild !== 'undefined') {
const guildID = this.guilds.resolveID(options.guild);
if (!guildID) throw new TypeError('INVALID_TYPE', 'options.guild', 'GuildResolvable');
query.set('guild_id', guildID);
}
return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`;
}
toJSON() {
return super.toJSON({
readyAt: false,
presences: false,
});
}
@ -416,6 +457,13 @@ class Client extends BaseClient {
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number');
}
if (
typeof options.messageEditHistoryMaxSize !== 'number' ||
isNaN(options.messageEditHistoryMaxSize) ||
options.messageEditHistoryMaxSize < -1
) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageEditHistoryMaxSize', 'a number greater than or equal to -1');
}
if (typeof options.fetchAllMembers !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean');
}
@ -442,6 +490,14 @@ class Client extends BaseClient {
module.exports = Client;
/**
* Options for {@link Client#generateInvite}.
* @typedef {Object} InviteGenerationOptions
* @property {PermissionResolvable} [permissions] Permissions to request
* @property {GuildResolvable} [guild] Guild to preselect
* @property {boolean} [disableGuildSelect] Whether to disable the guild selection
*/
/**
* Emitted for general warnings.
* @event Client#warn

View file

@ -0,0 +1,207 @@
'use strict';
const BaseClient = require('./BaseClient');
const ApplicationCommand = require('../structures/ApplicationCommand');
const Interaction = require('../structures/Interaction');
const { Events, ApplicationCommandOptionType, InteractionType, InteractionResponseType } = require('../util/Constants');
let sodium;
/**
* Interaction client is used for interactions.
*
* @example
* const client = new InteractionClient({
* token: ABC,
* publicKey: XYZ,
* });
*
* client.on('interactionCreate', () => {
* // automatically handles long responses
* if (will take a long time) {
* doSomethingLong.then((d) => {
* interaction.reply({
* content: 'wow that took long',
* });
* });
* } else {
* interaction.reply('hi!');
* }
* });
* ```
*/
class InteractionClient extends BaseClient {
/**
* @param {Options} options Options for the client.
* @param {undefined} client For internal use.
*/
constructor(options, client) {
super(options);
Object.defineProperty(this, 'token', {
value: options.token,
writable: true,
});
Object.defineProperty(this, 'clientID', {
value: options.clientID,
writable: true,
});
Object.defineProperty(this, 'publicKey', {
value: options.publicKey ? Buffer.from(options.publicKey, 'hex') : undefined,
writable: true,
});
// Compat for direct usage
this.client = client || this;
this.interactionClient = this;
}
/**
* Get registered slash commands.
* @param {Snowflake} [guildID] Optional guild ID.
* @returns {Command[]}
*/
async getCommands(guildID) {
let path = this.client.api.applications('@me');
if (guildID) {
path = path.guilds(guildID);
}
const commands = await path.commands.get();
return commands.map(c => new ApplicationCommand(this, c, guildID));
}
/**
* Create a command.
* @param {Object} command The command description.
* @param {Snowflake?} guildID Optional guild ID.
* @returns {Promise<ApplicationCommand>} The created command.
*/
async createCommand(command, guildID) {
let path = this.client.api.applications(this.client.user.id);
if (guildID) {
path = path.guilds(guildID);
}
const c = await path.commands.post({
data: {
name: command.name,
description: command.description,
options: command.options.map(function m(o) {
return {
type: ApplicationCommandOptionType[o.type],
name: o.name,
description: o.description,
default: o.default,
required: o.required,
choices: o.choices,
options: o.options ? o.options.map(m) : undefined,
};
}),
},
});
return new ApplicationCommand(this, c, guildID);
}
handle(data) {
switch (data.type) {
case InteractionType.PING:
return {
type: InteractionResponseType.PONG,
};
case InteractionType.APPLICATION_COMMAND: {
let timedOut = false;
let resolve;
const directPromise = new Promise(r => {
resolve = r;
this.client.setTimeout(() => {
timedOut = true;
r({
type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE,
});
}, 250);
});
const syncHandle = {
acknowledge() {
if (!timedOut) {
resolve({
type: InteractionResponseType.ACKNOWLEDGE_WITH_SOURCE,
});
}
},
reply(resolved) {
if (timedOut) {
return false;
}
resolve({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: resolved.data,
});
return true;
},
};
const interaction = new Interaction(this.client, data, syncHandle);
/**
* Emitted when an interaction is created.
* @event Client#interactionCreate
* @param {Interaction} interaction The interaction which was created.
*/
this.client.emit(Events.INTERACTION_CREATE, interaction);
return directPromise;
}
default:
throw new RangeError('Invalid interaction data');
}
}
/**
* An express-like middleware factory which can be used
* with webhook interactions.
* @returns {Function} The middleware function.
*/
middleware() {
return async (req, res) => {
const timestamp = req.get('x-signature-timestamp');
const signature = req.get('x-signature-ed25519');
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const body = Buffer.concat(chunks);
if (sodium === undefined) {
sodium = require('../util/Sodium');
}
if (
!sodium.methods.verify(
Buffer.from(signature, 'hex'),
Buffer.concat([Buffer.from(timestamp), body]),
this.publicKey,
)
) {
res.status(403).end();
return;
}
const data = JSON.parse(body.toString());
const result = await this.handle(data);
res.status(200).end(JSON.stringify(result));
};
}
async handleFromGateway(data) {
const result = await this.handle(data);
await this.client.api.interactions(data.id, data.token).callback.post({
data: result,
});
}
}
module.exports = InteractionClient;

View file

@ -81,23 +81,25 @@ class GenericAction {
}
getMember(data, guild) {
const id = data.user.id;
return this.getPayload(
{
user: {
id,
},
},
guild.members,
id,
PartialTypes.GUILD_MEMBER,
);
return this.getPayload(data, guild.members, data.user.id, PartialTypes.GUILD_MEMBER);
}
getUser(data) {
const id = data.user_id;
return data.user || this.getPayload({ id }, this.client.users, id, PartialTypes.USER);
}
getUserFromMember(data) {
if (data.guild_id && data.member && data.member.user) {
const guild = this.client.guilds.cache.get(data.guild_id);
if (guild) {
return guild.members.add(data.member).user;
} else {
return this.client.users.add(data.member.user);
}
}
return this.getUser(data);
}
}
module.exports = GenericAction;

View file

@ -20,6 +20,7 @@ class ActionsManager {
this.register(require('./InviteCreate'));
this.register(require('./InviteDelete'));
this.register(require('./GuildMemberRemove'));
this.register(require('./GuildMemberUpdate'));
this.register(require('./GuildBanRemove'));
this.register(require('./GuildRoleCreate'));
this.register(require('./GuildRoleDelete'));
@ -35,6 +36,8 @@ class ActionsManager {
this.register(require('./GuildChannelsPositionUpdate'));
this.register(require('./GuildIntegrationsUpdate'));
this.register(require('./WebhooksUpdate'));
this.register(require('./TypingStart'));
this.register(require('./InteractionCreate'));
}
register(Action) {

View file

@ -5,13 +5,14 @@ const { Events } = require('../../util/Constants');
class GuildEmojiCreateAction extends Action {
handle(guild, createdEmoji) {
const already = guild.emojis.cache.has(createdEmoji.id);
const emoji = guild.emojis.add(createdEmoji);
/**
* Emitted whenever a custom emoji is created in a guild.
* @event Client#emojiCreate
* @param {GuildEmoji} emoji The emoji that was created
*/
this.client.emit(Events.GUILD_EMOJI_CREATE, emoji);
if (!already) this.client.emit(Events.GUILD_EMOJI_CREATE, emoji);
return { emoji };
}
}

View file

@ -9,7 +9,7 @@ class GuildMemberRemoveAction extends Action {
const guild = client.guilds.cache.get(data.guild_id);
let member = null;
if (guild) {
member = this.getMember(data, guild);
member = this.getMember({ user: data.user }, guild);
guild.memberCount--;
if (member) {
member.deleted = true;

View file

@ -0,0 +1,44 @@
'use strict';
const Action = require('./Action');
const { Status, Events } = require('../../util/Constants');
class GuildMemberUpdateAction extends Action {
handle(data, shard) {
const { client } = this;
if (data.user.username) {
const user = client.users.cache.get(data.user.id);
if (!user) {
client.users.add(data.user);
} else if (!user.equals(data.user)) {
client.actions.UserUpdate.handle(data.user);
}
}
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const member = this.getMember({ user: data.user }, guild);
if (member) {
const old = member._update(data);
/**
* Emitted whenever a guild member changes - i.e. new role, removed role, nickname.
* Also emitted when the user's details (e.g. username) change.
* @event Client#guildMemberUpdate
* @param {GuildMember} oldMember The member before the update
* @param {GuildMember} newMember The member after the update
*/
if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_UPDATE, old, member);
} else {
const newMember = guild.members.add(data);
/**
* Emitted whenever a member becomes available in a large guild.
* @event Client#guildMemberAvailable
* @param {GuildMember} member The member that became available
*/
this.client.emit(Events.GUILD_MEMBER_AVAILABLE, newMember);
}
}
}
}
module.exports = GuildMemberUpdateAction;

View file

@ -0,0 +1,15 @@
'use strict';
const Action = require('./Action');
class InteractionCreateAction extends Action {
handle(data) {
this.client.interactionClient.handleFromGateway(data).catch(e => {
this.client.emit('error', e);
});
return {};
}
}
module.exports = InteractionCreateAction;

View file

@ -9,7 +9,7 @@ class InviteCreateAction extends Action {
const client = this.client;
const channel = client.channels.cache.get(data.channel_id);
const guild = client.guilds.cache.get(data.guild_id);
if (!channel && !guild) return false;
if (!channel) return false;
const inviteData = Object.assign(data, { channel, guild });
const invite = new Invite(client, inviteData);

View file

@ -8,14 +8,17 @@ const { PartialTypes } = require('../../util/Constants');
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id' } }
channel_id: 'id',
// If originating from a guild
guild_id: 'id',
member: { ..., user: { ... } } }
*/
class MessageReactionAdd extends Action {
handle(data) {
if (!data.emoji) return false;
const user = this.getUser(data);
const user = this.getUserFromMember(data);
if (!user) return false;
// Verify channel
@ -28,6 +31,8 @@ class MessageReactionAdd extends Action {
// Verify reaction
if (message.partial && !this.client.options.partials.includes(PartialTypes.REACTION)) return false;
const existing = message.reactions.cache.get(data.emoji.id || data.emoji.name);
if (existing && existing.users.cache.has(user.id)) return { message, reaction: existing, user };
const reaction = message.reactions.add({
emoji: data.emoji,
count: message.partial ? null : 0,

View file

@ -7,7 +7,8 @@ const { Events } = require('../../util/Constants');
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', id: null },
channel_id: 'id' } }
channel_id: 'id',
guild_id: 'id' }
*/
class MessageReactionRemove extends Action {

View file

@ -9,9 +9,9 @@ class MessageUpdateAction extends Action {
const { id, channel_id, guild_id, author, timestamp, type } = data;
const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel);
if (message) {
message.patch(data);
const old = message.patch(data);
return {
old: message._edits[0],
old,
updated: message,
};
}

View file

@ -0,0 +1,58 @@
'use strict';
const Action = require('./Action');
const { Events } = require('../../util/Constants');
const textBasedChannelTypes = ['dm', 'text', 'news'];
class TypingStart extends Action {
handle(data) {
const channel = this.getChannel(data);
if (!channel) {
return;
}
if (!textBasedChannelTypes.includes(channel.type)) {
this.client.emit(Events.WARN, `Discord sent a typing packet to a ${channel.type} channel ${channel.id}`);
return;
}
const user = this.getUserFromMember(data);
const timestamp = new Date(data.timestamp * 1000);
if (channel && user) {
if (channel._typing.has(user.id)) {
const typing = channel._typing.get(user.id);
typing.lastTimestamp = timestamp;
typing.elapsedTime = Date.now() - typing.since;
this.client.clearTimeout(typing.timeout);
typing.timeout = this.tooLate(channel, user);
} else {
const since = new Date();
const lastTimestamp = new Date();
channel._typing.set(user.id, {
user,
since,
lastTimestamp,
elapsedTime: Date.now() - since,
timeout: this.tooLate(channel, user),
});
/**
* Emitted whenever a user starts typing in a channel.
* @event Client#typingStart
* @param {Channel} channel The channel the user started typing in
* @param {User} user The user that started typing
*/
this.client.emit(Events.TYPING_START, channel, user);
}
}
}
tooLate(channel, user) {
return channel.client.setTimeout(() => {
channel._typing.delete(user.id);
}, 10000);
}
}
module.exports = TypingStart;

View file

@ -13,6 +13,7 @@ class UserUpdateAction extends Action {
if (!oldUser.equals(newUser)) {
/**
* Emitted whenever a user's details (e.g. username) are changed.
* Triggered by the Discord gateway events USER_UPDATE, GUILD_MEMBER_UPDATE, and PRESENCE_UPDATE.
* @event Client#userUpdate
* @param {User} oldUser The user before the update
* @param {User} newUser The user after the update

View file

@ -1,14 +1,15 @@
'use strict';
const Action = require('./Action');
const VoiceState = require('../../structures/VoiceState');
const { Events } = require('../../util/Constants');
const Structures = require('../../util/Structures');
class VoiceStateUpdate extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const VoiceState = Structures.get('VoiceState');
// Update the state
const oldState = guild.voiceStates.cache.has(data.user_id)
? guild.voiceStates.cache.get(data.user_id)._clone()

View file

@ -18,6 +18,7 @@ const { Events } = require('../../util/Constants');
* }
* ```
* @implements {PlayInterface}
* @extends {EventEmitter}
*/
class VoiceBroadcast extends EventEmitter {
constructor(client) {

View file

@ -144,7 +144,6 @@ class VoiceConnection extends EventEmitter {
/**
* Sets whether the voice connection should display as "speaking", "soundshare" or "none".
* @param {BitFieldResolvable} value The new speaking state
* @private
*/
setSpeaking(value) {
if (this.speaking.equals(value)) return;
@ -166,7 +165,7 @@ class VoiceConnection extends EventEmitter {
/**
* The voice state of this connection
* @type {VoiceState}
* @type {?VoiceState}
*/
get voice() {
return this.channel.guild.voice;
@ -204,8 +203,8 @@ class VoiceConnection extends EventEmitter {
* Set the token and endpoint required to connect to the voice servers.
* @param {string} token The voice token
* @param {string} endpoint The voice endpoint
* @private
* @returns {void}
* @private
*/
setTokenAndEndpoint(token, endpoint) {
this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`);
@ -474,7 +473,11 @@ class VoiceConnection extends EventEmitter {
}
onStartSpeaking({ user_id, ssrc, speaking }) {
this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking });
this.ssrcMap.set(+ssrc, {
...(this.ssrcMap.get(+ssrc) || {}),
userID: user_id,
speaking: speaking,
});
}
/**
@ -502,7 +505,7 @@ class VoiceConnection extends EventEmitter {
}
if (guild && user && !speaking.equals(old)) {
const member = guild.member(user);
const member = guild.members.resolve(user);
if (member) {
/**
* Emitted once a guild member changes speaking state.

View file

@ -1,7 +1,7 @@
'use strict';
const { Writable } = require('stream');
const secretbox = require('../util/Secretbox');
const secretbox = require('../../../util/Sodium');
const Silence = require('../util/Silence');
const VolumeInterface = require('../util/VolumeInterface');
@ -56,7 +56,7 @@ class StreamDispatcher extends Writable {
* The broadcast controlling this dispatcher, if any
* @type {?VoiceBroadcast}
*/
this.broadcast = this.streams.broadcast;
this.broadcast = this.streams.broadcast || null;
this._pausedTime = 0;
this._silentPausedTime = 0;

View file

@ -189,7 +189,11 @@ class VoiceWebSocket extends EventEmitter {
this.emit('sessionDescription', packet.d);
break;
case VoiceOPCodes.CLIENT_CONNECT:
this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id);
this.connection.ssrcMap.set(+packet.d.audio_ssrc, {
userID: packet.d.user_id,
speaking: 0,
hasVideo: Boolean(packet.d.video_ssrc),
});
break;
case VoiceOPCodes.CLIENT_DISCONNECT:
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);

View file

@ -1,7 +1,9 @@
'use strict';
const EventEmitter = require('events');
const secretbox = require('../util/Secretbox');
const sodium = require('../../../util/Sodium');
const Speaking = require('../../../util/Speaking');
const { SILENCE_FRAME } = require('../util/Silence');
// The delay between packets when a user is considered to have stopped speaking
// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
@ -56,7 +58,7 @@ class PacketHandler extends EventEmitter {
}
// Open packet
let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secret_key);
let packet = sodium.methods.open(buffer.slice(12, end), this.nonce, secret_key);
if (!packet) return new Error('Failed to decrypt voice packet');
packet = Buffer.from(packet);
@ -84,8 +86,31 @@ class PacketHandler extends EventEmitter {
const userStat = this.connection.ssrcMap.get(ssrc);
if (!userStat) return;
let opusPacket;
const streamInfo = this.streams.get(userStat.userID);
// If the user is in video, we need to check if the packet is just silence
if (userStat.hasVideo) {
opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
// Only emit an error if we were actively receiving packets from this user
if (streamInfo) {
this.emit('error', opusPacket);
return;
}
}
if (SILENCE_FRAME.equals(opusPacket)) {
// If this is a silence frame, pretend we never received it
return;
}
}
let speakingTimeout = this.speakingTimeouts.get(ssrc);
if (typeof speakingTimeout === 'undefined') {
// Ensure at least the speaking bit is set.
// As the object is by reference, it's only needed once per client re-connect.
if (userStat.speaking === 0) {
userStat.speaking = Speaking.FLAGS.SPEAKING;
}
this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking });
speakingTimeout = this.receiver.connection.client.setTimeout(() => {
try {
@ -101,15 +126,17 @@ class PacketHandler extends EventEmitter {
speakingTimeout.refresh();
}
let stream = this.streams.get(userStat.userID);
if (!stream) return;
stream = stream.stream;
const opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
this.emit('error', opusPacket);
return;
if (streamInfo) {
const { stream } = streamInfo;
if (!opusPacket) {
opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) {
this.emit('error', opusPacket);
return;
}
}
stream.push(opusPacket);
}
stream.push(opusPacket);
}
}

View file

@ -10,4 +10,6 @@ class Silence extends Readable {
}
}
Silence.SILENCE_FRAME = SILENCE_FRAME;
module.exports = Silence;

View file

@ -18,15 +18,13 @@ 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(1).map(Number);
const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007];
/**
* The WebSocket manager for this client.
* <info>This class forwards raw dispatch events,
* read more about it here {@link https://discordapp.com/developers/docs/topics/gateway}</info>
* read more about it here {@link https://discord.com/developers/docs/topics/gateway}</info>
* @extends EventEmitter
*/
class WebSocketManager extends EventEmitter {
@ -45,7 +43,7 @@ class WebSocketManager extends EventEmitter {
* The gateway this manager uses
* @type {?string}
*/
this.gateway = undefined;
this.gateway = null;
/**
* The amount of shards this manager handles
@ -78,7 +76,7 @@ class WebSocketManager extends EventEmitter {
/**
* The current status of this WebSocketManager
* @type {number}
* @type {Status}
*/
this.status = Status.IDLE;
@ -100,11 +98,11 @@ class WebSocketManager extends EventEmitter {
* The current session limit of the client
* @private
* @type {?Object}
* @prop {number} total Total number of identifies available
* @prop {number} remaining Number of identifies remaining
* @prop {number} reset_after Number of milliseconds after which the limit resets
* @property {number} total Total number of identifies available
* @property {number} remaining Number of identifies remaining
* @property {number} reset_after Number of milliseconds after which the limit resets
*/
this.sessionStartLimit = undefined;
this.sessionStartLimit = null;
}
/**
@ -214,7 +212,7 @@ class WebSocketManager extends EventEmitter {
if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
// These event codes cannot be resumed
shard.sessionID = undefined;
shard.sessionID = null;
}
/**

View file

@ -56,10 +56,10 @@ class WebSocketShard extends EventEmitter {
/**
* The current session ID of the shard
* @type {string}
* @type {?string}
* @private
*/
this.sessionID = undefined;
this.sessionID = null;
/**
* The previous heartbeat ping of the shard
@ -83,6 +83,7 @@ class WebSocketShard extends EventEmitter {
/**
* Contains the rate limit queue and metadata
* @name WebSocketShard#ratelimit
* @type {Object}
* @private
*/
@ -98,6 +99,7 @@ class WebSocketShard extends EventEmitter {
/**
* The WebSocket connection for the current shard
* @name WebSocketShard#connection
* @type {?WebSocket}
* @private
*/
@ -110,6 +112,7 @@ class WebSocketShard extends EventEmitter {
/**
* The compression to use
* @name WebSocketShard#inflate
* @type {?Inflate}
* @private
*/
@ -117,13 +120,15 @@ class WebSocketShard extends EventEmitter {
/**
* The HELLO timeout
* @type {?NodeJS.Timer}
* @name WebSocketShard#helloTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'helloTimeout', { value: undefined, writable: true });
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });
/**
* If the manager attached its event handlers on the shard
* @name WebSocketShard#eventsAttached
* @type {boolean}
* @private
*/
@ -131,20 +136,23 @@ class WebSocketShard extends EventEmitter {
/**
* A set of guild IDs this shard expects to receive
* @name WebSocketShard#expectedGuilds
* @type {?Set<string>}
* @private
*/
Object.defineProperty(this, 'expectedGuilds', { value: undefined, writable: true });
Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true });
/**
* The ready timeout
* @type {?NodeJS.Timer}
* @name WebSocketShard#readyTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'readyTimeout', { value: undefined, writable: true });
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
/**
* Time when the WebSocket connection was opened
* @name WebSocketShard#connectedAt
* @type {number}
* @private
*/
@ -407,6 +415,7 @@ class WebSocketShard extends EventEmitter {
this.identify();
break;
case OPCodes.RECONNECT:
this.debug('[RECONNECT] Discord asked us to reconnect');
this.destroy({ closeCode: 4000 });
break;
case OPCodes.INVALID_SESSION:
@ -419,7 +428,7 @@ class WebSocketShard extends EventEmitter {
// Reset the sequence
this.sequence = -1;
// Reset the session ID as it's invalid
this.sessionID = undefined;
this.sessionID = null;
// Set the status to reconnecting
this.status = Status.RECONNECTING;
// Finally, emit the INVALID_SESSION event
@ -448,7 +457,7 @@ class WebSocketShard extends EventEmitter {
// Step 0. Clear the ready timeout, if it exists
if (this.readyTimeout) {
this.manager.client.clearTimeout(this.readyTimeout);
this.readyTimeout = undefined;
this.readyTimeout = null;
}
// Step 1. If we don't have any other guilds pending, we are ready
if (!this.expectedGuilds.size) {
@ -471,7 +480,7 @@ class WebSocketShard extends EventEmitter {
this.debug(`Shard did not receive any more guild packets in 15 seconds.
Unavailable guild count: ${this.expectedGuilds.size}`);
this.readyTimeout = undefined;
this.readyTimeout = null;
this.status = Status.READY;
@ -489,7 +498,7 @@ class WebSocketShard extends EventEmitter {
if (this.helloTimeout) {
this.debug('Clearing the HELLO timeout.');
this.manager.client.clearTimeout(this.helloTimeout);
this.helloTimeout = undefined;
this.helloTimeout = null;
}
return;
}
@ -510,7 +519,7 @@ class WebSocketShard extends EventEmitter {
if (this.heartbeatInterval) {
this.debug('Clearing the heartbeat interval.');
this.manager.client.clearInterval(this.heartbeatInterval);
this.heartbeatInterval = undefined;
this.heartbeatInterval = null;
}
return;
}
@ -622,7 +631,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://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
* a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands).
* 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
@ -725,7 +734,7 @@ class WebSocketShard extends EventEmitter {
// Step 5: Reset the sequence and session ID if requested
if (reset) {
this.sequence = -1;
this.sessionID = undefined;
this.sessionID = null;
}
// Step 6: reset the ratelimit data

View file

@ -10,13 +10,21 @@ module.exports = (client, { d: data }) => {
for (const member of data.members) members.set(member.user.id, guild.members.add(member));
if (data.presences) {
for (const presence of data.presences) guild.presences.cache.add(Object.assign(presence, { guild }));
for (const presence of data.presences) guild.presences.add(Object.assign(presence, { guild }));
}
/**
* Emitted whenever a chunk of guild members is received (all members come from the same guild).
* @event Client#guildMembersChunk
* @param {Collection<Snowflake, GuildMember>} members The members in the chunk
* @param {Guild} guild The guild related to the member chunk
* @param {Object} chunk Properties of the received chunk
* @param {number} chunk.index Index of the received chunk
* @param {number} chunk.count Number of chunks the client should receive
* @param {?string} chunk.nonce Nonce for this chunk
*/
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild);
client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild, {
count: data.chunk_count,
index: data.chunk_index,
nonce: data.nonce,
});
};

View file

@ -1,22 +1,5 @@
'use strict';
const { Status, Events } = require('../../../util/Constants');
module.exports = (client, { d: data }, shard) => {
const guild = client.guilds.cache.get(data.guild_id);
if (guild) {
const member = guild.members.cache.get(data.user.id);
if (member) {
const old = member._update(data);
if (shard.status === Status.READY) {
/**
* Emitted whenever a guild member changes - i.e. new role, removed role, nickname.
* @event Client#guildMemberUpdate
* @param {GuildMember} oldMember The member before the update
* @param {GuildMember} newMember The member after the update
*/
client.emit(Events.GUILD_MEMBER_UPDATE, old, member);
}
}
}
module.exports = (client, packet, shard) => {
client.actions.GuildMemberUpdate.handle(packet.d, shard);
};

View file

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

View file

@ -1,49 +1,5 @@
'use strict';
const { Events } = require('../../../util/Constants');
module.exports = (client, { d: data }) => {
const channel = client.channels.cache.get(data.channel_id);
const user = client.users.cache.get(data.user_id);
const timestamp = new Date(data.timestamp * 1000);
if (channel && user) {
if (channel.type === 'voice') {
client.emit(Events.WARN, `Discord sent a typing packet to a voice channel ${channel.id}`);
return;
}
if (channel._typing.has(user.id)) {
const typing = channel._typing.get(user.id);
typing.lastTimestamp = timestamp;
typing.elapsedTime = Date.now() - typing.since;
client.clearTimeout(typing.timeout);
typing.timeout = tooLate(channel, user);
} else {
const since = new Date();
const lastTimestamp = new Date();
channel._typing.set(user.id, {
user,
since,
lastTimestamp,
elapsedTime: Date.now() - since,
timeout: tooLate(channel, user),
});
/**
* Emitted whenever a user starts typing in a channel.
* @event Client#typingStart
* @param {Channel} channel The channel the user started typing in
* @param {User} user The user that started typing
*/
client.emit(Events.TYPING_START, channel, user);
}
}
module.exports = (client, packet) => {
client.actions.TypingStart.handle(packet.d);
};
function tooLate(channel, user) {
return channel.client.setTimeout(() => {
channel._typing.delete(user.id);
}, 10000);
}

View file

@ -21,11 +21,16 @@ const Messages = {
DISALLOWED_INTENTS: 'Privileged intent provided is not enabled or whitelisted.',
SHARDING_NO_SHARDS: 'No shards have been spawned.',
SHARDING_IN_PROCESS: 'Shards are still being spawned.',
SHARDING_SHARD_NOT_FOUND: id => `Shard ${id} could not be found.`,
SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`,
SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`,
SHARDING_WORKER_EXISTS: id => `Shard ${id} already has an active worker.`,
SHARDING_READY_TIMEOUT: id => `Shard ${id}'s Client took too long to become ready.`,
SHARDING_READY_DISCONNECTED: id => `Shard ${id}'s Client disconnected before becoming ready.`,
SHARDING_READY_DIED: id => `Shard ${id}'s process exited before its Client became ready.`,
SHARDING_NO_CHILD_EXISTS: id => `Shard ${id} has no active process or worker.`,
SHARDING_SHARD_MISCALCULATION: (shard, guild, count) =>
`Calculated invalid shard ${shard} for guild ${guild} with ${count} shards.`,
COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
COLOR_CONVERT: 'Unable to convert color to a number.',
@ -66,7 +71,7 @@ const Messages = {
IMAGE_SIZE: size => `Invalid image size: ${size}`,
MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.',
MESSAGE_NONCE_TYPE: 'Message nonce must fit in an unsigned 64-bit integer.',
MESSAGE_NONCE_TYPE: 'Message nonce must be an integer or a string.',
TYPING_COUNT: 'Count must be at least 1',
@ -99,6 +104,8 @@ const Messages = {
DELETE_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot delete them",
FETCH_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot fetch them",
MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.',
};
for (const [name, message] of Object.entries(Messages)) register(name, message);

View file

@ -6,6 +6,7 @@ module.exports = {
// "Root" classes (starting points)
BaseClient: require('./client/BaseClient'),
Client: require('./client/Client'),
InteractionClient: require('./client/InteractionClient'),
Shard: require('./sharding/Shard'),
ShardClientUtil: require('./sharding/ShardClientUtil'),
ShardingManager: require('./sharding/ShardingManager'),
@ -28,10 +29,12 @@ module.exports = {
SnowflakeUtil: require('./util/Snowflake'),
Structures: require('./util/Structures'),
SystemChannelFlags: require('./util/SystemChannelFlags'),
UserFlags: require('./util/UserFlags'),
Util: Util,
version: require('../package.json').version,
// Managers
BaseGuildEmojiManager: require('./managers/BaseGuildEmojiManager'),
ChannelManager: require('./managers/ChannelManager'),
GuildChannelManager: require('./managers/GuildChannelManager'),
GuildEmojiManager: require('./managers/GuildEmojiManager'),
@ -39,6 +42,7 @@ module.exports = {
GuildMemberManager: require('./managers/GuildMemberManager'),
GuildMemberRoleManager: require('./managers/GuildMemberRoleManager'),
GuildManager: require('./managers/GuildManager'),
ReactionManager: require('./managers/ReactionManager'),
ReactionUserManager: require('./managers/ReactionUserManager'),
MessageManager: require('./managers/MessageManager'),
PresenceManager: require('./managers/PresenceManager'),
@ -54,6 +58,8 @@ module.exports = {
splitMessage: Util.splitMessage,
// Structures
Application: require('./structures/interfaces/Application'),
ApplicationCommand: require('./structures/ApplicationCommand'),
Base: require('./structures/Base'),
Activity: require('./structures/Presence').Activity,
APIMessage: require('./structures/APIMessage'),
@ -74,7 +80,9 @@ module.exports = {
GuildEmoji: require('./structures/GuildEmoji'),
GuildMember: require('./structures/GuildMember'),
GuildPreview: require('./structures/GuildPreview'),
GuildTemplate: require('./structures/GuildTemplate'),
Integration: require('./structures/Integration'),
Interaction: require('./structures/Interaction'),
Invite: require('./structures/Invite'),
Message: require('./structures/Message'),
MessageAttachment: require('./structures/MessageAttachment'),

View file

@ -0,0 +1,80 @@
'use strict';
const BaseManager = require('./BaseManager');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const { parseEmoji } = require('../util/Util');
/**
* Holds methods to resolve GuildEmojis and stores their cache.
* @extends {BaseManager}
*/
class BaseGuildEmojiManager extends BaseManager {
constructor(client, iterable) {
super(client, iterable, GuildEmoji);
}
/**
* The cache of GuildEmojis
* @type {Collection<Snowflake, GuildEmoji>}
* @name BaseGuildEmojiManager#cache
*/
/**
* Data that can be resolved into a GuildEmoji object. This can be:
* * A custom emoji ID
* * A GuildEmoji object
* * A ReactionEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
*/
/**
* Resolves an EmojiResolvable to an Emoji object.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
return super.resolve(emoji);
}
/**
* Resolves an EmojiResolvable to an Emoji ID string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?Snowflake}
*/
resolveID(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
return super.resolveID(emoji);
}
/**
* 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
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveIdentifier(emoji) {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
const res = parseEmoji(emoji);
if (res && res.name.length) {
emoji = `${res.animated ? 'a:' : ''}${res.name}${res.id ? `:${res.id}` : ''}`;
}
if (!emoji.includes('%')) return encodeURIComponent(emoji);
return emoji;
}
return null;
}
}
module.exports = BaseGuildEmojiManager;

View file

@ -74,6 +74,7 @@ class ChannelManager extends BaseManager {
* Obtains a channel from Discord, or the channel cache if it's already available.
* @param {Snowflake} id ID of the channel
* @param {boolean} [cache=true] Whether to cache the new channel object if it isn't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Channel>}
* @example
* // Fetch a channel by its id
@ -81,9 +82,11 @@ class ChannelManager extends BaseManager {
* .then(channel => console.log(channel.name))
* .catch(console.error);
*/
async fetch(id, cache = true) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
async fetch(id, cache = true, force = false) {
if (!force) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.channels(id).get();
return this.add(data, null, cache);

View file

@ -46,7 +46,7 @@ class GuildChannelManager extends BaseManager {
* @memberof GuildChannelManager
* @instance
* @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve
* @returns {?Channel}
* @returns {?GuildChannel}
*/
/**

View file

@ -1,19 +1,18 @@
'use strict';
const BaseManager = require('./BaseManager');
const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
const { TypeError } = require('../errors');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const Collection = require('../util/Collection');
const DataResolver = require('../util/DataResolver');
/**
* Manages API methods for GuildEmojis and stores their cache.
* @extends {BaseManager}
* @extends {BaseGuildEmojiManager}
*/
class GuildEmojiManager extends BaseManager {
class GuildEmojiManager extends BaseGuildEmojiManager {
constructor(guild, iterable) {
super(guild.client, iterable, GuildEmoji);
super(guild.client, iterable);
/**
* The guild this manager belongs to
* @type {Guild}
@ -21,12 +20,6 @@ class GuildEmojiManager extends BaseManager {
this.guild = guild;
}
/**
* The cache of GuildEmojis
* @type {Collection<Snowflake, GuildEmoji>}
* @name GuildEmojiManager#cache
*/
add(data, cache) {
return super.add(data, cache, { extras: [this.guild] });
}
@ -73,57 +66,6 @@ class GuildEmojiManager extends BaseManager {
.emojis.post({ data, reason })
.then(emoji => this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji);
}
/**
* Data that can be resolved into an GuildEmoji object. This can be:
* * A custom emoji ID
* * A GuildEmoji object
* * A ReactionEmoji object
* @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable
*/
/**
* Resolves an EmojiResolvable to an Emoji object.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?GuildEmoji}
*/
resolve(emoji) {
if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id);
return super.resolve(emoji);
}
/**
* Resolves an EmojiResolvable to an Emoji ID string.
* @param {EmojiResolvable} emoji The Emoji resolvable to identify
* @returns {?Snowflake}
*/
resolveID(emoji) {
if (emoji instanceof ReactionEmoji) return emoji.id;
return super.resolveID(emoji);
}
/**
* Data that can be resolved to give an emoji identifier. This can be:
* * The unicode representation of an emoji
* * An EmojiResolvable
* @typedef {string|EmojiResolvable} EmojiIdentifierResolvable
*/
/**
* Resolves an EmojiResolvable to an emoji identifier.
* @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
* @returns {?string}
*/
resolveIdentifier(emoji) {
const emojiResolvable = this.resolve(emoji);
if (emojiResolvable) return emojiResolvable.identifier;
if (emoji instanceof ReactionEmoji) return emoji.identifier;
if (typeof emoji === 'string') {
if (!emoji.includes('%')) return encodeURIComponent(emoji);
else return emoji;
}
return null;
}
}
module.exports = GuildEmojiManager;

View file

@ -8,6 +8,7 @@ const GuildMember = require('../structures/GuildMember');
const Invite = require('../structures/Invite');
const Role = require('../structures/Role');
const {
ChannelTypes,
Events,
VerificationLevels,
DefaultMessageNotifications,
@ -129,6 +130,8 @@ class GuildManager extends BaseManager {
* <warn>This is only available to bots in fewer than 10 guilds.</warn>
* @param {string} name The name of the guild
* @param {Object} [options] Options for the creating
* @param {number} [options.afkChannelID] The ID of the AFK channel
* @param {number} [options.afkTimeout] The AFK timeout in seconds
* @param {PartialChannelData[]} [options.channels] The channels for this guild
* @param {DefaultMessageNotifications} [options.defaultMessageNotifications] The default message notifications
* for the guild
@ -137,18 +140,22 @@ class GuildManager extends BaseManager {
* @param {string} [options.region] The region for the server, defaults to the closest one available
* @param {PartialRoleData[]} [options.roles] The roles for this guild,
* the first element of this array is used to change properties of the guild's everyone role.
* @param {number} [options.systemChannelID] The ID of the system channel
* @param {VerificationLevel} [options.verificationLevel] The verification level for the guild
* @returns {Promise<Guild>} The guild that was created
*/
async create(
name,
{
afkChannelID,
afkTimeout,
channels = [],
defaultMessageNotifications,
explicitContentFilter,
icon = null,
region,
roles = [],
systemChannelID,
verificationLevel,
} = {},
) {
@ -163,6 +170,7 @@ class GuildManager extends BaseManager {
explicitContentFilter = ExplicitContentFilterLevels.indexOf(explicitContentFilter);
}
for (const channel of channels) {
if (channel.type) channel.type = ChannelTypes[channel.type.toUpperCase()];
channel.parent_id = channel.parentID;
delete channel.parentID;
if (!channel.permissionOverwrites) continue;
@ -187,8 +195,11 @@ class GuildManager extends BaseManager {
verification_level: verificationLevel,
default_message_notifications: defaultMessageNotifications,
explicit_content_filter: explicitContentFilter,
channels,
roles,
channels,
afk_channel_id: afkChannelID,
afk_timeout: afkTimeout,
system_channel_id: systemChannelID,
},
})
.then(data => {
@ -196,21 +207,46 @@ class GuildManager extends BaseManager {
const handleGuild = guild => {
if (guild.id === data.id) {
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
this.client.clearTimeout(timeout);
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
this.client.decrementMaxListeners();
resolve(guild);
}
};
this.client.incrementMaxListeners();
this.client.on(Events.GUILD_CREATE, handleGuild);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
this.client.decrementMaxListeners();
resolve(this.client.guilds.add(data));
}, 10000);
return undefined;
}, reject),
);
}
/**
* Obtains a guild from Discord, or the guild cache if it's already available.
* @param {Snowflake} id ID of the guild
* @param {boolean} [cache=true] Whether to cache the new guild object if it isn't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Guild>}
* @example
* // Fetch a guild by its id
* client.guilds.fetch('222078108977594368')
* .then(guild => console.log(guild.name))
* .catch(console.error);
*/
async fetch(id, cache = true, force = false) {
if (!force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
const data = await this.client.api.guilds(id).get({ query: { with_counts: true } });
return this.add(data, cache);
}
}
module.exports = GuildManager;

View file

@ -1,10 +1,11 @@
'use strict';
const BaseManager = require('./BaseManager');
const { Error, TypeError } = require('../errors');
const { Error, TypeError, RangeError } = require('../errors');
const GuildMember = require('../structures/GuildMember');
const Collection = require('../util/Collection');
const { Events, OPCodes } = require('../util/Constants');
const SnowflakeUtil = require('../util/Snowflake');
/**
* Manages API methods for GuildMembers and stores their cache.
@ -67,6 +68,7 @@ class GuildMemberManager extends BaseManager {
* @typedef {Object} FetchMemberOptions
* @property {UserResolvable} user The user to fetch
* @property {boolean} [cache=true] Whether or not to cache the fetched member
* @property {boolean} [force=false] Whether to skip the cache check and request the API
*/
/**
@ -76,6 +78,9 @@ class GuildMemberManager extends BaseManager {
* @property {?string} query Limit fetch to members with similar usernames
* @property {number} [limit=0] Maximum number of members to request
* @property {boolean} [withPresences=false] Whether or not to include the presences
* @property {number} [time=120e3] Timeout for receipt of members
* @property {?string} nonce Nonce for this request (32 characters max - default to base 16 now timestamp)
* @property {boolean} [force=false] Whether to skip the cache check and request the API
*/
/**
@ -95,6 +100,11 @@ class GuildMemberManager extends BaseManager {
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch a single member without checking cache
* guild.members.fetch({ user, force: true })
* .then(console.log)
* .catch(console.error)
* @example
* // Fetch a single member without caching
* guild.members.fetch({ user, cache: false })
* .then(console.log)
@ -133,6 +143,7 @@ class GuildMemberManager extends BaseManager {
* @param {number} [options.days=7] Number of days of inactivity required to kick
* @param {boolean} [options.dry=false] Get number of users that will be kicked, without actually kicking them
* @param {boolean} [options.count=true] Whether or not to return the number of users that have been kicked.
* @param {RoleResolvable[]} [options.roles=[]] Array of roles to bypass the "...and no roles" constraint when pruning
* @param {string} [options.reason] Reason for this prune
* @returns {Promise<number|null>} The number of members that were/will be kicked
* @example
@ -145,16 +156,39 @@ class GuildMemberManager extends BaseManager {
* guild.members.prune({ days: 1, reason: 'too many people!' })
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error);
* @example
* // Include members with a specified role
* guild.members.prune({ days: 7, roles: ['657259391652855808'] })
* .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error);
*/
prune({ days = 7, dry = false, count = true, reason } = {}) {
prune({ days = 7, dry = false, count: compute_prune_count = true, roles = [], reason } = {}) {
if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
return this.client.api
.guilds(this.guild.id)
.prune[dry ? 'get' : 'post']({
query: {
days,
compute_prune_count: count,
},
const query = { days };
const resolvedRoles = [];
for (const role of roles) {
const resolvedRole = this.guild.roles.resolveID(role);
if (!resolvedRole) {
return Promise.reject(new TypeError('INVALID_TYPE', 'roles', 'Array of Roles or Snowflakes', true));
}
resolvedRoles.push(resolvedRole);
}
if (resolvedRoles.length) {
query.include_roles = dry ? resolvedRoles.join(',') : resolvedRoles;
}
const endpoint = this.client.api.guilds(this.guild.id).prune;
if (dry) {
return endpoint.get({ query, reason }).then(data => data.pruned);
}
return endpoint
.post({
data: { ...query, compute_prune_count },
reason,
})
.then(data => data.pruned);
@ -164,7 +198,7 @@ class GuildMemberManager extends BaseManager {
* Bans a user from the guild.
* @param {UserResolvable} user The user to ban
* @param {Object} [options] Options for the ban
* @param {number} [options.days=0] Number of days of messages to delete
* @param {number} [options.days=0] Number of days of messages to delete, must be between 0 and 7
* @param {string} [options.reason] Reason for banning
* @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
* If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
@ -176,12 +210,13 @@ class GuildMemberManager extends BaseManager {
* .catch(console.error);
*/
ban(user, options = { days: 0 }) {
if (options.days) options['delete-message-days'] = options.days;
if (typeof options !== 'object') return Promise.reject(new TypeError('INVALID_TYPE', 'options', 'object', true));
if (options.days) options.delete_message_days = options.days;
const id = this.client.users.resolveID(user);
if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true));
return this.client.api
.guilds(this.guild.id)
.bans[id].put({ query: options })
.bans[id].put({ data: options })
.then(() => {
if (user instanceof GuildMember) return user;
const _user = this.client.users.resolve(id);
@ -213,9 +248,12 @@ class GuildMemberManager extends BaseManager {
.then(() => this.client.users.resolve(user));
}
_fetchSingle({ user, cache }) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return Promise.resolve(existing);
_fetchSingle({ user, cache, force = false }) {
if (!force) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return Promise.resolve(existing);
}
return this.client.api
.guilds(this.guild.id)
.members(user)
@ -223,13 +261,22 @@ class GuildMemberManager extends BaseManager {
.then(data => this.add(data, cache));
}
_fetchMany({ limit = 0, withPresences: presences = false, user: user_ids, query } = {}) {
_fetchMany({
limit = 0,
withPresences: presences = false,
user: user_ids,
query,
time = 120e3,
nonce = SnowflakeUtil.generate(),
force = false,
} = {}) {
return new Promise((resolve, reject) => {
if (this.guild.memberCount === this.cache.size && !query && !limit && !presences && !user_ids) {
if (this.guild.memberCount === this.cache.size && !query && !limit && !presences && !user_ids && !force) {
resolve(this.cache);
return;
}
if (!query && !user_ids) query = '';
if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH');
this.guild.shard.send({
op: OPCodes.REQUEST_GUILD_MEMBERS,
d: {
@ -237,33 +284,41 @@ class GuildMemberManager extends BaseManager {
presences,
user_ids,
query,
nonce,
limit,
},
});
const fetchedMembers = new Collection();
const option = query || limit || presences || user_ids;
const handler = (members, guild) => {
if (guild.id !== this.guild.id) return;
let i = 0;
const handler = (members, _, chunk) => {
timeout.refresh();
if (chunk.nonce !== nonce) return;
i++;
for (const member of members.values()) {
if (option) fetchedMembers.set(member.id, member);
}
if (
this.guild.memberCount <= this.cache.size ||
(option && members.size < 1000) ||
(limit && fetchedMembers.size >= limit)
(limit && fetchedMembers.size >= limit) ||
i === chunk.count
) {
this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
this.client.clearTimeout(timeout);
this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
this.client.decrementMaxListeners();
let fetched = option ? fetchedMembers : this.cache;
if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
resolve(fetched);
}
};
const timeout = this.guild.client.setTimeout(() => {
this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
this.client.decrementMaxListeners();
reject(new Error('GUILD_MEMBERS_TIMEOUT'));
}, 120e3);
this.guild.client.on(Events.GUILD_MEMBERS_CHUNK, handler);
}, time);
this.client.incrementMaxListeners();
this.client.on(Events.GUILD_MEMBERS_CHUNK, handler);
});
}
}

View file

@ -90,12 +90,7 @@ class GuildMemberRoleManager {
} else {
roleOrRoles = this.guild.roles.resolve(roleOrRoles);
if (roleOrRoles === null) {
throw new TypeError(
'INVALID_TYPE',
'roles',
'Role, Snowflake or Array or Collection of Roles or Snowflakes',
true,
);
throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes');
}
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason });

View file

@ -1,6 +1,7 @@
'use strict';
const BaseManager = require('./BaseManager');
const { TypeError } = require('../errors');
const Message = require('../structures/Message');
const Collection = require('../util/Collection');
const LimitedCollection = require('../util/LimitedCollection');
@ -45,6 +46,7 @@ class MessageManager extends BaseManager {
* Those need to be fetched separately in such a case.</info>
* @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters.
* @param {boolean} [cache=true] Whether to cache the message(s)
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Message>|Promise<Collection<Snowflake, Message>>}
* @example
* // Get message
@ -62,8 +64,8 @@ class MessageManager extends BaseManager {
* .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`))
* .catch(console.error);
*/
fetch(message, cache = true) {
return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache);
fetch(message, cache = true, force = false) {
return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache);
}
/**
@ -74,7 +76,7 @@ class MessageManager extends BaseManager {
* @returns {Promise<Collection<Snowflake, Message>>}
* @example
* // Get pinned messages
* channel.fetchPinned()
* channel.messages.fetchPinned()
* .then(messages => console.log(`Received ${messages.size} messages`))
* .catch(console.error);
*/
@ -115,20 +117,21 @@ class MessageManager extends BaseManager {
* Deletes a message, even if it's not cached.
* @param {MessageResolvable} message The message to delete
* @param {string} [reason] Reason for deleting this message, if it does not belong to the client user
* @returns {Promise<void>}
*/
async delete(message, reason) {
message = this.resolveID(message);
if (message) {
await this.client.api
.channels(this.channel.id)
.messages(message)
.delete({ reason });
}
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).messages(message).delete({ reason });
}
async _fetchId(messageID, cache) {
const existing = this.cache.get(messageID);
if (existing && !existing.partial) return existing;
async _fetchId(messageID, cache, force) {
if (!force) {
const existing = this.cache.get(messageID);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.channels[this.channel.id].messages[messageID].get();
return this.add(data, cache);
}

View file

@ -24,7 +24,7 @@ class ReactionManager extends BaseManager {
/**
* The reaction cache of this manager
* @type {Collection<Snowflake, MessageReaction>}
* @type {Collection<string|Snowflake, MessageReaction>}
* @name ReactionManager#cache
*/

View file

@ -48,16 +48,16 @@ class ReactionUserManager extends BaseManager {
/**
* Removes a user from this reaction.
* @param {UserResolvable} [user=this.reaction.message.client.user] The user to remove the reaction of
* @param {UserResolvable} [user=this.client.user] The user to remove the reaction of
* @returns {Promise<MessageReaction>}
*/
remove(user = this.reaction.message.client.user) {
const message = this.reaction.message;
const userID = message.client.users.resolveID(user);
remove(user = this.client.user) {
const userID = this.client.users.resolveID(user);
if (!userID) return Promise.reject(new Error('REACTION_RESOLVE_USER'));
return message.client.api.channels[message.channel.id].messages[message.id].reactions[
this.reaction.emoji.identifier
][userID === message.client.user.id ? '@me' : userID]
const message = this.reaction.message;
return this.client.api.channels[message.channel.id].messages[message.id].reactions[this.reaction.emoji.identifier][
userID === this.client.user.id ? '@me' : userID
]
.delete()
.then(() => this.reaction);
}

View file

@ -2,6 +2,7 @@
const BaseManager = require('./BaseManager');
const Role = require('../structures/Role');
const Collection = require('../util/Collection');
const Permissions = require('../util/Permissions');
const { resolveColor } = require('../util/Util');
@ -31,9 +32,10 @@ class RoleManager extends BaseManager {
/**
* Obtains one or more roles from Discord, or the role cache if they're already available.
* @param {Snowflake} [id] ID or IDs of the role(s)
* @param {boolean} [cache=true] Whether to cache the new roles objects if it weren't already
* @returns {Promise<Role|RoleManager>}
* @param {Snowflake} [id] ID of the role to fetch
* @param {boolean} [cache=true] Whether to cache the new role object(s) if they weren't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<?Role|Collection<Snowflake, Role>>}
* @example
* // Fetch all roles from the guild
* message.guild.roles.fetch()
@ -45,16 +47,17 @@ class RoleManager extends BaseManager {
* .then(role => console.log(`The role color is: ${role.color}`))
* .catch(console.error);
*/
async fetch(id, cache = true) {
if (id) {
async fetch(id, cache = true, force = false) {
if (id && !force) {
const existing = this.cache.get(id);
if (existing) return existing;
}
// We cannot fetch a single role, as of this commit's date, Discord API throws with 405
const roles = await this.client.api.guilds(this.guild.id).roles.get();
for (const role of roles) this.add(role, cache);
return id ? this.cache.get(id) || null : this;
const data = await this.client.api.guilds(this.guild.id).roles.get();
const roles = new Collection();
for (const role of data) roles.set(role.id, this.add(role, cache));
return id ? roles.get(id) || null : roles;
}
/**
@ -125,11 +128,11 @@ class RoleManager extends BaseManager {
/**
* The `@everyone` role of the guild
* @type {?Role}
* @type {Role}
* @readonly
*/
get everyone() {
return this.cache.get(this.guild.id) || null;
return this.cache.get(this.guild.id);
}
/**

View file

@ -55,11 +55,15 @@ class UserManager extends BaseManager {
* Obtains a user from Discord, or the user cache if it's already available.
* @param {Snowflake} id ID of the user
* @param {boolean} [cache=true] Whether to cache the new user object if it isn't already
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<User>}
*/
async fetch(id, cache = true) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
async fetch(id, cache = true, force = false) {
if (!force) {
const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.users(id).get();
return this.add(data, cache);
}

View file

@ -1,7 +1,6 @@
'use strict';
const BaseManager = require('./BaseManager');
const VoiceState = require('../structures/VoiceState');
/**
* Manages API methods for VoiceStates and stores their cache.
@ -9,7 +8,7 @@ const VoiceState = require('../structures/VoiceState');
*/
class VoiceStateManager extends BaseManager {
constructor(guild, iterable) {
super(guild.client, iterable, VoiceState);
super(guild.client, iterable, { name: 'VoiceState' });
/**
* The guild this manager belongs to
* @type {Guild}
@ -27,7 +26,7 @@ class VoiceStateManager extends BaseManager {
const existing = this.cache.get(data.user_id);
if (existing) return existing._patch(data);
const entry = new VoiceState(this.guild, data);
const entry = new this.holds(this.guild, data);
if (cache) this.cache.set(data.user_id, entry);
return entry;
}

View file

@ -1,8 +1,8 @@
'use strict';
const https = require('https');
const FormData = require('@discordjs/form-data');
const AbortController = require('abort-controller');
const FormData = require('form-data');
const fetch = require('node-fetch');
const { browser, UserAgent } = require('../util/Constants');
@ -15,11 +15,13 @@ class APIRequest {
this.method = method;
this.route = options.route;
this.options = options;
this.retries = 0;
let queryString = '';
if (options.query) {
// Filter out undefined query options
const query = Object.entries(options.query).filter(([, value]) => value !== null && typeof value !== 'undefined');
const query = Object.entries(options.query)
.filter(([, value]) => ![null, 'null', 'undefined'].includes(value) && typeof value !== 'undefined')
.flatMap(([key, value]) => (Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]]));
queryString = new URLSearchParams(query).toString();
}
this.path = `${path}${queryString && `?${queryString}`}`;

95
src/rest/AsyncQueue.js Normal file
View file

@ -0,0 +1,95 @@
/**
* MIT License
*
* Copyright (c) 2020 kyranet, discord.js
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
'use strict';
// TODO(kyranet, vladfrangu): replace this with discord.js v13's core AsyncQueue.
/**
* An async queue that preserves the stack and prevents lock-ups.
* @private
*/
class AsyncQueue {
constructor() {
/**
* The promises array.
* @type {Array<{promise: Promise<void>, resolve: Function}>}
* @private
*/
this.promises = [];
}
/**
* The remaining amount of queued promises
* @type {number}
*/
get remaining() {
return this.promises.length;
}
/**
* Waits for last promise and queues a new one.
* @returns {Promise<void>}
* @example
* const queue = new AsyncQueue();
* async function request(url, options) {
* await queue.wait();
* try {
* const result = await fetch(url, options);
* // Do some operations with 'result'
* } finally {
* // Remove first entry from the queue and resolve for the next entry
* queue.shift();
* }
* }
*
* request(someUrl1, someOptions1); // Will call fetch() immediately
* request(someUrl2, someOptions2); // Will call fetch() after the first finished
* request(someUrl3, someOptions3); // Will call fetch() after the second finished
*/
wait() {
const next = this.promises.length ? this.promises[this.promises.length - 1].promise : Promise.resolve();
let resolve;
const promise = new Promise(res => {
resolve = res;
});
this.promises.push({
resolve,
promise,
});
return next;
}
/**
* Frees the queue's lock for the next item to process.
*/
shift() {
const deferred = this.promises.shift();
if (typeof deferred !== 'undefined') deferred.resolve();
}
}
module.exports = AsyncQueue;

View file

@ -35,19 +35,6 @@ class RESTManager {
return Endpoints.CDN(this.client.options.http.cdn);
}
push(handler, apiRequest) {
return new Promise((resolve, reject) => {
handler
.push({
request: apiRequest,
resolve,
reject,
retries: 0,
})
.catch(reject);
});
}
request(method, url, options = {}) {
const apiRequest = new APIRequest(this, method, url, options);
let handler = this.handlers.get(apiRequest.route);
@ -57,7 +44,11 @@ class RESTManager {
this.handlers.set(apiRequest.route, handler);
}
return this.push(handler, apiRequest);
return handler.push(apiRequest);
}
get endpoint() {
return this.client.options.http.api;
}
set endpoint(endpoint) {

View file

@ -1,5 +1,6 @@
'use strict';
const AsyncQueue = require('./AsyncQueue');
const DiscordAPIError = require('./DiscordAPIError');
const HTTPError = require('./HTTPError');
const {
@ -25,46 +26,31 @@ function calculateReset(reset, serverDate) {
class RequestHandler {
constructor(manager) {
this.manager = manager;
this.busy = false;
this.queue = [];
this.queue = new AsyncQueue();
this.reset = -1;
this.remaining = -1;
this.limit = -1;
this.retryAfter = -1;
}
push(request) {
if (this.busy) {
this.queue.push(request);
return this.run();
} else {
return this.execute(request);
async push(request) {
await this.queue.wait();
try {
return await this.execute(request);
} finally {
this.queue.shift();
}
}
run() {
if (this.queue.length === 0) return Promise.resolve();
return this.execute(this.queue.shift());
}
get limited() {
return Boolean(this.manager.globalTimeout) || (this.remaining <= 0 && Date.now() < this.reset);
}
get _inactive() {
return this.queue.length === 0 && !this.limited && this.busy !== true;
return this.queue.remaining === 0 && !this.limited;
}
async execute(item) {
// Insert item back to the beginning if currently busy
if (this.busy) {
this.queue.unshift(item);
return null;
}
this.busy = true;
const { reject, request, resolve } = item;
async execute(request) {
// After calculations and requests have been done, pre-emptively stop further requests
if (this.limited) {
const timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
@ -102,9 +88,13 @@ class RequestHandler {
try {
res = await request.make();
} catch (error) {
// NodeFetch error expected for all "operational" errors, such as 500 status code
this.busy = false;
return reject(new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path));
// Retry the specified number of times for request abortions
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path);
}
request.retries++;
return this.execute(request);
}
if (res && res.headers) {
@ -120,7 +110,7 @@ class RequestHandler {
this.retryAfter = retryAfter ? Number(retryAfter) : -1;
// https://github.com/discordapp/discord-api-docs/issues/182
if (item.request.route.includes('reactions')) {
if (request.route.includes('reactions')) {
this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250;
}
@ -137,43 +127,46 @@ class RequestHandler {
}
}
// Finished handling headers, safe to unlock manager
this.busy = false;
// Handle 2xx and 3xx responses
if (res.ok) {
const success = await parseResponse(res);
// Nothing wrong with the request, proceed with the next one
resolve(success);
return this.run();
} else if (res.status === 429) {
// A ratelimit was hit - this should never happen
this.queue.unshift(item);
this.manager.client.emit('debug', `429 hit on route ${item.request.route}`);
await Util.delayFor(this.retryAfter);
return this.run();
} else if (res.status >= 500 && res.status < 600) {
// Retry the specified number of times for possible serverside issues
if (item.retries === this.manager.client.options.retryLimit) {
return reject(
new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path),
);
} else {
item.retries++;
this.queue.unshift(item);
return this.run();
}
} else {
// Handle possible malformed requests
try {
const data = await parseResponse(res);
if (res.status >= 400 && res.status < 500) {
return reject(new DiscordAPIError(request.path, data, request.method, res.status));
}
return null;
} catch (err) {
return reject(new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path));
}
return parseResponse(res);
}
// Handle 4xx responses
if (res.status >= 400 && res.status < 500) {
// Handle ratelimited requests
if (res.status === 429) {
// A ratelimit was hit - this should never happen
this.manager.client.emit('debug', `429 hit on route ${request.route}`);
await Util.delayFor(this.retryAfter);
return this.execute(request);
}
// Handle possible malformed requests
let data;
try {
data = await parseResponse(res);
} catch (err) {
throw new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path);
}
throw new DiscordAPIError(request.path, data, request.method, res.status);
}
// Handle 5xx responses
if (res.status >= 500 && res.status < 600) {
// Retry the specified number of times for possible serverside issues
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(res.statusText, res.constructor.name, res.status, request.method, request.path);
}
request.retries++;
return this.execute(request);
}
// Fallback in the rare case a status code outside the range 200..=599 is returned
return null;
}
}

View file

@ -44,7 +44,7 @@ class Shard extends EventEmitter {
/**
* Arguments for the shard's process executable (only when {@link ShardingManager#mode} is `process`)
* @type {?string[]}
* @type {string[]}
*/
this.execArgv = manager.execArgv;
@ -124,6 +124,9 @@ class Shard extends EventEmitter {
.on('exit', this._exitListener);
}
this._evals.clear();
this._fetches.clear();
/**
* Emitted upon the creation of the shard's child process/worker.
* @event Shard#spawn
@ -225,6 +228,10 @@ class Shard extends EventEmitter {
* .catch(console.error);
*/
fetchClientValue(prop) {
// Shard is dead (maybe respawning), don't cache anything and error immediately
if (!this.process && !this.worker) return Promise.reject(new Error('SHARDING_NO_CHILD_EXISTS', this.id));
// Cached promise from previous call
if (this._fetches.has(prop)) return this._fetches.get(prop);
const promise = new Promise((resolve, reject) => {
@ -255,6 +262,10 @@ class Shard extends EventEmitter {
* @returns {Promise<*>} Result of the script execution
*/
eval(script) {
// Shard is dead (maybe respawning), don't cache anything and error immediately
if (!this.process && !this.worker) return Promise.reject(new Error('SHARDING_NO_CHILD_EXISTS', this.id));
// Cached promise from previous call
if (this._evals.has(script)) return this._evals.get(script);
const promise = new Promise((resolve, reject) => {
@ -323,18 +334,20 @@ class Shard extends EventEmitter {
// Shard is requesting a property fetch
if (message._sFetchProp) {
this.manager.fetchClientValues(message._sFetchProp).then(
results => this.send({ _sFetchProp: message._sFetchProp, _result: results }),
err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }),
const resp = { _sFetchProp: message._sFetchProp, _sFetchPropShard: message._sFetchPropShard };
this.manager.fetchClientValues(message._sFetchProp, message._sFetchPropShard).then(
results => this.send({ ...resp, _result: results }),
err => this.send({ ...resp, _error: Util.makePlainError(err) }),
);
return;
}
// Shard is requesting an eval broadcast
if (message._sEval) {
this.manager.broadcastEval(message._sEval).then(
results => this.send({ _sEval: message._sEval, _result: results }),
err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }),
const resp = { _sEval: message._sEval, _sEvalShard: message._sEvalShard };
this.manager.broadcastEval(message._sEval, message._sEvalShard).then(
results => this.send({ ...resp, _result: results }),
err => this.send({ ...resp, _error: Util.makePlainError(err) }),
);
return;
}

View file

@ -79,6 +79,7 @@ class ShardClientUtil {
* Sends a message to the master process.
* @param {*} message Message to send
* @returns {Promise<void>}
* @emits Shard#message
*/
send(message) {
return new Promise((resolve, reject) => {
@ -95,28 +96,29 @@ class ShardClientUtil {
}
/**
* Fetches a client property value of each shard.
* Fetches a client property value of each shard, or a given shard.
* @param {string} prop Name of the client property to get, using periods for nesting
* @returns {Promise<Array<*>>}
* @param {number} [shard] Shard to fetch property from, all if undefined
* @returns {Promise<*>|Promise<Array<*>>}
* @example
* client.shard.fetchClientValues('guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error);
* @see {@link ShardingManager#fetchClientValues}
*/
fetchClientValues(prop) {
fetchClientValues(prop, shard) {
return new Promise((resolve, reject) => {
const parent = this.parentPort || process;
const listener = message => {
if (!message || message._sFetchProp !== prop) return;
if (!message || message._sFetchProp !== prop || message._sFetchPropShard !== shard) return;
parent.removeListener('message', listener);
if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error));
};
parent.on('message', listener);
this.send({ _sFetchProp: prop }).catch(err => {
this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => {
parent.removeListener('message', listener);
reject(err);
});
@ -124,29 +126,30 @@ class ShardClientUtil {
}
/**
* Evaluates a script or function on all shards, in the context of the {@link Clients}.
* Evaluates a script or function on all shards, or a given shard, in the context of the {@link Client}s.
* @param {string|Function} script JavaScript to run on each shard
* @returns {Promise<Array<*>>} Results of the script execution
* @param {number} [shard] Shard to run script on, all if undefined
* @returns {Promise<*>|Promise<Array<*>>} Results of the script execution
* @example
* client.shard.broadcastEval('this.guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error);
* @see {@link ShardingManager#broadcastEval}
*/
broadcastEval(script) {
broadcastEval(script, shard) {
return new Promise((resolve, reject) => {
const parent = this.parentPort || process;
script = typeof script === 'function' ? `(${script})(this)` : script;
const listener = message => {
if (!message || message._sEval !== script) return;
if (!message || message._sEval !== script || message._sEvalShard !== shard) return;
parent.removeListener('message', listener);
if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error));
};
parent.on('message', listener);
this.send({ _sEval: script }).catch(err => {
this.send({ _sEval: script, _sEvalShard: shard }).catch(err => {
parent.removeListener('message', listener);
reject(err);
});
@ -223,6 +226,18 @@ class ShardClientUtil {
}
return this._singleton;
}
/**
* Get the shard ID for a given guild ID.
* @param {Snowflake} guildID Snowflake guild ID to get shard ID for
* @param {number} shardCount Number of shards
* @returns {number}
*/
static shardIDForGuildID(guildID, shardCount) {
const shard = Number(BigInt(guildID) >> 22n) % shardCount;
if (shard < 0) throw new Error('SHARDING_SHARD_MISCALCULATION', shard, guildID, shardCount);
return shard;
}
}
module.exports = ShardClientUtil;

View file

@ -20,9 +20,7 @@ const Util = require('../util/Util');
class ShardingManager extends EventEmitter {
/**
* The mode to spawn shards with for a {@link ShardingManager}: either "process" to use child processes, or
* "worker" to use workers. The "worker" mode relies on the experimental
* [Worker threads](https://nodejs.org/api/worker_threads.html) functionality that is present in Node v10.5.0 or
* newer. Node must be started with the `--experimental-worker` flag to expose it.
* "worker" to use [Worker threads](https://nodejs.org/api/worker_threads.html).
* @typedef {Object} ShardingManagerMode
*/
@ -224,30 +222,48 @@ class ShardingManager extends EventEmitter {
}
/**
* Evaluates a script on all shards, in the context of the {@link Client}s.
* Evaluates a script on all shards, or a given shard, in the context of the {@link Client}s.
* @param {string} script JavaScript to run on each shard
* @returns {Promise<Array<*>>} Results of the script execution
* @param {number} [shard] Shard to run on, all if undefined
* @returns {Promise<*>|Promise<Array<*>>} Results of the script execution
*/
broadcastEval(script) {
const promises = [];
for (const shard of this.shards.values()) promises.push(shard.eval(script));
return Promise.all(promises);
broadcastEval(script, shard) {
return this._performOnShards('eval', [script], shard);
}
/**
* Fetches a client property value of each shard.
* Fetches a client property value of each shard, or a given shard.
* @param {string} prop Name of the client property to get, using periods for nesting
* @returns {Promise<Array<*>>}
* @param {number} [shard] Shard to fetch property from, all if undefined
* @returns {Promise<*>|Promise<Array<*>>}
* @example
* manager.fetchClientValues('guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error);
*/
fetchClientValues(prop) {
fetchClientValues(prop, shard) {
return this._performOnShards('fetchClientValue', [prop], shard);
}
/**
* Runs a method with given arguments on all shards, or a given shard.
* @param {string} method Method name to run on each shard
* @param {Array<*>} args Arguments to pass through to the method call
* @param {number} [shard] Shard to run on, all if undefined
* @returns {Promise<*>|Promise<Array<*>>} Results of the method execution
* @private
*/
_performOnShards(method, args, shard) {
if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS'));
if (this.shards.size !== this.shardList.length) return Promise.reject(new Error('SHARDING_IN_PROCESS'));
if (typeof shard === 'number') {
if (this.shards.has(shard)) return this.shards.get(shard)[method](...args);
return Promise.reject(new Error('SHARDING_SHARD_NOT_FOUND', shard));
}
const promises = [];
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
for (const sh of this.shards.values()) promises.push(sh[method](...args));
return Promise.all(promises);
}

View file

@ -74,13 +74,21 @@ class APIMessage {
return this.target instanceof Message;
}
/**
* Whether or not the target is an interaction
* @type {boolean}
* @readonly
*/
get isInteraction() {
const Interaction = require('./Interaction');
return this.target instanceof Interaction;
}
/**
* Makes the content of this message.
* @returns {?(string|string[])}
*/
makeContent() {
const GuildMember = require('./GuildMember');
let content;
if (this.options.content === null) {
content = '';
@ -88,14 +96,16 @@ class APIMessage {
content = Util.resolveString(this.options.content);
}
if (typeof content !== 'string') return content;
const disableMentions =
typeof this.options.disableMentions === 'undefined'
? this.target.client.options.disableMentions
: this.options.disableMentions;
if (disableMentions === 'all') {
content = Util.removeMentions(content || '');
content = Util.removeMentions(content);
} else if (disableMentions === 'everyone') {
content = (content || '').replace(/@([^<>@ ]*)/gmsu, (match, target) => {
content = content.replace(/@([^<>@ ]*)/gmsu, (match, target) => {
if (target.match(/^[&!]?\d+$/)) {
return `@${target}`;
} else {
@ -108,29 +118,18 @@ class APIMessage {
const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false;
const splitOptions = isSplit ? { ...this.options.split } : undefined;
let mentionPart = '';
if (this.options.reply && !this.isUser && this.target.type !== 'dm') {
const id = this.target.client.users.resolveID(this.options.reply);
mentionPart = `<@${this.options.reply instanceof GuildMember && this.options.reply.nickname ? '!' : ''}${id}>, `;
if (isSplit) {
splitOptions.prepend = `${mentionPart}${splitOptions.prepend || ''}`;
}
}
if (content || mentionPart) {
if (content) {
if (isCode) {
const codeName = typeof this.options.code === 'string' ? this.options.code : '';
content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content || '')}\n\`\`\``;
content = `\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content)}\n\`\`\``;
if (isSplit) {
splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`;
splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`;
}
} else if (mentionPart) {
content = `${mentionPart}${content || ''}`;
}
if (isSplit) {
content = Util.splitMessage(content || '', splitOptions);
content = Util.splitMessage(content, splitOptions);
}
}
@ -149,8 +148,11 @@ class APIMessage {
let nonce;
if (typeof this.options.nonce !== 'undefined') {
nonce = parseInt(this.options.nonce);
if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE');
nonce = this.options.nonce;
// eslint-disable-next-line max-len
if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') {
throw new RangeError('MESSAGE_NONCE_TYPE');
}
}
const embedLikes = [];
@ -174,6 +176,29 @@ class APIMessage {
if (this.isMessage) {
// eslint-disable-next-line eqeqeq
flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield;
} else if (this.isInteraction) {
flags = this.options.ephemeral ? MessageFlags.EPHEMERAL : undefined;
}
let allowedMentions =
typeof this.options.allowedMentions === 'undefined'
? this.target.client.options.allowedMentions
: this.options.allowedMentions;
if (allowedMentions) {
allowedMentions = Util.cloneObject(allowedMentions);
allowedMentions.replied_user = allowedMentions.repliedUser;
delete allowedMentions.repliedUser;
}
let message_reference;
if (typeof this.options.replyTo !== 'undefined') {
const message_id = this.isMessage
? this.target.channel.messages.resolveID(this.options.replyTo)
: this.target.messages.resolveID(this.options.replyTo);
if (message_id) {
message_reference = { message_id };
}
}
this.data = {
@ -184,8 +209,10 @@ class APIMessage {
embeds,
username,
avatar_url: avatarURL,
allowed_mentions: this.options.allowedMentions,
allowed_mentions:
typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
flags,
message_reference,
};
return this;
}
@ -239,8 +266,8 @@ class APIMessage {
data = { ...this.data, content: this.data.content[i] };
opt = { ...this.options, content: this.data.content[i] };
} else {
data = { content: this.data.content[i], tts: this.data.tts };
opt = { content: this.data.content[i], tts: this.data.tts };
data = { content: this.data.content[i], tts: this.data.tts, allowed_mentions: this.options.allowedMentions };
opt = { content: this.data.content[i], tts: this.data.tts, allowedMentions: this.options.allowedMentions };
}
const apiMessage = new APIMessage(this.target, opt);

View file

@ -0,0 +1,102 @@
'use strict';
const Base = require('./Base');
const { ApplicationCommandOptionType } = require('../util/Constants');
const Snowflake = require('../util/Snowflake');
/**
* Represents an application command, see {@link InteractionClient}.
* @extends {Base}
*/
class ApplicationCommand extends Base {
constructor(client, data, guildID) {
super(client);
/**
* The ID of the guild this command is part of, if any.
* @type {Snowflake?}
* @readonly
*/
this.guildID = guildID || null;
this._patch(data);
}
_patch(data) {
/**
* The ID of this command.
* @type {Snowflake}
* @readonly
*/
this.id = data.id;
/**
* The ID of the application which owns this command.
* @type {Snowflake}
* @readonly
*/
this.appplicationID = data.application_id;
/**
* The name of this command.
* @type {string}
* @readonly
*/
this.name = data.name;
/**
* The description of this command.
* @type {string}
* @readonly
*/
this.description = data.description;
/**
* The options of this command.
* @type {Object[]}
* @readonly
*/
this.options = data.options?.map(function m(o) {
return {
type: ApplicationCommandOptionType[o.type],
name: o.name,
description: o.description,
default: o.default,
required: o.required,
choices: o.choices,
options: o.options ? o.options.map(m) : undefined,
};
});
}
/**
* The timestamp the command was created at.
* @type {number}
* @readonly
*/
get createdTimestamp() {
return Snowflake.deconstruct(this.id).timestamp;
}
/**
* The time the command was created at.
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* Delete this command.
*/
async delete() {
let path = this.client.api.applications('@me');
if (this.guildID) {
path = path.guilds(this.guildID);
}
await path.commands(this.id).delete();
}
}
module.exports = ApplicationCommand;

View file

@ -4,6 +4,7 @@ const Util = require('../util/Util');
/**
* Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models).
* @abstract
*/
class Base {
constructor(client) {

View file

@ -5,6 +5,7 @@ const Emoji = require('./Emoji');
/**
* Parent class for {@link GuildEmoji} and {@link GuildPreviewEmoji}.
* @extends {Emoji}
* @abstract
*/
class BaseGuildEmoji extends Emoji {
constructor(client, data, guild) {
@ -16,6 +17,10 @@ class BaseGuildEmoji extends Emoji {
*/
this.guild = guild;
this.requiresColons = null;
this.managed = null;
this.available = null;
/**
* Array of role ids this emoji is active for
* @name BaseGuildEmoji#_roles
@ -30,26 +35,29 @@ class BaseGuildEmoji extends Emoji {
_patch(data) {
if (data.name) this.name = data.name;
/**
* Whether or not this emoji requires colons surrounding it
* @type {boolean}
* @name GuildEmoji#requiresColons
*/
if (typeof data.require_colons !== 'undefined') this.requiresColons = data.require_colons;
if (typeof data.require_colons !== 'undefined') {
/**
* Whether or not this emoji requires colons surrounding it
* @type {?boolean}
*/
this.requiresColons = data.require_colons;
}
/**
* Whether this emoji is managed by an external service
* @type {boolean}
* @name GuildEmoji#managed
*/
if (typeof data.managed !== 'undefined') this.managed = data.managed;
if (typeof data.managed !== 'undefined') {
/**
* Whether this emoji is managed by an external service
* @type {?boolean}
*/
this.managed = data.managed;
}
/**
* Whether this emoji is available
* @type {boolean}
* @name GuildEmoji#available
*/
if (typeof data.available !== 'undefined') this.available = data.available;
if (typeof data.available !== 'undefined') {
/**
* Whether this emoji is available
* @type {?boolean}
*/
this.available = data.available;
}
if (data.roles) this._roles = data.roles;
}

View file

@ -9,7 +9,7 @@ const GuildChannel = require('./GuildChannel');
class CategoryChannel extends GuildChannel {
/**
* Channels that are a part of this category
* @type {?Collection<Snowflake, GuildChannel>}
* @type {Collection<Snowflake, GuildChannel>}
* @readonly
*/
get children() {

View file

@ -7,6 +7,7 @@ const Snowflake = require('../util/Snowflake');
/**
* Represents any channel on Discord.
* @extends {Base}
* @abstract
*/
class Channel extends Base {
constructor(client, data) {
@ -90,10 +91,19 @@ class Channel extends Base {
/**
* Fetches this channel.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Channel>}
*/
fetch() {
return this.client.channels.fetch(this.id, true);
fetch(force = false) {
return this.client.channels.fetch(this.id, true, force);
}
/**
* Indicates whether this channel is text-based.
* @returns {boolean}
*/
isText() {
return 'messages' in this;
}
static create(client, data, guild) {

View file

@ -1,46 +1,15 @@
'use strict';
const Base = require('./Base');
const Team = require('./Team');
const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants');
const Snowflake = require('../util/Snowflake');
const AssetTypes = Object.keys(ClientApplicationAssetTypes);
const Application = require('./interfaces/Application');
/**
* Represents a Client OAuth2 Application.
* @extends {Base}
* @extends {Application}
*/
class ClientApplication extends Base {
constructor(client, data) {
super(client);
this._patch(data);
}
class ClientApplication extends Application {
_patch(data) {
/**
* The ID of the app
* @type {Snowflake}
*/
this.id = data.id;
/**
* The name of the app
* @type {string}
*/
this.name = data.name;
/**
* The app's description
* @type {string}
*/
this.description = data.description;
/**
* The app's icon hash
* @type {string}
*/
this.icon = data.icon;
super._patch(data);
/**
* The app's cover image
@ -72,85 +41,6 @@ class ClientApplication extends Base {
*/
this.owner = data.team ? new Team(this.client, data.team) : data.owner ? this.client.users.add(data.owner) : null;
}
/**
* The timestamp the app was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return Snowflake.deconstruct(this.id).timestamp;
}
/**
* The time the app was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* A link to the application's icon.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string} URL to the icon
*/
iconURL({ format, size } = {}) {
if (!this.icon) return null;
return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size });
}
/**
* A link to this application's cover image.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string} URL to the cover image
*/
coverImage({ format, size } = {}) {
if (!this.cover) return null;
return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size });
}
/**
* Asset data.
* @typedef {Object} ClientAsset
* @property {Snowflake} id The asset ID
* @property {string} name The asset name
* @property {string} type The asset type
*/
/**
* Gets the clients rich presence assets.
* @returns {Promise<Array<ClientAsset>>}
*/
fetchAssets() {
return this.client.api.oauth2
.applications(this.id)
.assets.get()
.then(assets =>
assets.map(a => ({
id: a.id,
name: a.name,
type: AssetTypes[a.type - 1],
})),
);
}
/**
* When concatenated with a string, this automatically returns the application's name instead of the
* ClientApplication object.
* @returns {string}
* @example
* // Logs: Application name: My App
* console.log(`Application name: ${application}`);
*/
toString() {
return this.name;
}
toJSON() {
return super.toJSON({ createdTimestamp: true });
}
}
module.exports = ClientApplication;

View file

@ -94,11 +94,9 @@ class ClientUser extends Structures.get('User') {
* @property {PresenceStatusData} [status] Status of the user
* @property {boolean} [afk] Whether the user is AFK
* @property {Object} [activity] Activity the user is playing
* @property {Object|string} [activity.application] An application object or application id
* @property {string} [activity.application.id] The id of the application
* @property {string} [activity.name] Name of the activity
* @property {ActivityType|number} [activity.type] Type of the activity
* @property {string} [activity.url] Stream url
* @property {string} [activity.url] Twitch / YouTube stream URL
* @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on
*/
@ -141,10 +139,10 @@ class ClientUser extends Structures.get('User') {
}
/**
* Options for setting an activity
* Options for setting an activity.
* @typedef ActivityOptions
* @type {Object}
* @property {string} [url] Twitch stream URL
* @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
*/

View file

@ -61,10 +61,11 @@ class DMChannel extends Channel {
/**
* Fetch this DMChannel.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<DMChannel>}
*/
fetch() {
return this.recipient.createDM();
fetch(force = false) {
return this.recipient.createDM(force);
}
/**

View file

@ -82,7 +82,7 @@ class Emoji extends Base {
* @example
* // Send a custom emoji from a guild:
* const emoji = guild.emojis.cache.first();
* msg.reply(`Hello! ${emoji}`);
* msg.channel.send(`Hello! ${emoji}`);
* @example
* // Send the emoji used in a reaction to the channel the reaction is part of
* reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`);

View file

@ -1,12 +1,15 @@
'use strict';
const { deprecate } = require('util');
const Base = require('./Base');
const GuildAuditLogs = require('./GuildAuditLogs');
const GuildPreview = require('./GuildPreview');
const GuildTemplate = require('./GuildTemplate');
const Integration = require('./Integration');
const Invite = require('./Invite');
const VoiceRegion = require('./VoiceRegion');
const Webhook = require('./Webhook');
const { Error, TypeError } = require('../errors');
const GuildChannelManager = require('../managers/GuildChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildMemberManager = require('../managers/GuildMemberManager');
@ -15,6 +18,7 @@ const RoleManager = require('../managers/RoleManager');
const VoiceStateManager = require('../managers/VoiceStateManager');
const Collection = require('../util/Collection');
const {
browser,
ChannelTypes,
DefaultMessageNotifications,
PartialTypes,
@ -129,11 +133,17 @@ class Guild extends Base {
this.icon = data.icon;
/**
* The hash of the guild splash image (VIP only)
* The hash of the guild invite splash image
* @type {?string}
*/
this.splash = data.splash;
/**
* The hash of the guild discovery splash image
* @type {?string}
*/
this.discoverySplash = data.discovery_splash;
/**
* The region the guild is located in
* @type {string}
@ -147,7 +157,7 @@ class Guild extends Base {
this.memberCount = data.member_count || this.memberCount;
/**
* Whether the guild is "large" (has more than 250 members)
* Whether the guild is "large" (has more than large_threshold members, 50 by default)
* @type {boolean}
*/
this.large = Boolean('large' in data ? data.large : this.large);
@ -157,15 +167,17 @@ class Guild extends Base {
* * ANIMATED_ICON
* * BANNER
* * COMMERCE
* * COMMUNITY
* * DISCOVERABLE
* * FEATURABLE
* * INVITE_SPLASH
* * PUBLIC
* * NEWS
* * PARTNERED
* * RELAY_ENABLED
* * VANITY_URL
* * VERIFIED
* * VIP_REGIONS
* * WELCOME_SCREEN_ENABLED
* @typedef {string} Features
*/
@ -202,6 +214,7 @@ class Guild extends Base {
/**
* Whether embedded images are enabled on this guild
* @type {boolean}
* @deprecated
*/
this.embedEnabled = data.embed_enabled;
@ -220,35 +233,38 @@ class Guild extends Base {
*/
this.premiumTier = data.premium_tier;
/**
* The total number of users currently boosting this server
* @type {?number}
* @name Guild#premiumSubscriptionCount
*/
if (typeof data.premium_subscription_count !== 'undefined') {
/**
* The total number of boosts for this server
* @type {?number}
*/
this.premiumSubscriptionCount = data.premium_subscription_count;
}
/**
* Whether widget images are enabled on this guild
* @type {?boolean}
* @name Guild#widgetEnabled
*/
if (typeof data.widget_enabled !== 'undefined') this.widgetEnabled = data.widget_enabled;
if (typeof data.widget_enabled !== 'undefined') {
/**
* Whether widget images are enabled on this guild
* @type {?boolean}
*/
this.widgetEnabled = data.widget_enabled;
}
/**
* The widget channel ID, if enabled
* @type {?string}
* @name Guild#widgetChannelID
*/
if (typeof data.widget_channel_id !== 'undefined') this.widgetChannelID = data.widget_channel_id;
if (typeof data.widget_channel_id !== 'undefined') {
/**
* The widget channel ID, if enabled
* @type {?string}
*/
this.widgetChannelID = data.widget_channel_id;
}
/**
* The embed channel ID, if enabled
* @type {?string}
* @name Guild#embedChannelID
*/
if (typeof data.embed_channel_id !== 'undefined') this.embedChannelID = data.embed_channel_id;
if (typeof data.embed_channel_id !== 'undefined') {
/**
* The embed channel ID, if enabled
* @type {?string}
* @deprecated
*/
this.embedChannelID = data.embed_channel_id;
}
/**
* The verification level of the guild
@ -287,28 +303,64 @@ class Guild extends Base {
*/
this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze();
/**
* The maximum amount of members the guild can have
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
* @name Guild#maximumMembers
*/
if (typeof data.max_members !== 'undefined') this.maximumMembers = data.max_members || 250000;
if (typeof data.max_members !== 'undefined') {
/**
* The maximum amount of members the guild can have
* @type {?number}
*/
this.maximumMembers = data.max_members;
} else if (typeof this.maximumMembers === 'undefined') {
this.maximumMembers = null;
}
if (typeof data.max_presences !== 'undefined') {
/**
* The maximum amount of presences the guild can have
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
*/
this.maximumPresences = data.max_presences || 25000;
} else if (typeof this.maximumPresences === 'undefined') {
this.maximumPresences = null;
}
if (typeof data.approximate_member_count !== 'undefined') {
/**
* The approximate amount of members the guild has
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
*/
this.approximateMemberCount = data.approximate_member_count;
} else if (typeof this.approximateMemberCount === 'undefined') {
this.approximateMemberCount = null;
}
if (typeof data.approximate_presence_count !== 'undefined') {
/**
* The approximate amount of presences the guild has
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
*/
this.approximatePresenceCount = data.approximate_presence_count;
} else if (typeof this.approximatePresenceCount === 'undefined') {
this.approximatePresenceCount = null;
}
/**
* The maximum amount of presences the guild can have
* <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info>
* @type {?number}
* @name Guild#maximumPresences
*/
if (typeof data.max_presences !== 'undefined') this.maximumPresences = data.max_presences || 25000;
/**
* The vanity URL code of the guild, if any
* The vanity invite code of the guild, if any
* @type {?string}
*/
this.vanityURLCode = data.vanity_url_code;
/* eslint-disable max-len */
/**
* The use count of the vanity URL code of the guild, if any
* <info>You will need to fetch this parameter using {@link Guild#fetchVanityData} if you want to receive it</info>
* @type {?number}
*/
this.vanityURLUses = null;
/* eslint-enable max-len */
/**
* The description of the guild, if any
* @type {?string}
@ -327,18 +379,22 @@ class Guild extends Base {
/**
* The ID of the rules channel for the guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?Snowflake}
*/
this.rulesChannelID = data.rules_channel_id;
/**
* The ID of the public updates channel for the guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* The ID of the community updates channel for the guild
* @type {?Snowflake}
*/
this.publicUpdatesChannelID = data.public_updates_channel_id;
/**
* The preferred locale of the guild, defaults to `en-US`
* @type {string}
*/
this.preferredLocale = data.preferred_locale;
if (data.channels) {
this.channels.cache.clear();
for (const rawChannel of data.channels) {
@ -463,11 +519,14 @@ class Guild extends Base {
* @readonly
*/
get nameAcronym() {
return this.name.replace(/\w+/g, name => name[0]).replace(/\s/g, '');
return this.name
.replace(/'s /g, ' ')
.replace(/\w+/g, e => e[0])
.replace(/\s/g, '');
}
/**
* The URL to this guild's splash.
* The URL to this guild's invite splash image.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
@ -476,6 +535,16 @@ class Guild extends Base {
return this.client.rest.cdn.Splash(this.id, this.splash, format, size);
}
/**
* The URL to this guild's discovery splash image.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
discoverySplashURL({ format, size } = {}) {
if (!this.discoverySplash) return null;
return this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, format, size);
}
/**
* The owner of the guild
* @type {?GuildMember}
@ -521,6 +590,7 @@ class Guild extends Base {
* Embed channel for this guild
* @type {?TextChannel}
* @readonly
* @deprecated
*/
get embedChannel() {
return this.client.channels.cache.get(this.embedChannelID) || null;
@ -528,7 +598,6 @@ class Guild extends Base {
/**
* Rules channel for this guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?TextChannel}
* @readonly
*/
@ -538,7 +607,6 @@ class Guild extends Base {
/**
* Public updates channel for this guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?TextChannel}
* @readonly
*/
@ -569,18 +637,6 @@ class Guild extends Base {
return this.voiceStates.cache.get(this.client.user.id);
}
/**
* Returns the GuildMember form of a User object, if the user is present in the guild.
* @param {UserResolvable} user The user that you want to obtain the GuildMember of
* @returns {?GuildMember}
* @example
* // Get the guild member of a user
* const member = guild.member(message.author);
*/
member(user) {
return this.members.resolve(user);
}
/**
* Fetches this guild.
* @returns {Promise<Guild>}
@ -588,7 +644,7 @@ class Guild extends Base {
fetch() {
return this.client.api
.guilds(this.id)
.get()
.get({ query: { with_counts: true } })
.then(data => {
this._patch(data);
return this;
@ -642,6 +698,8 @@ class Guild extends Base {
/**
* Fetches a collection of integrations to this guild.
* Resolves with a collection mapping integrations by their ids.
* @param {Object} [options] Options for fetching integrations
* @param {boolean} [options.includeApplications] Whether to include bot and Oauth2 webhook integrations
* @returns {Promise<Collection<string, Integration>>}
* @example
* // Fetch integrations
@ -649,10 +707,14 @@ class Guild extends Base {
* .then(integrations => console.log(`Fetched ${integrations.size} integrations`))
* .catch(console.error);
*/
fetchIntegrations() {
fetchIntegrations({ includeApplications = false } = {}) {
return this.client.api
.guilds(this.id)
.integrations.get()
.integrations.get({
query: {
include_applications: includeApplications,
},
})
.then(data =>
data.reduce(
(collection, integration) => collection.set(integration.id, new Integration(this.client, integration, this)),
@ -661,6 +723,20 @@ class Guild extends Base {
);
}
/**
* Fetches a collection of templates from this guild.
* Resolves with a collection mapping templates by their codes.
* @returns {Promise<Collection<string, GuildTemplate>>}
*/
fetchTemplates() {
return this.client.api
.guilds(this.id)
.templates.get()
.then(templates =>
templates.reduce((col, data) => col.set(data.code, new GuildTemplate(this.client, data)), new Collection()),
);
}
/**
* The data for creating an integration.
* @typedef {Object} IntegrationData
@ -681,6 +757,19 @@ class Guild extends Base {
.then(() => this);
}
/**
* Creates a template for the guild.
* @param {string} name The name for the template
* @param {string} [description] The description for the template
* @returns {Promise<GuildTemplate>}
*/
createTemplate(name, description) {
return this.client.api
.guilds(this.id)
.templates.post({ data: { name, description } })
.then(data => new GuildTemplate(this.client, data));
}
/**
* Fetches a collection of invites to this guild.
* Resolves with a collection mapping invites by their codes.
@ -711,7 +800,7 @@ class Guild extends Base {
}
/**
* Obtains a guild preview for this guild from Discord, only available for public guilds.
* Obtains a guild preview for this guild from Discord.
* @returns {Promise<GuildPreview>}
*/
fetchPreview() {
@ -725,6 +814,7 @@ class Guild extends Base {
* Fetches the vanity url invite code to this guild.
* Resolves with a string matching the vanity url invite code, not the full url.
* @returns {Promise<string>}
* @deprecated
* @example
* // Fetch invites
* guild.fetchVanityCode()
@ -734,13 +824,36 @@ class Guild extends Base {
* .catch(console.error);
*/
fetchVanityCode() {
return this.fetchVanityData().then(vanity => vanity.code);
}
/**
* An object containing information about a guild's vanity invite.
* @typedef {Object} Vanity
* @property {?string} code Vanity invite code
* @property {?number} uses How many times this invite has been used
*/
/**
* Fetches the vanity url invite object to this guild.
* Resolves with an object containing the vanity url invite code and the use count
* @returns {Promise<Vanity>}
* @example
* // Fetch invite data
* guild.fetchVanityData()
* .then(res => {
* console.log(`Vanity URL: https://discord.gg/${res.code} with ${res.uses} uses`);
* })
* .catch(console.error);
*/
async fetchVanityData() {
if (!this.features.includes('VANITY_URL')) {
return Promise.reject(new Error('VANITY_URL'));
throw new Error('VANITY_URL');
}
return this.client.api
.guilds(this.id, 'vanity-url')
.get()
.then(res => res.code);
const data = await this.client.api.guilds(this.id, 'vanity-url').get();
this.vanityURLUses = data.uses;
return data;
}
/**
@ -779,15 +892,23 @@ class Guild extends Base {
}
/**
* The Guild Embed object
* @typedef {Object} GuildEmbedData
* @property {boolean} enabled Whether the embed is enabled
* @property {?GuildChannel} channel The embed channel
* Data for the Guild Widget object
* @typedef {Object} GuildWidget
* @property {boolean} enabled Whether the widget is enabled
* @property {?GuildChannel} channel The widget channel
*/
/**
* The Guild Widget object
* @typedef {Object} GuildWidgetData
* @property {boolean} enabled Whether the widget is enabled
* @property {?GuildChannelResolvable} channel The widget channel
*/
/**
* Fetches the guild embed.
* @returns {Promise<GuildEmbedData>}
* @returns {Promise<GuildWidget>}
* @deprecated
* @example
* // Fetches the guild embed
* guild.fetchEmbed()
@ -795,13 +916,26 @@ class Guild extends Base {
* .catch(console.error);
*/
fetchEmbed() {
return this.client.api
.guilds(this.id)
.embed.get()
.then(data => ({
enabled: data.enabled,
channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null,
}));
return this.fetchWidget();
}
/**
* Fetches the guild widget.
* @returns {Promise<GuildWidget>}
* @example
* // Fetches the guild widget
* guild.fetchWidget()
* .then(widget => console.log(`The widget is ${widget.enabled ? 'enabled' : 'disabled'}`))
* .catch(console.error);
*/
async fetchWidget() {
const data = await this.client.api.guilds(this.id).widget.get();
this.widgetEnabled = this.embedEnabled = data.enabled;
this.widgetChannelID = this.embedChannelID = data.channel_id;
return {
enabled: data.enabled,
channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null,
};
}
/**
@ -848,29 +982,25 @@ class Guild extends Base {
* @param {boolean} [options.deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`)
* @returns {Promise<GuildMember>}
*/
addMember(user, options) {
async addMember(user, options) {
user = this.client.users.resolveID(user);
if (!user) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable'));
if (this.members.cache.has(user)) return Promise.resolve(this.members.cache.get(user));
if (!user) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
if (this.members.cache.has(user)) return this.members.cache.get(user);
options.access_token = options.accessToken;
if (options.roles) {
const roles = [];
for (let role of options.roles instanceof Collection ? options.roles.values() : options.roles) {
role = this.roles.resolve(role);
if (!role) {
return Promise.reject(
new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true),
);
throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true);
}
roles.push(role.id);
}
options.roles = roles;
}
return this.client.api
.guilds(this.id)
.members(user)
.put({ data: options })
.then(data => this.members.add(data));
const data = await this.client.api.guilds(this.id).members(user).put({ data: options });
// Data is an empty buffer if the member is already part of the guild.
return data instanceof (browser ? ArrayBuffer : Buffer) ? this.members.fetch(user) : this.members.add(data);
}
/**
@ -885,10 +1015,14 @@ class Guild extends Base {
* @property {number} [afkTimeout] The AFK timeout of the guild
* @property {Base64Resolvable} [icon] The icon of the guild
* @property {GuildMemberResolvable} [owner] The owner of the guild
* @property {Base64Resolvable} [splash] The splash screen of the guild
* @property {Base64Resolvable} [splash] The invite splash image of the guild
* @property {Base64Resolvable} [discoverySplash] The discovery splash image of the guild
* @property {Base64Resolvable} [banner] The banner of the guild
* @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild
* @property {ChannelResolvable} [rulesChannel] The rules channel of the guild
* @property {ChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild
* @property {string} [preferredLocale] The preferred locale of the guild
*/
/**
@ -925,6 +1059,7 @@ class Guild extends Base {
if (typeof data.icon !== 'undefined') _data.icon = data.icon;
if (data.owner) _data.owner_id = this.client.users.resolveID(data.owner);
if (data.splash) _data.splash = data.splash;
if (data.discoverySplash) _data.discovery_splash = data.discoverySplash;
if (data.banner) _data.banner = data.banner;
if (typeof data.explicitContentFilter !== 'undefined') {
_data.explicit_content_filter =
@ -941,6 +1076,13 @@ class Guild extends Base {
if (typeof data.systemChannelFlags !== 'undefined') {
_data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags);
}
if (typeof data.rulesChannel !== 'undefined') {
_data.rules_channel_id = this.client.channels.resolveID(data.rulesChannel);
}
if (typeof data.publicUpdatesChannel !== 'undefined') {
_data.public_updates_channel_id = this.client.channels.resolveID(data.publicUpdatesChannel);
}
if (data.preferredLocale) _data.preferred_locale = data.preferredLocale;
return this.client.api
.guilds(this.id)
.patch({ data: _data, reason })
@ -987,7 +1129,7 @@ class Guild extends Base {
* @example
* // Edit the guild name
* guild.setName('Discord Guild')
* .then(updated => console.log(`Updated guild name to ${guild}`))
* .then(updated => console.log(`Updated guild name to ${updated.name}`))
* .catch(console.error);
*/
setName(name, reason) {
@ -1100,9 +1242,9 @@ class Guild extends Base {
}
/**
* Sets a new guild splash screen.
* @param {Base64Resolvable|BufferResolvable} splash The new splash screen of the guild
* @param {string} [reason] Reason for changing the guild's splash screen
* Sets a new guild invite splash image.
* @param {Base64Resolvable|BufferResolvable} splash The new invite splash image of the guild
* @param {string} [reason] Reason for changing the guild's invite splash image
* @returns {Promise<Guild>}
* @example
* // Edit the guild splash
@ -1114,6 +1256,21 @@ class Guild extends Base {
return this.edit({ splash: await DataResolver.resolveImage(splash), reason });
}
/**
* Sets a new guild discovery splash image.
* @param {Base64Resolvable|BufferResolvable} discoverySplash The new discovery splash image of the guild
* @param {string} [reason] Reason for changing the guild's discovery splash image
* @returns {Promise<Guild>}
* @example
* // Edit the guild discovery splash
* guild.setDiscoverySplash('./discoverysplash.png')
* .then(updated => console.log('Updated the guild discovery splash'))
* .catch(console.error);
*/
async setDiscoverySplash(discoverySplash, reason) {
return this.edit({ discoverySplash: await DataResolver.resolveImage(discoverySplash), reason });
}
/**
* Sets a new guild banner.
* @param {Base64Resolvable|BufferResolvable} banner The new banner of the guild
@ -1128,6 +1285,51 @@ class Guild extends Base {
return this.edit({ banner: await DataResolver.resolveImage(banner), reason });
}
/**
* Edits the rules channel of the guild.
* @param {ChannelResolvable} rulesChannel The new rules channel
* @param {string} [reason] Reason for changing the guild's rules channel
* @returns {Promise<Guild>}
* @example
* // Edit the guild rules channel
* guild.setRulesChannel(channel)
* .then(updated => console.log(`Updated guild rules channel to ${guild.rulesChannel.name}`))
* .catch(console.error);
*/
setRulesChannel(rulesChannel, reason) {
return this.edit({ rulesChannel }, reason);
}
/**
* Edits the community updates channel of the guild.
* @param {ChannelResolvable} publicUpdatesChannel The new community updates channel
* @param {string} [reason] Reason for changing the guild's community updates channel
* @returns {Promise<Guild>}
* @example
* // Edit the guild community updates channel
* guild.setPublicUpdatesChannel(channel)
* .then(updated => console.log(`Updated guild community updates channel to ${guild.publicUpdatesChannel.name}`))
* .catch(console.error);
*/
setPublicUpdatesChannel(publicUpdatesChannel, reason) {
return this.edit({ publicUpdatesChannel }, reason);
}
/**
* Edits the 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
* // Edit the guild preferred locale
* guild.setPreferredLocale('en-US')
* .then(updated => console.log(`Updated guild preferred locale to ${guild.preferredLocale}`))
* .catch(console.error);
*/
setPreferredLocale(preferredLocale, reason) {
return this.edit({ preferredLocale }, reason);
}
/**
* The data needed for updating a channel's position.
* @typedef {Object} ChannelPosition
@ -1202,23 +1404,51 @@ class Guild extends Base {
/**
* Edits the guild's embed.
* @param {GuildEmbedData} embed The embed for the guild
* @param {GuildWidgetData} embed The embed for the guild
* @param {string} [reason] Reason for changing the guild's embed
* @returns {Promise<Guild>}
* @deprecated
*/
setEmbed(embed, reason) {
return this.setWidget(embed, reason);
}
/**
* Edits the guild's widget.
* @param {GuildWidgetData} widget The widget for the guild
* @param {string} [reason] Reason for changing the guild's widget
* @returns {Promise<Guild>}
*/
setWidget(widget, reason) {
return this.client.api
.guilds(this.id)
.embed.patch({
.widget.patch({
data: {
enabled: embed.enabled,
channel_id: this.channels.resolveID(embed.channel),
enabled: widget.enabled,
channel_id: this.channels.resolveID(widget.channel),
},
reason,
})
.then(() => this);
}
/**
* Get the commands associated with this guild.
* @returns {ApplicationCommand[]} A list of commands.
*/
getCommands() {
return this.client.interactionClient.getCommands(this.id);
}
/**
* Create a command. See {@link InteractionClient}.
* @param {Object} command The command description.
* @returns {ApplicationCommand} The created command.
*/
createCommand(command) {
return this.client.interactionClient.createCommand(command, this.id);
}
/**
* Leaves the guild.
* @returns {Promise<Guild>}
@ -1267,6 +1497,7 @@ class Guild extends Base {
this.id === guild.id &&
this.available === guild.available &&
this.splash === guild.splash &&
this.discoverySplash === guild.discoverySplash &&
this.region === guild.region &&
this.name === guild.name &&
this.memberCount === guild.memberCount &&
@ -1311,6 +1542,7 @@ class Guild extends Base {
});
json.iconURL = this.iconURL();
json.splashURL = this.splashURL();
json.discoverySplashURL = this.discoverySplashURL();
json.bannerURL = this.bannerURL();
return json;
}
@ -1333,9 +1565,24 @@ class Guild extends Base {
_sortedChannels(channel) {
const category = channel.type === ChannelTypes.CATEGORY;
return Util.discordSort(
this.channels.cache.filter(c => c.type === channel.type && (category || c.parent === channel.parent)),
this.channels.cache.filter(
c =>
(['text', 'news', 'store'].includes(channel.type)
? ['text', 'news', 'store'].includes(c.type)
: c.type === channel.type) &&
(category || c.parent === channel.parent),
),
);
}
}
Guild.prototype.setEmbed = deprecate(Guild.prototype.setEmbed, 'Guild#setEmbed: Use setWidget instead');
Guild.prototype.fetchEmbed = deprecate(Guild.prototype.fetchEmbed, 'Guild#fetchEmbed: Use fetchWidget instead');
Guild.prototype.fetchVanityCode = deprecate(
Guild.prototype.fetchVanityCode,
'Guild#fetchVanityCode: Use fetchVanityData() instead',
);
module.exports = Guild;

View file

@ -24,7 +24,7 @@ const Util = require('../util/Util');
/**
* Key mirror of all available audit log targets.
* @name GuildAuditLogs.Targets
* @type {AuditLogTargetType}
* @type {Object<string, string>}
*/
const Targets = {
ALL: 'ALL',
@ -84,7 +84,7 @@ const Targets = {
/**
* All available actions keyed under their names to their numeric values.
* @name GuildAuditLogs.Actions
* @type {AuditLogAction}
* @type {Object<string, number>}
*/
const Actions = {
ALL: null,

View file

@ -17,6 +17,7 @@ const Util = require('../util/Util');
* - {@link NewsChannel}
* - {@link StoreChannel}
* @extends {Channel}
* @abstract
*/
class GuildChannel extends Channel {
/**
@ -52,7 +53,7 @@ class GuildChannel extends Channel {
* The ID of the category parent of this channel
* @type {?Snowflake}
*/
this.parentID = data.parent_id;
this.parentID = data.parent_id || null;
/**
* A map of permission overwrites in this channel for roles and users
@ -227,7 +228,7 @@ class GuildChannel extends Channel {
*/
updateOverwrite(userOrRole, options, reason) {
userOrRole = this.guild.roles.resolve(userOrRole) || this.client.users.resolve(userOrRole);
if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true));
if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role'));
const existing = this.permissionOverwrites.get(userOrRole.id);
if (existing) return existing.update(options, reason).then(() => this);
@ -250,7 +251,7 @@ class GuildChannel extends Channel {
*/
createOverwrite(userOrRole, options, reason) {
userOrRole = this.guild.roles.resolve(userOrRole) || this.client.users.resolve(userOrRole);
if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true));
if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role'));
const type = userOrRole instanceof Role ? 'role' : 'member';
const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options);
@ -298,7 +299,7 @@ class GuildChannel extends 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 {Snowflake} [parentID] The parent ID of the channel
* @property {?Snowflake} [parentID] The parent ID 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]
@ -334,8 +335,22 @@ class GuildChannel extends Channel {
});
}
const permission_overwrites =
data.permissionOverwrites && data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
let permission_overwrites;
if (data.permissionOverwrites) {
permission_overwrites = data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
if (data.lockPermissions) {
if (data.parentID) {
const newParent = this.guild.channels.resolve(data.parentID);
if (newParent && newParent.type === 'category') {
permission_overwrites = newParent.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
} else if (this.parent) {
permission_overwrites = this.parent.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild));
}
}
const newData = await this.client.api.channels(this.id).patch({
data: {
@ -398,7 +413,7 @@ class GuildChannel extends Channel {
/**
* Sets a new topic for the guild channel.
* @param {string} topic The 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<GuildChannel>}
* @example
@ -499,7 +514,7 @@ class GuildChannel extends Channel {
* @param {boolean} [options.nsfw=this.nsfw] Whether the new channel is nsfw (only text)
* @param {number} [options.bitrate=this.bitrate] Bitrate of the new channel in bits (only voice)
* @param {number} [options.userLimit=this.userLimit] Maximum amount of users allowed in the new channel (only voice)
* @param {number} [options.rateLimitPerUser=ThisType.rateLimitPerUser] Ratelimit per user for the new channel (only text)
* @param {number} [options.rateLimitPerUser=this.rateLimitPerUser] Ratelimit per user for the new channel (only text)
* @param {ChannelResolvable} [options.parent=this.parent] Parent of the new channel
* @param {string} [options.reason] Reason for cloning this channel
* @returns {Promise<GuildChannel>}

View file

@ -11,13 +11,19 @@ const Permissions = require('../util/Permissions');
*/
class GuildEmoji extends BaseGuildEmoji {
/**
* @name GuildEmoji
* @kind constructor
* @memberof GuildEmoji
* @param {Client} client The instantiating client
* @param {Object} data The data for the guild emoji
* @param {Guild} guild The guild the guild emoji is part of
*/
constructor(client, data, guild) {
super(client, data, guild);
/**
* The user who created this emoji
* @type {?User}
*/
this.author = null;
}
/**
* The guild this emoji is part of
@ -31,6 +37,11 @@ class GuildEmoji extends BaseGuildEmoji {
return clone;
}
_patch(data) {
super._patch(data);
if (typeof data.user !== 'undefined') this.author = this.client.users.add(data.user);
}
/**
* Whether the emoji is deletable by the client user
* @type {boolean}
@ -54,20 +65,18 @@ class GuildEmoji extends BaseGuildEmoji {
* Fetches the author for this emoji
* @returns {Promise<User>}
*/
fetchAuthor() {
async fetchAuthor() {
if (this.managed) {
return Promise.reject(new Error('EMOJI_MANAGED'));
throw new Error('EMOJI_MANAGED');
} else {
if (!this.guild.me) return Promise.reject(new Error('GUILD_UNCACHED_ME'));
if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME');
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) {
return Promise.reject(new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild));
throw new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild);
}
}
return this.client.api
.guilds(this.guild.id)
.emojis(this.id)
.get()
.then(emoji => this.client.users.add(emoji.user));
const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get();
this._patch(data);
return this.author;
}
/**

View file

@ -1,13 +1,12 @@
'use strict';
const Base = require('./Base');
const { Presence } = require('./Presence');
const Role = require('./Role');
const VoiceState = require('./VoiceState');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { Error } = require('../errors');
const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager');
const Permissions = require('../util/Permissions');
let Structures;
/**
* Represents a member of a guild on Discord.
@ -29,13 +28,6 @@ class GuildMember extends Base {
*/
this.guild = guild;
/**
* The user that this guild member instance represents
* @type {User}
* @name GuildMember#user
*/
if (data.user) this.user = client.users.add(data.user, true);
/**
* The timestamp the member joined the guild at
* @type {?number}
@ -66,23 +58,29 @@ class GuildMember extends Base {
*/
this.deleted = false;
/**
* The nickname of this member, if they have one
* @type {?string}
*/
this.nickname = null;
this._roles = [];
if (data) this._patch(data);
}
_patch(data) {
/**
* The nickname of this member, if they have one
* @type {?string}
* @name GuildMember#nickname
*/
if (typeof data.nick !== 'undefined') this.nickname = data.nick;
if ('user' in data) {
/**
* The user that this guild member instance represents
* @type {User}
*/
this.user = this.client.users.add(data.user, true);
}
if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime();
if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime();
if (data.user) this.user = this.guild.client.users.add(data.user);
if (data.roles) this._roles = data.roles;
if ('nick' in data) this.nickname = data.nick;
if ('joined_at' in data) this.joinedTimestamp = new Date(data.joined_at).getTime();
if ('premium_since' in data) this.premiumSinceTimestamp = new Date(data.premium_since).getTime();
if ('roles' in data) this._roles = data.roles;
}
_clone() {
@ -125,6 +123,8 @@ class GuildMember extends Base {
* @readonly
*/
get voice() {
if (!Structures) Structures = require('../util/Structures');
const VoiceState = Structures.get('VoiceState');
return this.guild.voiceStates.cache.get(this.id) || new VoiceState(this.guild, { user_id: this.id });
}
@ -152,6 +152,8 @@ class GuildMember extends Base {
* @readonly
*/
get presence() {
if (!Structures) Structures = require('../util/Structures');
const Presence = Structures.get('Presence');
return (
this.guild.presences.cache.get(this.id) ||
new Presence(this.client, {
@ -194,7 +196,7 @@ class GuildMember extends Base {
/**
* The nickname of this member, or their username if they don't have one
* @type {string}
* @type {?string}
* @readonly
*/
get displayName() {
@ -265,7 +267,8 @@ class GuildMember extends Base {
*/
hasPermission(permission, { checkAdmin = true, checkOwner = true } = {}) {
if (checkOwner && this.user.id === this.guild.ownerID) return true;
return this.roles.cache.some(r => r.permissions.has(permission, checkAdmin));
const permissions = new Permissions(this.roles.cache.map(role => role.permissions));
return permissions.has(permission, checkAdmin);
}
/**
@ -356,7 +359,7 @@ class GuildMember extends Base {
/**
* Bans this guild member.
* @param {Object} [options] Options for the ban
* @param {number} [options.days=0] Number of days of messages to delete
* @param {number} [options.days=0] Number of days of messages to delete, must be between 0 and 7
* @param {string} [options.reason] Reason for banning
* @returns {Promise<GuildMember>}
* @example
@ -371,10 +374,11 @@ class GuildMember extends Base {
/**
* Fetches this GuildMember.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<GuildMember>}
*/
fetch() {
return this.guild.members.fetch(this.id, true);
fetch(force = false) {
return this.guild.members.fetch({ user: this.id, cache: true, force });
}
/**

View file

@ -5,7 +5,7 @@ const GuildPreviewEmoji = require('./GuildPreviewEmoji');
const Collection = require('../util/Collection');
/**
* Represents the data about the guild any bot can preview, connected to the specified public guild.
* Represents the data about the guild any bot can preview, connected to the specified guild.
* @extends {Base}
*/
class GuildPreview extends Base {
@ -18,37 +18,37 @@ class GuildPreview extends Base {
}
/**
* Builds the public guild with the provided data.
* @param {*} data The raw data of the public guild
* Builds the guild with the provided data.
* @param {*} data The raw data of the guild
* @private
*/
_patch(data) {
/**
* The id of this public guild
* The id of this guild
* @type {string}
*/
this.id = data.id;
/**
* The name of this public guild
* The name of this guild
* @type {string}
*/
this.name = data.name;
/**
* The icon of this public guild
* The icon of this guild
* @type {?string}
*/
this.icon = data.icon;
/**
* The splash icon of this public guild
* The splash icon of this guild
* @type {?string}
*/
this.splash = data.splash;
/**
* The discovery splash icon of this public guild
* The discovery splash icon of this guild
* @type {?string}
*/
this.discoverySplash = data.discovery_splash;
@ -60,26 +60,26 @@ class GuildPreview extends Base {
this.features = data.features;
/**
* The approximate count of members in this public guild
* The approximate count of members in this guild
* @type {number}
*/
this.approximateMemberCount = data.approximate_member_count;
/**
* The approximate count of online members in this public guild
* The approximate count of online members in this guild
* @type {number}
*/
this.approximatePresenceCount = data.approximate_presence_count;
/**
* The description for this public guild
* The description for this guild
* @type {?string}
*/
this.description = data.description;
this.description = data.description || null;
if (!this.emojis) {
/**
* Collection of emojis belonging to this public guild
* Collection of emojis belonging to this guild
* @type {Collection<Snowflake, GuildPreviewEmoji>}
*/
this.emojis = new Collection();
@ -92,7 +92,7 @@ class GuildPreview extends Base {
}
/**
* The URL to this public guild's splash.
* The URL to this guild's splash.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
@ -102,7 +102,7 @@ class GuildPreview extends Base {
}
/**
* The URL to this public guild's discovery splash.
* The URL to this guild's discovery splash.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
@ -112,7 +112,7 @@ class GuildPreview extends Base {
}
/**
* The URL to this public guild's icon.
* The URL to this guild's icon.
* @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string}
*/
@ -122,7 +122,7 @@ class GuildPreview extends Base {
}
/**
* Fetches this public guild.
* Fetches this guild.
* @returns {Promise<GuildPreview>}
*/
fetch() {

View file

@ -0,0 +1,225 @@
'use strict';
const Base = require('./Base');
const { Events } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
/**
* Represents the template for a guild.
* @extends {Base}
*/
class GuildTemplate extends Base {
/**
* @param {Client} client The instantiating client
* @param {Object} data The raw data for the template
*/
constructor(client, data) {
super(client);
this._patch(data);
}
/**
* Builds or updates the template with the provided data.
* @param {Object} data The raw data for the template
* @returns {GuildTemplate}
* @private
*/
_patch(data) {
/**
* The unique code of this template
* @type {string}
*/
this.code = data.code;
/**
* The name of this template
* @type {string}
*/
this.name = data.name;
/**
* The description of this template
* @type {?string}
*/
this.description = data.description;
/**
* The amount of times this template has been used
* @type {number}
*/
this.usageCount = data.usage_count;
/**
* The ID of the user that created this template
* @type {Snowflake}
*/
this.creatorID = data.creator_id;
/**
* The user that created this template
* @type {User}
*/
this.creator = this.client.users.add(data.creator);
/**
* The time of when this template was created at
* @type {Date}
*/
this.createdAt = new Date(data.created_at);
/**
* The time of when this template was last synced to the guild
* @type {Date}
*/
this.updatedAt = new Date(data.updated_at);
/**
* The ID of the guild that this template belongs to
* @type {Snowflake}
*/
this.guildID = data.source_guild_id;
/**
* The data of the guild that this template would create
* @type {Object}
* @see {@link https://discord.com/developers/docs/resources/guild#guild-resource}
*/
this.serializedGuild = data.serialized_source_guild;
/**
* Whether this template has unsynced changes
* @type {?boolean}
*/
this.unSynced = 'is_dirty' in data ? Boolean(data.is_dirty) : null;
return this;
}
/**
* Creates a guild based from this template.
* <warn>This is only available to bots in fewer than 10 guilds.</warn>
* @param {string} name The name of the guild
* @param {BufferResolvable|Base64Resolvable} [icon] The icon for the guild
* @returns {Promise<Guild>}
*/
async createGuild(name, icon) {
const { client } = this;
const data = await client.api.guilds.templates(this.code).post({
data: {
name,
icon: await DataResolver.resolveImage(icon),
},
});
// eslint-disable-next-line consistent-return
return new Promise(resolve => {
const createdGuild = client.guilds.cache.get(data.id);
if (createdGuild) return resolve(createdGuild);
const resolveGuild = guild => {
client.off(Events.GUILD_CREATE, handleGuild);
client.decrementMaxListeners();
resolve(guild);
};
const handleGuild = guild => {
if (guild.id === data.id) {
client.clearTimeout(timeout);
resolveGuild(guild);
}
};
client.incrementMaxListeners();
client.on(Events.GUILD_CREATE, handleGuild);
const timeout = client.setTimeout(() => resolveGuild(client.guilds.add(data)), 10000);
});
}
/**
* Updates the metadata on this template.
* @param {Object} options Options for the template
* @param {string} [options.name] The name of this template
* @param {string} [options.description] The description of this template
* @returns {Promise<GuildTemplate>}
*/
edit({ name, description } = {}) {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.patch({ data: { name, description } })
.then(data => this._patch(data));
}
/**
* Deletes this template.
* @returns {Promise<GuildTemplate>}
*/
delete() {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.delete()
.then(() => this);
}
/**
* Syncs this template to the current state of the guild.
* @returns {Promise<GuildTemplate>}
*/
sync() {
return this.client.api
.guilds(this.guildID)
.templates(this.code)
.put()
.then(data => this._patch(data));
}
/**
* The timestamp of when this template was created at
* @type {number}
* @readonly
*/
get createdTimestamp() {
return this.createdAt.getTime();
}
/**
* The timestamp of when this template was last synced to the guild
* @type {number}
* @readonly
*/
get updatedTimestamp() {
return this.updatedAt.getTime();
}
/**
* The guild that this template belongs to
* @type {?Guild}
* @readonly
*/
get guild() {
return this.client.guilds.cache.get(this.guildID) || null;
}
/**
* The URL of this template
* @type {string}
* @readonly
*/
get url() {
return `${this.client.options.http.template}/${this.code}`;
}
/**
* When concatenated with a string, this automatically returns the templates's code instead of the template object.
* @returns {string}
* @example
* // Logs: Template: FKvmczH2HyUf
* console.log(`Template: ${guildTemplate}!`);
*/
toString() {
return this.code;
}
}
module.exports = GuildTemplate;

View file

@ -1,6 +1,7 @@
'use strict';
const Base = require('./Base');
const IntegrationApplication = require('./IntegrationApplication');
/**
* The information account for an integration
@ -58,11 +59,15 @@ class Integration extends Base {
*/
this.role = this.guild.roles.cache.get(data.role_id);
/**
* The user for this integration
* @type {User}
*/
this.user = this.client.users.add(data.user);
if (data.user) {
/**
* The user for this integration
* @type {?User}
*/
this.user = this.client.users.add(data.user);
} else {
this.user = null;
}
/**
* The account integration information
@ -90,6 +95,20 @@ class Integration extends Base {
* @type {number}
*/
this.expireGracePeriod = data.expire_grace_period;
if ('application' in data) {
if (this.application) {
this.application._patch(data.application);
} else {
/**
* The application for this integration
* @type {?IntegrationApplication}
*/
this.application = new IntegrationApplication(this.client, data.application);
}
} else if (!this.application) {
this.application = null;
}
}
/**

View file

@ -0,0 +1,25 @@
'use strict';
const Application = require('./interfaces/Application');
/**
* Represents an Integration's OAuth2 Application.
* @extends {Application}
*/
class IntegrationApplication extends Application {
_patch(data) {
super._patch(data);
if (typeof data.bot !== 'undefined') {
/**
* The bot {@link User user} for this application
* @type {?User}
*/
this.bot = this.client.users.add(data.bot);
} else if (!this.bot) {
this.bot = null;
}
}
}
module.exports = IntegrationApplication;

View file

@ -0,0 +1,133 @@
'use strict';
const APIMessage = require('./APIMessage');
const Base = require('./Base');
const Snowflake = require('../util/Snowflake');
/**
* Represents an interaction, see {@link InteractionClient}.
* @extends {Base}
*/
class Interaction extends Base {
constructor(client, data, syncHandle) {
super(client);
this.syncHandle = syncHandle;
this._patch(data);
}
_patch(data) {
/**
* The ID of this interaction.
* @type {Snowflake}
* @readonly
*/
this.id = data.id;
/**
* The token of this interaction.
* @type {string}
* @readonly
*/
this.token = data.token;
/**
* The ID of the invoked command.
* @type {Snowflake}
* @readonly
*/
this.commandID = data.data.id;
/**
* The name of the invoked command.
* @type {string}
* @readonly
*/
this.commandName = data.data.name;
/**
* The options passed to the command.
* @type {Object}
* @readonly
*/
this.options = data.data.options;
/**
* The channel this interaction was sent in.
* @type {?Channel}
* @readonly
*/
this.channel = this.client.channels?.cache.get(data.channel_id) || null;
/**
* The guild this interaction was sent in, if any.
* @type {?Guild}
* @readonly
*/
this.guild = data.guild_id ? this.client.guilds?.cache.get(data.guild_id) : null;
/**
* If this interaction was sent in a guild, the member which sent it.
* @type {?Member}
* @readonly
*/
this.member = data.member ? this.guild?.members.add(data.member, false) : null;
}
/**
* The timestamp the interaction was created at.
* @type {number}
* @readonly
*/
get createdTimestamp() {
return Snowflake.deconstruct(this.id).timestamp;
}
/**
* The time the interaction was created at.
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* Acknowledge this interaction without content.
*/
async acknowledge() {
await this.syncHandle.acknowledge();
}
/**
* Reply to this interaction.
* @param {(StringResolvable | APIMessage)?} content The content for the message.
* @param {(MessageOptions | MessageAdditions)?} options The options to provide.
*/
async reply(content, options) {
let apiMessage;
if (content instanceof APIMessage) {
apiMessage = content.resolveData();
} else {
apiMessage = APIMessage.create(this, content, options).resolveData();
if (Array.isArray(apiMessage.data.content)) {
throw new Error('Message is too long');
}
}
const resolved = await apiMessage.resolveFiles();
if (!this.syncHandle.reply(resolved)) {
const clientID =
this.client.interactionClient.clientID || (await this.client.api.oauth2.applications('@me').get()).id;
await this.client.api.webhooks(clientID, this.token).post({
auth: false,
data: resolved.data,
files: resolved.files,
});
}
}
}
module.exports = Interaction;

View file

@ -13,6 +13,7 @@ const Collection = require('../util/Collection');
const { MessageTypes } = require('../util/Constants');
const MessageFlags = require('../util/MessageFlags');
const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/Snowflake');
const Util = require('../util/Util');
/**
@ -23,14 +24,14 @@ class Message extends Base {
/**
* @param {Client} client The instantiating client
* @param {Object} data The data for the message
* @param {TextChannel|DMChannel} channel The channel the message was sent in
* @param {TextChannel|DMChannel|NewsChannel} channel The channel the message was sent in
*/
constructor(client, data, channel) {
super(client);
/**
* The channel that the message was sent in
* @type {TextChannel|DMChannel}
* @type {TextChannel|DMChannel|NewsChannel}
*/
this.channel = channel;
@ -50,35 +51,62 @@ class Message extends Base {
*/
this.id = data.id;
/**
* The type of the message
* @type {MessageType}
*/
this.type = MessageTypes[data.type];
if ('type' in data) {
/**
* The type of the message
* @type {?MessageType}
*/
this.type = MessageTypes[data.type];
/**
* The content of the message
* @type {string}
*/
this.content = data.content;
/**
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {?boolean}
*/
this.system = data.type !== 0;
} else if (typeof this.type !== 'string') {
this.system = null;
this.type = null;
}
/**
* The author of the message
* @type {?User}
*/
this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null;
if ('content' in data) {
/**
* The content of the message
* @type {?string}
*/
this.content = data.content;
} else if (typeof this.content !== 'string') {
this.content = null;
}
/**
* Whether or not this message is pinned
* @type {boolean}
*/
this.pinned = data.pinned;
if ('author' in data) {
/**
* The author of the message
* @type {?User}
*/
this.author = this.client.users.add(data.author, !data.webhook_id);
} else if (!this.author) {
this.author = null;
}
/**
* Whether or not the message was Text-To-Speech
* @type {boolean}
*/
this.tts = data.tts;
if ('pinned' in data) {
/**
* Whether or not this message is pinned
* @type {?boolean}
*/
this.pinned = Boolean(data.pinned);
} else if (typeof this.pinned !== 'boolean') {
this.pinned = null;
}
if ('tts' in data) {
/**
* Whether or not the message was Text-To-Speech
* @type {?boolean}
*/
this.tts = data.tts;
} else if (typeof this.tts !== 'boolean') {
this.tts = null;
}
/**
* A random number or string used for checking message delivery
@ -86,13 +114,7 @@ class Message extends Base {
* lost if re-fetched</warn>
* @type {?string}
*/
this.nonce = data.nonce;
/**
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {boolean}
*/
this.system = data.type !== 0;
this.nonce = 'nonce' in data ? data.nonce : null;
/**
* A list of embeds in the message - e.g. YouTube Player
@ -115,13 +137,13 @@ class Message extends Base {
* The timestamp the message was sent at
* @type {number}
*/
this.createdTimestamp = new Date(data.timestamp).getTime();
this.createdTimestamp = SnowflakeUtil.deconstruct(this.id).timestamp;
/**
* The timestamp the message was last edited at (if applicable)
* @type {?number}
*/
this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
this.editedTimestamp = 'edited_timestamp' in data ? new Date(data.edited_timestamp).getTime() : null;
/**
* A manager of the reactions belonging to this message
@ -183,11 +205,11 @@ class Message extends Base {
this.flags = new MessageFlags(data.flags).freeze();
/**
* Reference data sent in a crossposted message.
* Reference data sent in a crossposted message or inline reply.
* @typedef {Object} MessageReference
* @property {string} channelID ID of the channel the message was crossposted from
* @property {?string} guildID ID of the guild the message was crossposted from
* @property {?string} messageID ID of the message that was crossposted
* @property {string} channelID ID of the channel the message was referenced
* @property {?string} guildID ID of the guild the message was referenced
* @property {?string} messageID ID of the message that was referenced
*/
/**
@ -201,6 +223,10 @@ class Message extends Base {
messageID: data.message_reference.message_id,
}
: null;
if (data.referenced_message) {
this.channel.messages.add(data.referenced_message);
}
}
/**
@ -213,13 +239,18 @@ class Message extends Base {
}
/**
* Updates the message.
* Updates the message and returns the old message.
* @param {Object} data Raw Discord message update data
* @returns {Message}
* @private
*/
patch(data) {
const clone = this._clone();
this._edits.unshift(clone);
const { messageEditHistoryMaxSize } = this.client.options;
if (messageEditHistoryMaxSize !== 0) {
const editsLimit = messageEditHistoryMaxSize === -1 ? Infinity : messageEditHistoryMaxSize;
if (this._edits.unshift(clone) > editsLimit) this._edits.pop();
}
if ('edited_timestamp' in data) this.editedTimestamp = new Date(data.edited_timestamp).getTime();
if ('content' in data) this.content = data.content;
@ -240,12 +271,14 @@ class Message extends Base {
this.mentions = new Mentions(
this,
'mentions' in data ? data.mentions : this.mentions.users,
'mentions_roles' in data ? data.mentions_roles : this.mentions.roles,
'mention_roles' in data ? data.mention_roles : this.mentions.roles,
'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone,
'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels,
);
this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze();
return clone;
}
/**
@ -255,7 +288,7 @@ class Message extends Base {
* @readonly
*/
get member() {
return this.guild ? this.guild.member(this.author) || null : null;
return this.guild ? this.guild.members.resolve(this.author) || null : null;
}
/**
@ -291,7 +324,7 @@ class Message extends Base {
* @readonly
*/
get url() {
return `https://discordapp.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`;
return `https://discord.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`;
}
/**
@ -396,12 +429,42 @@ class Message extends Base {
);
}
/**
* The Message this crosspost/reply/pin-add references, if cached
* @type {?Message}
* @readonly
*/
get referencedMessage() {
if (!this.reference) return null;
const referenceChannel = this.client.channels.resolve(this.reference.channelID);
if (!referenceChannel) return null;
return referenceChannel.messages.resolve(this.reference.messageID);
}
/**
* Whether the message is crosspostable by the client user
* @type {boolean}
* @readonly
*/
get crosspostable() {
return (
this.channel.type === 'news' &&
!this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) &&
this.type === 'DEFAULT' &&
this.channel.viewable &&
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.SEND_MESSAGES) &&
(this.author.id === this.client.user.id ||
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES))
);
}
/**
* Options that can be passed into editMessage.
* @typedef {Object} MessageEditOptions
* @property {string} [content] Content to be edited
* @property {Object} [embed] An embed to be added/edited
* @property {MessageEmbed|Object} [embed] An embed to be added/edited
* @property {string|boolean} [code] Language for optional codeblock formatting to apply
* @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
*/
/**
@ -426,26 +489,56 @@ class Message extends Base {
}
/**
* Pins this message to the channel's pinned messages.
* Publishes a message in an announcement channel to all channels following it.
* @returns {Promise<Message>}
* @example
* // Crosspost a message
* if (message.channel.type === 'news') {
* message.crosspost()
* .then(() => console.log('Crossposted message'))
* .catch(console.error);
* }
*/
pin() {
async crosspost() {
await this.client.api.channels(this.channel.id).messages(this.id).crosspost.post();
return this;
}
/**
* Pins this message to the channel's pinned messages.
* @param {Object} [options] Options for pinning
* @param {string} [options.reason] Reason for pinning
* @returns {Promise<Message>}
* @example
* // Pin a message with a reason
* message.pin({ reason: 'important' })
* .then(console.log)
* .catch(console.error)
*/
pin(options) {
return this.client.api
.channels(this.channel.id)
.pins(this.id)
.put()
.put(options)
.then(() => this);
}
/**
* Unpins this message from the channel's pinned messages.
* @param {Object} [options] Options for unpinning
* @param {string} [options.reason] Reason for unpinning
* @returns {Promise<Message>}
* @example
* // Unpin a message with a reason
* message.unpin({ reason: 'no longer relevant' })
* .then(console.log)
* .catch(console.error)
*/
unpin() {
unpin(options) {
return this.client.api
.channels(this.channel.id)
.pins(this.id)
.delete()
.delete(options)
.then(() => this);
}
@ -492,12 +585,12 @@ class Message extends Base {
* @returns {Promise<Message>}
* @example
* // Delete a message
* message.delete()
* .then(msg => console.log(`Deleted message from ${msg.author.username}`))
* message.delete({ timeout: 5000 })
* .then(msg => console.log(`Deleted message from ${msg.author.username} after 5 seconds`))
* .catch(console.error);
*/
delete(options = {}) {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
if (typeof options !== 'object') return Promise.reject(new TypeError('INVALID_TYPE', 'options', 'object', true));
const { timeout = 0, reason } = options;
if (timeout <= 0) {
return this.channel.messages.delete(this.id, reason).then(() => this);
@ -511,30 +604,29 @@ class Message extends Base {
}
/**
* Replies to the message.
* Send an inline reply to this message.
* @param {StringResolvable|APIMessage} [content=''] The content for the message
* @param {MessageOptions|MessageAdditions} [options={}] The options to provide
* @param {MessageOptions|MessageAdditions} [options] The additional options to provide
* @param {MessageResolvable} [options.replyTo=this] The message to reply to
* @returns {Promise<Message|Message[]>}
* @example
* // Reply to a message
* message.reply('Hey, I\'m a reply!')
* .then(() => console.log(`Sent a reply to ${message.author.username}`))
* .catch(console.error);
*/
reply(content, options) {
return this.channel.send(
content instanceof APIMessage
? content
: APIMessage.transformOptions(content, options, { reply: this.member || this.author }),
: APIMessage.transformOptions(content, options, {
replyTo: this,
}),
);
}
/**
* Fetch this message.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Message>}
*/
fetch() {
return this.channel.messages.fetch(this.id, true);
fetch(force = false) {
return this.channel.messages.fetch(this.id, true, force);
}
/**

View file

@ -42,7 +42,7 @@ class MessageCollector extends Collector {
this._handleChannelDeletion = this._handleChannelDeletion.bind(this);
this._handleGuildDeletion = this._handleGuildDeletion.bind(this);
if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1);
this.client.incrementMaxListeners();
this.client.on(Events.MESSAGE_CREATE, this.handleCollect);
this.client.on(Events.MESSAGE_DELETE, this.handleDispose);
this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
@ -55,7 +55,7 @@ class MessageCollector extends Collector {
this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener);
this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion);
if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1);
this.client.decrementMaxListeners();
});
}

View file

@ -7,6 +7,13 @@ const Util = require('../util/Util');
* Represents an embed in a message (image/video preview, rich embed, etc.)
*/
class MessageEmbed {
/**
* @name MessageEmbed
* @kind constructor
* @memberof MessageEmbed
* @param {MessageEmbed|Object} [data={}] MessageEmbed to clone or raw embed data
*/
constructor(data = {}, skipValidation = false) {
this.setup(data, skipValidation);
}
@ -22,39 +29,40 @@ class MessageEmbed {
* * `link` - a link embed
* @type {string}
*/
this.type = data.type;
this.type = data.type || 'rich';
/**
* The title of this embed
* @type {?string}
*/
this.title = data.title;
this.title = 'title' in data ? data.title : null;
/**
* The description of this embed
* @type {?string}
*/
this.description = data.description;
this.description = 'description' in data ? data.description : null;
/**
* The URL of this embed
* @type {?string}
*/
this.url = data.url;
this.url = 'url' in data ? data.url : null;
/**
* The color of this embed
* @type {?number}
*/
this.color = Util.resolveColor(data.color);
this.color = 'color' in data ? Util.resolveColor(data.color) : null;
/**
* The timestamp of this embed
* @type {?number}
*/
this.timestamp = data.timestamp ? new Date(data.timestamp).getTime() : null;
this.timestamp = 'timestamp' in data ? new Date(data.timestamp).getTime() : null;
/**
* Represents a field of a MessageEmbed
* @typedef {Object} EmbedField
* @property {string} name The name of this field
* @property {string} value The value of this field
@ -71,6 +79,7 @@ class MessageEmbed {
}
/**
* Represents the thumbnail of a MessageEmbed
* @typedef {Object} MessageEmbedThumbnail
* @property {string} url URL for this thumbnail
* @property {string} proxyURL ProxyURL for this thumbnail
@ -92,6 +101,7 @@ class MessageEmbed {
: null;
/**
* Represents the image of a MessageEmbed
* @typedef {Object} MessageEmbedImage
* @property {string} url URL for this image
* @property {string} proxyURL ProxyURL for this image
@ -113,6 +123,7 @@ class MessageEmbed {
: null;
/**
* Represents the video of a MessageEmbed
* @typedef {Object} MessageEmbedVideo
* @property {string} url URL of this video
* @property {string} proxyURL ProxyURL for this video
@ -135,6 +146,7 @@ class MessageEmbed {
: null;
/**
* Represents the author field of a MessageEmbed
* @typedef {Object} MessageEmbedAuthor
* @property {string} name The name of this author
* @property {string} url URL of this author
@ -156,6 +168,7 @@ class MessageEmbed {
: null;
/**
* Represents the provider of a MessageEmbed
* @typedef {Object} MessageEmbedProvider
* @property {string} name The name of this provider
* @property {string} url URL of this provider
@ -173,6 +186,7 @@ class MessageEmbed {
: null;
/**
* Represents the footer field of a MessageEmbed
* @typedef {Object} MessageEmbedFooter
* @property {string} text The text of this footer
* @property {string} iconURL URL of the icon for this footer

View file

@ -79,14 +79,14 @@ class MessageMentions {
}
/**
* Cached members for {@link MessageMention#members}
* Cached members for {@link MessageMentions#members}
* @type {?Collection<Snowflake, GuildMember>}
* @private
*/
this._members = null;
/**
* Cached channels for {@link MessageMention#channels}
* Cached channels for {@link MessageMentions#channels}
* @type {?Collection<Snowflake, GuildChannel>}
* @private
*/
@ -138,7 +138,7 @@ class MessageMentions {
if (!this.guild) return null;
this._members = new Collection();
this.users.forEach(user => {
const member = this.guild.member(user);
const member = this.guild.members.resolve(user);
if (member) this._members.set(member.user.id, member);
});
return this._members;
@ -164,7 +164,7 @@ class MessageMentions {
/**
* Checks if a user, guild member, role, or channel is mentioned.
* Takes into account user mentions, role mentions, and @everyone/@here mentions.
* @param {UserResolvable|GuildMember|Role|GuildChannel} data User/GuildMember/Role/Channel to check
* @param {UserResolvable|RoleResolvable|GuildChannelResolvable} data User/Role/Channel to check
* @param {Object} [options] Options
* @param {boolean} [options.ignoreDirect=false] - Whether to ignore direct mentions to the item
* @param {boolean} [options.ignoreRoles=false] - Whether to ignore role mentions to a guild member
@ -179,7 +179,11 @@ class MessageMentions {
}
if (!ignoreDirect) {
const id = data.id || data;
const id =
this.client.users.resolveID(data) ||
(this.guild && this.guild.roles.resolveID(data)) ||
this.client.channels.resolveID(data);
return this.users.has(id) || this.channels.has(id) || this.roles.has(id);
}

View file

@ -28,12 +28,6 @@ class MessageReaction {
*/
this.message = message;
/**
* Whether the client has given this reaction
* @type {boolean}
*/
this.me = data.me;
/**
* A manager of the users that have given this reaction
* @type {ReactionUserManager}
@ -46,13 +40,20 @@ class MessageReaction {
}
_patch(data) {
/**
* The number of people that have given the same reaction
* @type {?number}
* @name MessageReaction#count
*/
// eslint-disable-next-line eqeqeq
if (this.count == undefined) this.count = data.count;
if (this.count == undefined) {
/**
* The number of people that have given the same reaction
* @type {?number}
*/
this.count = data.count;
}
/**
* Whether the client has given this reaction
* @type {boolean}
*/
this.me = data.me;
}
/**

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