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"], "extends": ["eslint:recommended", "plugin:prettier/recommended"],
"plugins": ["import"], "plugins": ["import"],
"parserOptions": { "parserOptions": {
"ecmaVersion": 2019 "ecmaVersion": 2020
}, },
"env": { "env": {
"es6": true, "es2020": true,
"node": true "node": true
}, },
"overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }], "overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }],
@ -26,7 +27,8 @@
"singleQuote": true, "singleQuote": true,
"quoteProps": "as-needed", "quoteProps": "as-needed",
"trailingComma": "all", "trailingComma": "all",
"endOfLine": "lf" "endOfLine": "lf",
"arrowParens": "avoid"
} }
], ],
"strict": ["error", "global"], "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: 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 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` 3. If you're working on voice, also run `npm install @discordjs/opus` or `npm install opusscript`
4. Code your heart out! 4. Code your heart out!
5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid 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: - Operating system:
- Priority this issue should have please be realistic and elaborate if possible: - 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]. 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 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 blank_issues_enabled: false
contact_links: contact_links:
- name: discord.js discord server - name: Discord server
url: https://discord.gg/bRCvFy9 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 - name: Checkout repository
uses: actions/checkout@master uses: actions/checkout@master
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@master uses: actions/setup-node@master
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Build and deploy documentation - name: Build and deploy documentation
uses: discordjs/action-docs@v1 uses: discordjs/action-docs@v1
@ -35,13 +35,13 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@master uses: actions/checkout@master
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@master uses: actions/setup-node@master
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Build and deploy webpack - name: Build and deploy webpack
uses: discordjs/action-webpack@v1 uses: discordjs/action-webpack@v1

View file

@ -10,13 +10,13 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Run ESLint - name: Run ESLint
uses: icrawl/action-eslint@v1 uses: icrawl/action-eslint@v1
@ -28,17 +28,38 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Run TSLint - name: Run TSLint
run: npm run lint:typings 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: docs:
name: Documentation name: Documentation
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -46,13 +67,13 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Test documentation - name: Test documentation
run: npm run docs:test run: npm run docs:test

View file

@ -8,13 +8,13 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Run ESLint - name: Run ESLint
uses: icrawl/action-eslint@v1 uses: icrawl/action-eslint@v1
@ -26,17 +26,38 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Run TSLint - name: Run TSLint
run: npm run lint:typings 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: docs:
name: Documentation name: Documentation
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -44,13 +65,13 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Node v12 - name: Install Node v14
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 14
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Test documentation - name: Test documentation
run: npm run docs:test run: npm run docs:test

3
.gitignore vendored
View file

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

1
.npmrc
View file

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

View file

@ -5,7 +5,7 @@
</p> </p>
<br /> <br />
<p> <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/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://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> <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 ## Table of contents
- [Changes](#changes)
- [About](#about) - [About](#about)
- [Installation](#installation) - [Installation](#installation)
- [Audio engines](#audio-engines) - [Audio engines](#audio-engines)
@ -29,10 +30,14 @@
- [Contributing](#contributing) - [Contributing](#contributing)
- [Help](#help) - [Help](#help)
## Changes
This fork has the inline replies and slash commands for **testing** purposes.
## About ## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the 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 - Object-oriented
- Predictable abstractions - Predictable abstractions
@ -41,7 +46,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation ## 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. Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js` 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 ### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`) - [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: - 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`) - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
@ -76,7 +81,7 @@ client.on('ready', () => {
client.on('message', msg => { client.on('message', msg => {
if (msg.content === 'ping') { 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'); 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'); 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'); client.login('your token here');
``` ```
The results are the same as the URL examples: 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! 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'); client.login('your token here');
``` ```

View file

@ -23,9 +23,9 @@ client.on('message', message => {
// If the message is "what is my avatar" // If the message is "what is my avatar"
if (message.content === 'what is my avatar') { if (message.content === 'what is my avatar') {
// Send the user's avatar URL // 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'); 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'); client.login('your token here');

View file

@ -28,5 +28,5 @@ client.on('guildMemberAdd', member => {
channel.send(`Welcome to the server, ${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'); client.login('your token here');

View file

@ -33,7 +33,7 @@ client.on('message', message => {
// If we have a user mentioned // If we have a user mentioned
if (user) { if (user) {
// Now we get the member from the 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 the member is in the guild
if (member) { if (member) {
/** /**
@ -45,28 +45,28 @@ client.on('message', message => {
.kick('Optional reason that will display in the audit logs') .kick('Optional reason that will display in the audit logs')
.then(() => { .then(() => {
// We let the message author know we were able to kick the person // 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 => { .catch(err => {
// An error happened // An error happened
// This is generally due to the bot not being able to kick the member, // This is generally due to the bot not being able to kick the member,
// either due to missing permissions or role hierarchy // 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 // Log the error
console.error(err); console.error(err);
}); });
} else { } else {
// The mentioned user isn't in this guild // 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 // Otherwise, if no user was mentioned
} else { } 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'); client.login('your token here');
``` ```
@ -105,7 +105,7 @@ client.on('message', message => {
// If we have a user mentioned // If we have a user mentioned
if (user) { if (user) {
// Now we get the member from the 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 the member is in the guild
if (member) { if (member) {
/** /**
@ -121,28 +121,28 @@ client.on('message', message => {
}) })
.then(() => { .then(() => {
// We let the message author know we were able to ban the person // 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 => { .catch(err => {
// An error happened // An error happened
// This is generally due to the bot not being able to ban the member, // This is generally due to the bot not being able to ban the member,
// either due to missing permissions or role hierarchy // 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 // Log the error
console.error(err); console.error(err);
}); });
} else { } else {
// The mentioned user isn't in this guild // 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 { } else {
// Otherwise, if no user was mentioned // 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'); 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'); client.login('your token here');

View file

@ -6,8 +6,13 @@
// Import the discord.js module // Import the discord.js module
const Discord = require('discord.js'); 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'); const hook = new Discord.WebhookClient('webhook id', 'webhook token');
// Send a message using the webhook // 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` ## 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? ## How do I get voice working?

View file

@ -5,7 +5,7 @@
</p> </p>
<br /> <br />
<p> <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/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://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> <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 ## About
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the 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 - Object-oriented
- Predictable abstractions - Predictable abstractions
@ -33,7 +33,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
## Installation ## 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. Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js` 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 ### Optional packages
- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`) - [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: - 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`) - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
@ -68,7 +68,7 @@ client.on('ready', () => {
client.on('message', msg => { client.on('message', msg => {
if (msg.content === 'ping') { 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) { if (message.member.voice.channel) {
const connection = await message.member.voice.channel.join(); const connection = await message.member.voice.channel.join();
} else { } 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", "name": "discord.js",
"version": "12.1.0", "version": "12.5.0",
"description": "A powerful library for interacting with the Discord API", "description": "A powerful library for interacting with the Discord API",
"main": "./src/index", "main": "./src/index",
"types": "./typings/index.d.ts", "types": "./typings/index.d.ts",
"exports": {
".": [
{
"require": "./src/index.js",
"import": "./esm/discord.mjs"
},
"./src/index.js"
],
"./esm": "./esm/discord.mjs"
},
"scripts": { "scripts": {
"test": "npm run lint && npm run docs:test && npm run lint:typings", "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": "docgen --source src --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --source src --custom docs/index.yml", "docs:test": "docgen --source src --custom docs/index.yml",
"lint": "eslint src", "lint": "eslint src",
"lint:fix": "eslint src --fix", "lint:fix": "eslint src --fix",
"lint:typings": "tslint typings/index.d.ts", "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", "build:browser": "webpack",
"prepublishOnly": "npm run test && cross-env NODE_ENV=production npm run build:browser" "prepublishOnly": "npm run test && cross-env NODE_ENV=production npm run build:browser"
}, },
@ -36,68 +47,40 @@
"runkitExampleFilename": "./docs/examples/ping.js", "runkitExampleFilename": "./docs/examples/ping.js",
"unpkg": "./webpack/discord.min.js", "unpkg": "./webpack/discord.min.js",
"dependencies": { "dependencies": {
"@discordjs/collection": "^0.1.5", "@discordjs/collection": "^0.1.6",
"@discordjs/form-data": "^3.0.1",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
"form-data": "^3.0.0", "node-fetch": "^2.6.1",
"node-fetch": "^2.6.0", "prism-media": "^1.2.2",
"prism-media": "^1.2.0",
"setimmediate": "^1.0.5", "setimmediate": "^1.0.5",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"ws": "^7.2.1" "ws": "^7.3.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
}
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^8.3.5", "@commitlint/cli": "^11.0.0",
"@commitlint/config-angular": "^8.3.4", "@commitlint/config-angular": "^11.0.0",
"@types/node": "^10.12.24", "@types/node": "^12.12.6",
"@types/ws": "^7.2.1", "@types/ws": "^7.2.7",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"discord.js-docgen": "discordjs/docgen", "discord.js-docgen": "git+https://github.com/discordjs/docgen.git",
"dtslint": "^3.0.0", "dtslint": "^4.0.4",
"eslint": "^6.8.0", "eslint": "^7.11.0",
"eslint-config-prettier": "^6.10.0", "eslint-config-prettier": "^6.13.0",
"eslint-plugin-import": "^2.20.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.1.4",
"husky": "^4.2.3", "husky": "^4.3.0",
"jest": "^25.1.0", "jest": "^26.6.0",
"json-filter-loader": "^1.0.0", "json-filter-loader": "^1.0.0",
"lint-staged": "^10.0.8", "lint-staged": "^10.4.2",
"prettier": "^1.19.1", "prettier": "^2.1.2",
"terser-webpack-plugin": "^1.2.2", "terser-webpack-plugin": "^4.2.3",
"tslint": "^6.0.0", "tslint": "^6.1.3",
"typescript": "^3.8.2", "typescript": "^4.0.3",
"webpack": "^4.41.6", "webpack": "^4.44.2",
"webpack-cli": "^3.3.11" "webpack-cli": "^3.3.12"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=14.0.0"
}, },
"browser": { "browser": {
"@discordjs/opus": false, "@discordjs/opus": false,
@ -127,9 +110,9 @@
"src/client/voice/receiver/PacketHandler.js": false, "src/client/voice/receiver/PacketHandler.js": false,
"src/client/voice/receiver/Receiver.js": false, "src/client/voice/receiver/Receiver.js": false,
"src/client/voice/util/PlayInterface.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/Silence.js": false,
"src/client/voice/util/VolumeInterface.js": false "src/client/voice/util/VolumeInterface.js": false,
"src/util/Sodium.js": false
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -139,7 +122,7 @@
}, },
"lint-staged": { "lint-staged": {
"*.js": "eslint --fix", "*.js": "eslint --fix",
"*.ts": "prettier --write --single-quote --print-width 120 --trailing-comma all --end-of-line lf" "*.ts": "prettier --write"
}, },
"commitlint": { "commitlint": {
"extends": [ "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); 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) { toJSON(...props) {
return Util.flatten(this, { domain: false }, ...props); return Util.flatten(this, { domain: false }, ...props);
} }

View file

@ -1,17 +1,19 @@
'use strict'; 'use strict';
const BaseClient = require('./BaseClient'); const BaseClient = require('./BaseClient');
const InteractionClient = require('./InteractionClient');
const ActionsManager = require('./actions/ActionsManager'); const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager'); const WebSocketManager = require('./websocket/WebSocketManager');
const { Error, TypeError, RangeError } = require('../errors'); const { Error, TypeError, RangeError } = require('../errors');
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
const ChannelManager = require('../managers/ChannelManager'); const ChannelManager = require('../managers/ChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildManager = require('../managers/GuildManager'); const GuildManager = require('../managers/GuildManager');
const UserManager = require('../managers/UserManager'); const UserManager = require('../managers/UserManager');
const ShardClientUtil = require('../sharding/ShardClientUtil'); const ShardClientUtil = require('../sharding/ShardClientUtil');
const ClientApplication = require('../structures/ClientApplication'); const ClientApplication = require('../structures/ClientApplication');
const GuildPreview = require('../structures/GuildPreview'); const GuildPreview = require('../structures/GuildPreview');
const GuildTemplate = require('../structures/GuildTemplate');
const Invite = require('../structures/Invite'); const Invite = require('../structures/Invite');
const VoiceRegion = require('../structures/VoiceRegion'); const VoiceRegion = require('../structures/VoiceRegion');
const Webhook = require('../structures/Webhook'); const Webhook = require('../structures/Webhook');
@ -102,6 +104,12 @@ class Client extends BaseClient {
? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE) ? ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE)
: null; : 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 * All of the {@link User} objects that have been cached at any point, mapped by their IDs
* @type {UserManager} * @type {UserManager}
@ -135,7 +143,8 @@ class Client extends BaseClient {
Object.defineProperty(this, 'token', { writable: true }); Object.defineProperty(this, 'token', { writable: true });
if (!browser && !this.token && 'DISCORD_TOKEN' in process.env) { 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> * <warn>This should be kept private at all times.</warn>
* @type {?string} * @type {?string}
*/ */
@ -164,11 +173,11 @@ class Client extends BaseClient {
/** /**
* All custom emojis that the client has access to, mapped by their IDs * All custom emojis that the client has access to, mapped by their IDs
* @type {GuildEmojiManager} * @type {BaseGuildEmojiManager}
* @readonly * @readonly
*/ */
get emojis() { get emojis() {
const emojis = new GuildEmojiManager({ client: this }); const emojis = new BaseGuildEmojiManager(this);
for (const guild of this.guilds.cache.values()) { 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); 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. * 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 * @returns {Promise<string>} Token of the account used
* @example * @example
* client.login('my token'); * client.login('my token');
@ -253,6 +262,23 @@ class Client extends BaseClient {
.then(data => new Invite(this, data)); .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. * Obtains a webhook from Discord.
* @param {Snowflake} id ID of the webhook * @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 * @param {GuildResolvable} guild The guild to fetch the preview for
* @returns {Promise<GuildPreview>} * @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. * 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>} * @returns {Promise<string>}
* @example * @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}`)) * .then(link => console.log(`Generated bot invite link: ${link}`))
* .catch(console.error); * .catch(console.error);
*/ */
async generateInvite(permissions) { async generateInvite(options = {}) {
permissions = Permissions.resolve(permissions); 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 application = await this.fetchApplication();
const query = new URLSearchParams({ const query = new URLSearchParams({
client_id: application.id, client_id: application.id,
permissions: permissions, permissions: Permissions.resolve(options.permissions),
scope: 'bot', 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}`; return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`;
} }
toJSON() { toJSON() {
return super.toJSON({ return super.toJSON({
readyAt: false, readyAt: false,
presences: false,
}); });
} }
@ -416,6 +457,13 @@ class Client extends BaseClient {
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) { if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number'); 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') { if (typeof options.fetchAllMembers !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean'); throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean');
} }
@ -442,6 +490,14 @@ class Client extends BaseClient {
module.exports = Client; 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. * Emitted for general warnings.
* @event Client#warn * @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) { getMember(data, guild) {
const id = data.user.id; return this.getPayload(data, guild.members, data.user.id, PartialTypes.GUILD_MEMBER);
return this.getPayload(
{
user: {
id,
},
},
guild.members,
id,
PartialTypes.GUILD_MEMBER,
);
} }
getUser(data) { getUser(data) {
const id = data.user_id; const id = data.user_id;
return data.user || this.getPayload({ id }, this.client.users, id, PartialTypes.USER); 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; module.exports = GenericAction;

View file

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

View file

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

View file

@ -9,7 +9,7 @@ class GuildMemberRemoveAction extends Action {
const guild = client.guilds.cache.get(data.guild_id); const guild = client.guilds.cache.get(data.guild_id);
let member = null; let member = null;
if (guild) { if (guild) {
member = this.getMember(data, guild); member = this.getMember({ user: data.user }, guild);
guild.memberCount--; guild.memberCount--;
if (member) { if (member) {
member.deleted = true; 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 client = this.client;
const channel = client.channels.cache.get(data.channel_id); const channel = client.channels.cache.get(data.channel_id);
const guild = client.guilds.cache.get(data.guild_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 inviteData = Object.assign(data, { channel, guild });
const invite = new Invite(client, inviteData); const invite = new Invite(client, inviteData);

View file

@ -8,14 +8,17 @@ const { PartialTypes } = require('../../util/Constants');
{ user_id: 'id', { user_id: 'id',
message_id: 'id', message_id: 'id',
emoji: { name: '<27>', id: null }, 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 { class MessageReactionAdd extends Action {
handle(data) { handle(data) {
if (!data.emoji) return false; if (!data.emoji) return false;
const user = this.getUser(data); const user = this.getUserFromMember(data);
if (!user) return false; if (!user) return false;
// Verify channel // Verify channel
@ -28,6 +31,8 @@ class MessageReactionAdd extends Action {
// Verify reaction // Verify reaction
if (message.partial && !this.client.options.partials.includes(PartialTypes.REACTION)) return false; 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({ const reaction = message.reactions.add({
emoji: data.emoji, emoji: data.emoji,
count: message.partial ? null : 0, count: message.partial ? null : 0,

View file

@ -7,7 +7,8 @@ const { Events } = require('../../util/Constants');
{ user_id: 'id', { user_id: 'id',
message_id: 'id', message_id: 'id',
emoji: { name: '<27>', id: null }, emoji: { name: '<27>', id: null },
channel_id: 'id' } } channel_id: 'id',
guild_id: 'id' }
*/ */
class MessageReactionRemove extends Action { 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 { id, channel_id, guild_id, author, timestamp, type } = data;
const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel); const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel);
if (message) { if (message) {
message.patch(data); const old = message.patch(data);
return { return {
old: message._edits[0], old,
updated: message, 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)) { if (!oldUser.equals(newUser)) {
/** /**
* Emitted whenever a user's details (e.g. username) are changed. * 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 * @event Client#userUpdate
* @param {User} oldUser The user before the update * @param {User} oldUser The user before the update
* @param {User} newUser The user after the update * @param {User} newUser The user after the update

View file

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

View file

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

View file

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

View file

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

View file

@ -189,7 +189,11 @@ class VoiceWebSocket extends EventEmitter {
this.emit('sessionDescription', packet.d); this.emit('sessionDescription', packet.d);
break; break;
case VoiceOPCodes.CLIENT_CONNECT: 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; break;
case VoiceOPCodes.CLIENT_DISCONNECT: case VoiceOPCodes.CLIENT_DISCONNECT:
const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id); const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);

View file

@ -1,7 +1,9 @@
'use strict'; 'use strict';
const EventEmitter = require('events'); 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 // The delay between packets when a user is considered to have stopped speaking
// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200 // https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
@ -56,7 +58,7 @@ class PacketHandler extends EventEmitter {
} }
// Open packet // 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'); if (!packet) return new Error('Failed to decrypt voice packet');
packet = Buffer.from(packet); packet = Buffer.from(packet);
@ -84,8 +86,31 @@ class PacketHandler extends EventEmitter {
const userStat = this.connection.ssrcMap.get(ssrc); const userStat = this.connection.ssrcMap.get(ssrc);
if (!userStat) return; 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); let speakingTimeout = this.speakingTimeouts.get(ssrc);
if (typeof speakingTimeout === 'undefined') { 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 }); this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking });
speakingTimeout = this.receiver.connection.client.setTimeout(() => { speakingTimeout = this.receiver.connection.client.setTimeout(() => {
try { try {
@ -101,15 +126,17 @@ class PacketHandler extends EventEmitter {
speakingTimeout.refresh(); speakingTimeout.refresh();
} }
let stream = this.streams.get(userStat.userID); if (streamInfo) {
if (!stream) return; const { stream } = streamInfo;
stream = stream.stream; if (!opusPacket) {
const opusPacket = this.parseBuffer(buffer); opusPacket = this.parseBuffer(buffer);
if (opusPacket instanceof Error) { if (opusPacket instanceof Error) {
this.emit('error', opusPacket); this.emit('error', opusPacket);
return; 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; module.exports = Silence;

View file

@ -18,15 +18,13 @@ const BeforeReadyWhitelist = [
WSEvents.GUILD_MEMBER_REMOVE, WSEvents.GUILD_MEMBER_REMOVE,
]; ];
const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes) const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1).map(Number);
.slice(1)
.map(Number);
const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007]; const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007];
/** /**
* The WebSocket manager for this client. * The WebSocket manager for this client.
* <info>This class forwards raw dispatch events, * <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 * @extends EventEmitter
*/ */
class WebSocketManager extends EventEmitter { class WebSocketManager extends EventEmitter {
@ -45,7 +43,7 @@ class WebSocketManager extends EventEmitter {
* The gateway this manager uses * The gateway this manager uses
* @type {?string} * @type {?string}
*/ */
this.gateway = undefined; this.gateway = null;
/** /**
* The amount of shards this manager handles * The amount of shards this manager handles
@ -78,7 +76,7 @@ class WebSocketManager extends EventEmitter {
/** /**
* The current status of this WebSocketManager * The current status of this WebSocketManager
* @type {number} * @type {Status}
*/ */
this.status = Status.IDLE; this.status = Status.IDLE;
@ -100,11 +98,11 @@ class WebSocketManager extends EventEmitter {
* The current session limit of the client * The current session limit of the client
* @private * @private
* @type {?Object} * @type {?Object}
* @prop {number} total Total number of identifies available * @property {number} total Total number of identifies available
* @prop {number} remaining Number of identifies remaining * @property {number} remaining Number of identifies remaining
* @prop {number} reset_after Number of milliseconds after which the limit resets * @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)) { if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) {
// These event codes cannot be resumed // 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 * The current session ID of the shard
* @type {string} * @type {?string}
* @private * @private
*/ */
this.sessionID = undefined; this.sessionID = null;
/** /**
* The previous heartbeat ping of the shard * The previous heartbeat ping of the shard
@ -83,6 +83,7 @@ class WebSocketShard extends EventEmitter {
/** /**
* Contains the rate limit queue and metadata * Contains the rate limit queue and metadata
* @name WebSocketShard#ratelimit
* @type {Object} * @type {Object}
* @private * @private
*/ */
@ -98,6 +99,7 @@ class WebSocketShard extends EventEmitter {
/** /**
* The WebSocket connection for the current shard * The WebSocket connection for the current shard
* @name WebSocketShard#connection
* @type {?WebSocket} * @type {?WebSocket}
* @private * @private
*/ */
@ -110,6 +112,7 @@ class WebSocketShard extends EventEmitter {
/** /**
* The compression to use * The compression to use
* @name WebSocketShard#inflate
* @type {?Inflate} * @type {?Inflate}
* @private * @private
*/ */
@ -117,13 +120,15 @@ class WebSocketShard extends EventEmitter {
/** /**
* The HELLO timeout * The HELLO timeout
* @type {?NodeJS.Timer} * @name WebSocketShard#helloTimeout
* @type {?NodeJS.Timeout}
* @private * @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 * If the manager attached its event handlers on the shard
* @name WebSocketShard#eventsAttached
* @type {boolean} * @type {boolean}
* @private * @private
*/ */
@ -131,20 +136,23 @@ class WebSocketShard extends EventEmitter {
/** /**
* A set of guild IDs this shard expects to receive * A set of guild IDs this shard expects to receive
* @name WebSocketShard#expectedGuilds
* @type {?Set<string>} * @type {?Set<string>}
* @private * @private
*/ */
Object.defineProperty(this, 'expectedGuilds', { value: undefined, writable: true }); Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true });
/** /**
* The ready timeout * The ready timeout
* @type {?NodeJS.Timer} * @name WebSocketShard#readyTimeout
* @type {?NodeJS.Timeout}
* @private * @private
*/ */
Object.defineProperty(this, 'readyTimeout', { value: undefined, writable: true }); Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });
/** /**
* Time when the WebSocket connection was opened * Time when the WebSocket connection was opened
* @name WebSocketShard#connectedAt
* @type {number} * @type {number}
* @private * @private
*/ */
@ -407,6 +415,7 @@ class WebSocketShard extends EventEmitter {
this.identify(); this.identify();
break; break;
case OPCodes.RECONNECT: case OPCodes.RECONNECT:
this.debug('[RECONNECT] Discord asked us to reconnect');
this.destroy({ closeCode: 4000 }); this.destroy({ closeCode: 4000 });
break; break;
case OPCodes.INVALID_SESSION: case OPCodes.INVALID_SESSION:
@ -419,7 +428,7 @@ class WebSocketShard extends EventEmitter {
// Reset the sequence // Reset the sequence
this.sequence = -1; this.sequence = -1;
// Reset the session ID as it's invalid // Reset the session ID as it's invalid
this.sessionID = undefined; this.sessionID = null;
// Set the status to reconnecting // Set the status to reconnecting
this.status = Status.RECONNECTING; this.status = Status.RECONNECTING;
// Finally, emit the INVALID_SESSION event // Finally, emit the INVALID_SESSION event
@ -448,7 +457,7 @@ class WebSocketShard extends EventEmitter {
// Step 0. Clear the ready timeout, if it exists // Step 0. Clear the ready timeout, if it exists
if (this.readyTimeout) { if (this.readyTimeout) {
this.manager.client.clearTimeout(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 // Step 1. If we don't have any other guilds pending, we are ready
if (!this.expectedGuilds.size) { 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. this.debug(`Shard did not receive any more guild packets in 15 seconds.
Unavailable guild count: ${this.expectedGuilds.size}`); Unavailable guild count: ${this.expectedGuilds.size}`);
this.readyTimeout = undefined; this.readyTimeout = null;
this.status = Status.READY; this.status = Status.READY;
@ -489,7 +498,7 @@ class WebSocketShard extends EventEmitter {
if (this.helloTimeout) { if (this.helloTimeout) {
this.debug('Clearing the HELLO timeout.'); this.debug('Clearing the HELLO timeout.');
this.manager.client.clearTimeout(this.helloTimeout); this.manager.client.clearTimeout(this.helloTimeout);
this.helloTimeout = undefined; this.helloTimeout = null;
} }
return; return;
} }
@ -510,7 +519,7 @@ class WebSocketShard extends EventEmitter {
if (this.heartbeatInterval) { if (this.heartbeatInterval) {
this.debug('Clearing the heartbeat interval.'); this.debug('Clearing the heartbeat interval.');
this.manager.client.clearInterval(this.heartbeatInterval); this.manager.client.clearInterval(this.heartbeatInterval);
this.heartbeatInterval = undefined; this.heartbeatInterval = null;
} }
return; return;
} }
@ -622,7 +631,7 @@ class WebSocketShard extends EventEmitter {
/** /**
* Adds a packet to the queue to be sent to the gateway. * 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 * <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> * Do not use this method if you don't know what you're doing.</warn>
* @param {Object} data The full packet to send * @param {Object} data The full packet to send
* @param {boolean} [important=false] If this packet should be added first in queue * @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 // Step 5: Reset the sequence and session ID if requested
if (reset) { if (reset) {
this.sequence = -1; this.sequence = -1;
this.sessionID = undefined; this.sessionID = null;
} }
// Step 6: reset the ratelimit data // 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)); for (const member of data.members) members.set(member.user.id, guild.members.add(member));
if (data.presences) { 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). * Emitted whenever a chunk of guild members is received (all members come from the same guild).
* @event Client#guildMembersChunk * @event Client#guildMembersChunk
* @param {Collection<Snowflake, GuildMember>} members The members in the chunk * @param {Collection<Snowflake, GuildMember>} members The members in the chunk
* @param {Guild} guild The guild related to the member 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'; 'use strict';
const { Status, Events } = require('../../../util/Constants'); module.exports = (client, packet, shard) => {
client.actions.GuildMemberUpdate.handle(packet.d, shard);
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);
}
}
}
}; };

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'; 'use strict';
const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => {
client.actions.TypingStart.handle(packet.d);
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);
}
}
}; };
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.', DISALLOWED_INTENTS: 'Privileged intent provided is not enabled or whitelisted.',
SHARDING_NO_SHARDS: 'No shards have been spawned.', SHARDING_NO_SHARDS: 'No shards have been spawned.',
SHARDING_IN_PROCESS: 'Shards are still being 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_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`,
SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`, 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_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_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_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_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
COLOR_CONVERT: 'Unable to convert color to a number.', COLOR_CONVERT: 'Unable to convert color to a number.',
@ -66,7 +71,7 @@ const Messages = {
IMAGE_SIZE: size => `Invalid image size: ${size}`, IMAGE_SIZE: size => `Invalid image size: ${size}`,
MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.', 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', 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", 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", 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); for (const [name, message] of Object.entries(Messages)) register(name, message);

View file

@ -6,6 +6,7 @@ module.exports = {
// "Root" classes (starting points) // "Root" classes (starting points)
BaseClient: require('./client/BaseClient'), BaseClient: require('./client/BaseClient'),
Client: require('./client/Client'), Client: require('./client/Client'),
InteractionClient: require('./client/InteractionClient'),
Shard: require('./sharding/Shard'), Shard: require('./sharding/Shard'),
ShardClientUtil: require('./sharding/ShardClientUtil'), ShardClientUtil: require('./sharding/ShardClientUtil'),
ShardingManager: require('./sharding/ShardingManager'), ShardingManager: require('./sharding/ShardingManager'),
@ -28,10 +29,12 @@ module.exports = {
SnowflakeUtil: require('./util/Snowflake'), SnowflakeUtil: require('./util/Snowflake'),
Structures: require('./util/Structures'), Structures: require('./util/Structures'),
SystemChannelFlags: require('./util/SystemChannelFlags'), SystemChannelFlags: require('./util/SystemChannelFlags'),
UserFlags: require('./util/UserFlags'),
Util: Util, Util: Util,
version: require('../package.json').version, version: require('../package.json').version,
// Managers // Managers
BaseGuildEmojiManager: require('./managers/BaseGuildEmojiManager'),
ChannelManager: require('./managers/ChannelManager'), ChannelManager: require('./managers/ChannelManager'),
GuildChannelManager: require('./managers/GuildChannelManager'), GuildChannelManager: require('./managers/GuildChannelManager'),
GuildEmojiManager: require('./managers/GuildEmojiManager'), GuildEmojiManager: require('./managers/GuildEmojiManager'),
@ -39,6 +42,7 @@ module.exports = {
GuildMemberManager: require('./managers/GuildMemberManager'), GuildMemberManager: require('./managers/GuildMemberManager'),
GuildMemberRoleManager: require('./managers/GuildMemberRoleManager'), GuildMemberRoleManager: require('./managers/GuildMemberRoleManager'),
GuildManager: require('./managers/GuildManager'), GuildManager: require('./managers/GuildManager'),
ReactionManager: require('./managers/ReactionManager'),
ReactionUserManager: require('./managers/ReactionUserManager'), ReactionUserManager: require('./managers/ReactionUserManager'),
MessageManager: require('./managers/MessageManager'), MessageManager: require('./managers/MessageManager'),
PresenceManager: require('./managers/PresenceManager'), PresenceManager: require('./managers/PresenceManager'),
@ -54,6 +58,8 @@ module.exports = {
splitMessage: Util.splitMessage, splitMessage: Util.splitMessage,
// Structures // Structures
Application: require('./structures/interfaces/Application'),
ApplicationCommand: require('./structures/ApplicationCommand'),
Base: require('./structures/Base'), Base: require('./structures/Base'),
Activity: require('./structures/Presence').Activity, Activity: require('./structures/Presence').Activity,
APIMessage: require('./structures/APIMessage'), APIMessage: require('./structures/APIMessage'),
@ -74,7 +80,9 @@ module.exports = {
GuildEmoji: require('./structures/GuildEmoji'), GuildEmoji: require('./structures/GuildEmoji'),
GuildMember: require('./structures/GuildMember'), GuildMember: require('./structures/GuildMember'),
GuildPreview: require('./structures/GuildPreview'), GuildPreview: require('./structures/GuildPreview'),
GuildTemplate: require('./structures/GuildTemplate'),
Integration: require('./structures/Integration'), Integration: require('./structures/Integration'),
Interaction: require('./structures/Interaction'),
Invite: require('./structures/Invite'), Invite: require('./structures/Invite'),
Message: require('./structures/Message'), Message: require('./structures/Message'),
MessageAttachment: require('./structures/MessageAttachment'), 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. * Obtains a channel from Discord, or the channel cache if it's already available.
* @param {Snowflake} id ID of the channel * @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} [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>} * @returns {Promise<Channel>}
* @example * @example
* // Fetch a channel by its id * // Fetch a channel by its id
@ -81,9 +82,11 @@ class ChannelManager extends BaseManager {
* .then(channel => console.log(channel.name)) * .then(channel => console.log(channel.name))
* .catch(console.error); * .catch(console.error);
*/ */
async fetch(id, cache = true) { async fetch(id, cache = true, force = false) {
const existing = this.cache.get(id); if (!force) {
if (existing && !existing.partial) return existing; const existing = this.cache.get(id);
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.channels(id).get(); const data = await this.client.api.channels(id).get();
return this.add(data, null, cache); return this.add(data, null, cache);

View file

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

View file

@ -1,19 +1,18 @@
'use strict'; 'use strict';
const BaseManager = require('./BaseManager'); const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
const { TypeError } = require('../errors'); const { TypeError } = require('../errors');
const GuildEmoji = require('../structures/GuildEmoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const DataResolver = require('../util/DataResolver'); const DataResolver = require('../util/DataResolver');
/** /**
* Manages API methods for GuildEmojis and stores their cache. * Manages API methods for GuildEmojis and stores their cache.
* @extends {BaseManager} * @extends {BaseGuildEmojiManager}
*/ */
class GuildEmojiManager extends BaseManager { class GuildEmojiManager extends BaseGuildEmojiManager {
constructor(guild, iterable) { constructor(guild, iterable) {
super(guild.client, iterable, GuildEmoji); super(guild.client, iterable);
/** /**
* The guild this manager belongs to * The guild this manager belongs to
* @type {Guild} * @type {Guild}
@ -21,12 +20,6 @@ class GuildEmojiManager extends BaseManager {
this.guild = guild; this.guild = guild;
} }
/**
* The cache of GuildEmojis
* @type {Collection<Snowflake, GuildEmoji>}
* @name GuildEmojiManager#cache
*/
add(data, cache) { add(data, cache) {
return super.add(data, cache, { extras: [this.guild] }); return super.add(data, cache, { extras: [this.guild] });
} }
@ -73,57 +66,6 @@ class GuildEmojiManager extends BaseManager {
.emojis.post({ data, reason }) .emojis.post({ data, reason })
.then(emoji => this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji); .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; module.exports = GuildEmojiManager;

View file

@ -8,6 +8,7 @@ const GuildMember = require('../structures/GuildMember');
const Invite = require('../structures/Invite'); const Invite = require('../structures/Invite');
const Role = require('../structures/Role'); const Role = require('../structures/Role');
const { const {
ChannelTypes,
Events, Events,
VerificationLevels, VerificationLevels,
DefaultMessageNotifications, DefaultMessageNotifications,
@ -129,6 +130,8 @@ class GuildManager extends BaseManager {
* <warn>This is only available to bots in fewer than 10 guilds.</warn> * <warn>This is only available to bots in fewer than 10 guilds.</warn>
* @param {string} name The name of the guild * @param {string} name The name of the guild
* @param {Object} [options] Options for the creating * @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 {PartialChannelData[]} [options.channels] The channels for this guild
* @param {DefaultMessageNotifications} [options.defaultMessageNotifications] The default message notifications * @param {DefaultMessageNotifications} [options.defaultMessageNotifications] The default message notifications
* for the guild * 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 {string} [options.region] The region for the server, defaults to the closest one available
* @param {PartialRoleData[]} [options.roles] The roles for this guild, * @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. * 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 * @param {VerificationLevel} [options.verificationLevel] The verification level for the guild
* @returns {Promise<Guild>} The guild that was created * @returns {Promise<Guild>} The guild that was created
*/ */
async create( async create(
name, name,
{ {
afkChannelID,
afkTimeout,
channels = [], channels = [],
defaultMessageNotifications, defaultMessageNotifications,
explicitContentFilter, explicitContentFilter,
icon = null, icon = null,
region, region,
roles = [], roles = [],
systemChannelID,
verificationLevel, verificationLevel,
} = {}, } = {},
) { ) {
@ -163,6 +170,7 @@ class GuildManager extends BaseManager {
explicitContentFilter = ExplicitContentFilterLevels.indexOf(explicitContentFilter); explicitContentFilter = ExplicitContentFilterLevels.indexOf(explicitContentFilter);
} }
for (const channel of channels) { for (const channel of channels) {
if (channel.type) channel.type = ChannelTypes[channel.type.toUpperCase()];
channel.parent_id = channel.parentID; channel.parent_id = channel.parentID;
delete channel.parentID; delete channel.parentID;
if (!channel.permissionOverwrites) continue; if (!channel.permissionOverwrites) continue;
@ -187,8 +195,11 @@ class GuildManager extends BaseManager {
verification_level: verificationLevel, verification_level: verificationLevel,
default_message_notifications: defaultMessageNotifications, default_message_notifications: defaultMessageNotifications,
explicit_content_filter: explicitContentFilter, explicit_content_filter: explicitContentFilter,
channels,
roles, roles,
channels,
afk_channel_id: afkChannelID,
afk_timeout: afkTimeout,
system_channel_id: systemChannelID,
}, },
}) })
.then(data => { .then(data => {
@ -196,21 +207,46 @@ class GuildManager extends BaseManager {
const handleGuild = guild => { const handleGuild = guild => {
if (guild.id === data.id) { if (guild.id === data.id) {
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
this.client.clearTimeout(timeout); this.client.clearTimeout(timeout);
this.client.removeListener(Events.GUILD_CREATE, handleGuild);
this.client.decrementMaxListeners();
resolve(guild); resolve(guild);
} }
}; };
this.client.incrementMaxListeners();
this.client.on(Events.GUILD_CREATE, handleGuild); this.client.on(Events.GUILD_CREATE, handleGuild);
const timeout = this.client.setTimeout(() => { const timeout = this.client.setTimeout(() => {
this.client.removeListener(Events.GUILD_CREATE, handleGuild); this.client.removeListener(Events.GUILD_CREATE, handleGuild);
this.client.decrementMaxListeners();
resolve(this.client.guilds.add(data)); resolve(this.client.guilds.add(data));
}, 10000); }, 10000);
return undefined; return undefined;
}, reject), }, 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; module.exports = GuildManager;

View file

@ -1,10 +1,11 @@
'use strict'; 'use strict';
const BaseManager = require('./BaseManager'); const BaseManager = require('./BaseManager');
const { Error, TypeError } = require('../errors'); const { Error, TypeError, RangeError } = require('../errors');
const GuildMember = require('../structures/GuildMember'); const GuildMember = require('../structures/GuildMember');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const { Events, OPCodes } = require('../util/Constants'); const { Events, OPCodes } = require('../util/Constants');
const SnowflakeUtil = require('../util/Snowflake');
/** /**
* Manages API methods for GuildMembers and stores their cache. * Manages API methods for GuildMembers and stores their cache.
@ -67,6 +68,7 @@ class GuildMemberManager extends BaseManager {
* @typedef {Object} FetchMemberOptions * @typedef {Object} FetchMemberOptions
* @property {UserResolvable} user The user to fetch * @property {UserResolvable} user The user to fetch
* @property {boolean} [cache=true] Whether or not to cache the fetched member * @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 {?string} query Limit fetch to members with similar usernames
* @property {number} [limit=0] Maximum number of members to request * @property {number} [limit=0] Maximum number of members to request
* @property {boolean} [withPresences=false] Whether or not to include the presences * @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) * .then(console.log)
* .catch(console.error); * .catch(console.error);
* @example * @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 * // Fetch a single member without caching
* guild.members.fetch({ user, cache: false }) * guild.members.fetch({ user, cache: false })
* .then(console.log) * .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 {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.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 {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 * @param {string} [options.reason] Reason for this prune
* @returns {Promise<number|null>} The number of members that were/will be kicked * @returns {Promise<number|null>} The number of members that were/will be kicked
* @example * @example
@ -145,16 +156,39 @@ class GuildMemberManager extends BaseManager {
* guild.members.prune({ days: 1, reason: 'too many people!' }) * guild.members.prune({ days: 1, reason: 'too many people!' })
* .then(pruned => console.log(`I just pruned ${pruned} people!`)) * .then(pruned => console.log(`I just pruned ${pruned} people!`))
* .catch(console.error); * .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'); if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
return this.client.api
.guilds(this.guild.id) const query = { days };
.prune[dry ? 'get' : 'post']({ const resolvedRoles = [];
query: {
days, for (const role of roles) {
compute_prune_count: count, 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, reason,
}) })
.then(data => data.pruned); .then(data => data.pruned);
@ -164,7 +198,7 @@ class GuildMemberManager extends BaseManager {
* Bans a user from the guild. * Bans a user from the guild.
* @param {UserResolvable} user The user to ban * @param {UserResolvable} user The user to ban
* @param {Object} [options] Options for the 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 * @param {string} [options.reason] Reason for banning
* @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible. * @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 * 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); * .catch(console.error);
*/ */
ban(user, options = { days: 0 }) { 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); const id = this.client.users.resolveID(user);
if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true)); if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true));
return this.client.api return this.client.api
.guilds(this.guild.id) .guilds(this.guild.id)
.bans[id].put({ query: options }) .bans[id].put({ data: options })
.then(() => { .then(() => {
if (user instanceof GuildMember) return user; if (user instanceof GuildMember) return user;
const _user = this.client.users.resolve(id); const _user = this.client.users.resolve(id);
@ -213,9 +248,12 @@ class GuildMemberManager extends BaseManager {
.then(() => this.client.users.resolve(user)); .then(() => this.client.users.resolve(user));
} }
_fetchSingle({ user, cache }) { _fetchSingle({ user, cache, force = false }) {
const existing = this.cache.get(user); if (!force) {
if (existing && !existing.partial) return Promise.resolve(existing); const existing = this.cache.get(user);
if (existing && !existing.partial) return Promise.resolve(existing);
}
return this.client.api return this.client.api
.guilds(this.guild.id) .guilds(this.guild.id)
.members(user) .members(user)
@ -223,13 +261,22 @@ class GuildMemberManager extends BaseManager {
.then(data => this.add(data, cache)); .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) => { 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); resolve(this.cache);
return; return;
} }
if (!query && !user_ids) query = ''; if (!query && !user_ids) query = '';
if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH');
this.guild.shard.send({ this.guild.shard.send({
op: OPCodes.REQUEST_GUILD_MEMBERS, op: OPCodes.REQUEST_GUILD_MEMBERS,
d: { d: {
@ -237,33 +284,41 @@ class GuildMemberManager extends BaseManager {
presences, presences,
user_ids, user_ids,
query, query,
nonce,
limit, limit,
}, },
}); });
const fetchedMembers = new Collection(); const fetchedMembers = new Collection();
const option = query || limit || presences || user_ids; const option = query || limit || presences || user_ids;
const handler = (members, guild) => { let i = 0;
if (guild.id !== this.guild.id) return; const handler = (members, _, chunk) => {
timeout.refresh(); timeout.refresh();
if (chunk.nonce !== nonce) return;
i++;
for (const member of members.values()) { for (const member of members.values()) {
if (option) fetchedMembers.set(member.id, member); if (option) fetchedMembers.set(member.id, member);
} }
if ( if (
this.guild.memberCount <= this.cache.size || this.guild.memberCount <= this.cache.size ||
(option && members.size < 1000) || (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; let fetched = option ? fetchedMembers : this.cache;
if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first(); if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
resolve(fetched); resolve(fetched);
} }
}; };
const timeout = this.guild.client.setTimeout(() => { const timeout = this.client.setTimeout(() => {
this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
this.client.decrementMaxListeners();
reject(new Error('GUILD_MEMBERS_TIMEOUT')); reject(new Error('GUILD_MEMBERS_TIMEOUT'));
}, 120e3); }, time);
this.guild.client.on(Events.GUILD_MEMBERS_CHUNK, handler); this.client.incrementMaxListeners();
this.client.on(Events.GUILD_MEMBERS_CHUNK, handler);
}); });
} }
} }

View file

@ -90,12 +90,7 @@ class GuildMemberRoleManager {
} else { } else {
roleOrRoles = this.guild.roles.resolve(roleOrRoles); roleOrRoles = this.guild.roles.resolve(roleOrRoles);
if (roleOrRoles === null) { if (roleOrRoles === null) {
throw new TypeError( throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes');
'INVALID_TYPE',
'roles',
'Role, Snowflake or Array or Collection of Roles or Snowflakes',
true,
);
} }
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason }); 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'; 'use strict';
const BaseManager = require('./BaseManager'); const BaseManager = require('./BaseManager');
const { TypeError } = require('../errors');
const Message = require('../structures/Message'); const Message = require('../structures/Message');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const LimitedCollection = require('../util/LimitedCollection'); const LimitedCollection = require('../util/LimitedCollection');
@ -45,6 +46,7 @@ class MessageManager extends BaseManager {
* Those need to be fetched separately in such a case.</info> * 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 {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} [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>>} * @returns {Promise<Message>|Promise<Collection<Snowflake, Message>>}
* @example * @example
* // Get message * // Get message
@ -62,8 +64,8 @@ class MessageManager extends BaseManager {
* .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`))
* .catch(console.error); * .catch(console.error);
*/ */
fetch(message, cache = true) { fetch(message, cache = true, force = false) {
return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache); 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>>} * @returns {Promise<Collection<Snowflake, Message>>}
* @example * @example
* // Get pinned messages * // Get pinned messages
* channel.fetchPinned() * channel.messages.fetchPinned()
* .then(messages => console.log(`Received ${messages.size} messages`)) * .then(messages => console.log(`Received ${messages.size} messages`))
* .catch(console.error); * .catch(console.error);
*/ */
@ -115,20 +117,21 @@ class MessageManager extends BaseManager {
* Deletes a message, even if it's not cached. * Deletes a message, even if it's not cached.
* @param {MessageResolvable} message The message to delete * @param {MessageResolvable} message The message to delete
* @param {string} [reason] Reason for deleting this message, if it does not belong to the client user * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user
* @returns {Promise<void>}
*/ */
async delete(message, reason) { async delete(message, reason) {
message = this.resolveID(message); message = this.resolveID(message);
if (message) { if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api
.channels(this.channel.id) await this.client.api.channels(this.channel.id).messages(message).delete({ reason });
.messages(message)
.delete({ reason });
}
} }
async _fetchId(messageID, cache) { async _fetchId(messageID, cache, force) {
const existing = this.cache.get(messageID); if (!force) {
if (existing && !existing.partial) return existing; 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(); const data = await this.client.api.channels[this.channel.id].messages[messageID].get();
return this.add(data, cache); return this.add(data, cache);
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
'use strict'; 'use strict';
const https = require('https'); const https = require('https');
const FormData = require('@discordjs/form-data');
const AbortController = require('abort-controller'); const AbortController = require('abort-controller');
const FormData = require('form-data');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const { browser, UserAgent } = require('../util/Constants'); const { browser, UserAgent } = require('../util/Constants');
@ -15,11 +15,13 @@ class APIRequest {
this.method = method; this.method = method;
this.route = options.route; this.route = options.route;
this.options = options; this.options = options;
this.retries = 0;
let queryString = ''; let queryString = '';
if (options.query) { if (options.query) {
// Filter out undefined query options const query = Object.entries(options.query)
const query = Object.entries(options.query).filter(([, value]) => value !== null && typeof value !== 'undefined'); .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(); queryString = new URLSearchParams(query).toString();
} }
this.path = `${path}${queryString && `?${queryString}`}`; 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); 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 = {}) { request(method, url, options = {}) {
const apiRequest = new APIRequest(this, method, url, options); const apiRequest = new APIRequest(this, method, url, options);
let handler = this.handlers.get(apiRequest.route); let handler = this.handlers.get(apiRequest.route);
@ -57,7 +44,11 @@ class RESTManager {
this.handlers.set(apiRequest.route, handler); 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) { set endpoint(endpoint) {

View file

@ -1,5 +1,6 @@
'use strict'; 'use strict';
const AsyncQueue = require('./AsyncQueue');
const DiscordAPIError = require('./DiscordAPIError'); const DiscordAPIError = require('./DiscordAPIError');
const HTTPError = require('./HTTPError'); const HTTPError = require('./HTTPError');
const { const {
@ -25,46 +26,31 @@ function calculateReset(reset, serverDate) {
class RequestHandler { class RequestHandler {
constructor(manager) { constructor(manager) {
this.manager = manager; this.manager = manager;
this.busy = false; this.queue = new AsyncQueue();
this.queue = [];
this.reset = -1; this.reset = -1;
this.remaining = -1; this.remaining = -1;
this.limit = -1; this.limit = -1;
this.retryAfter = -1; this.retryAfter = -1;
} }
push(request) { async push(request) {
if (this.busy) { await this.queue.wait();
this.queue.push(request); try {
return this.run(); return await this.execute(request);
} else { } finally {
return this.execute(request); this.queue.shift();
} }
} }
run() {
if (this.queue.length === 0) return Promise.resolve();
return this.execute(this.queue.shift());
}
get limited() { get limited() {
return Boolean(this.manager.globalTimeout) || (this.remaining <= 0 && Date.now() < this.reset); return Boolean(this.manager.globalTimeout) || (this.remaining <= 0 && Date.now() < this.reset);
} }
get _inactive() { get _inactive() {
return this.queue.length === 0 && !this.limited && this.busy !== true; return this.queue.remaining === 0 && !this.limited;
} }
async execute(item) { async execute(request) {
// 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;
// After calculations and requests have been done, pre-emptively stop further requests // After calculations and requests have been done, pre-emptively stop further requests
if (this.limited) { if (this.limited) {
const timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now(); const timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
@ -102,9 +88,13 @@ class RequestHandler {
try { try {
res = await request.make(); res = await request.make();
} catch (error) { } catch (error) {
// NodeFetch error expected for all "operational" errors, such as 500 status code // Retry the specified number of times for request abortions
this.busy = false; if (request.retries === this.manager.client.options.retryLimit) {
return reject(new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path)); throw new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path);
}
request.retries++;
return this.execute(request);
} }
if (res && res.headers) { if (res && res.headers) {
@ -120,7 +110,7 @@ class RequestHandler {
this.retryAfter = retryAfter ? Number(retryAfter) : -1; this.retryAfter = retryAfter ? Number(retryAfter) : -1;
// https://github.com/discordapp/discord-api-docs/issues/182 // 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; this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250;
} }
@ -137,43 +127,46 @@ class RequestHandler {
} }
} }
// Finished handling headers, safe to unlock manager // Handle 2xx and 3xx responses
this.busy = false;
if (res.ok) { if (res.ok) {
const success = await parseResponse(res);
// Nothing wrong with the request, proceed with the next one // Nothing wrong with the request, proceed with the next one
resolve(success); return parseResponse(res);
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));
}
} }
// 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`) * Arguments for the shard's process executable (only when {@link ShardingManager#mode} is `process`)
* @type {?string[]} * @type {string[]}
*/ */
this.execArgv = manager.execArgv; this.execArgv = manager.execArgv;
@ -124,6 +124,9 @@ class Shard extends EventEmitter {
.on('exit', this._exitListener); .on('exit', this._exitListener);
} }
this._evals.clear();
this._fetches.clear();
/** /**
* Emitted upon the creation of the shard's child process/worker. * Emitted upon the creation of the shard's child process/worker.
* @event Shard#spawn * @event Shard#spawn
@ -225,6 +228,10 @@ class Shard extends EventEmitter {
* .catch(console.error); * .catch(console.error);
*/ */
fetchClientValue(prop) { 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); if (this._fetches.has(prop)) return this._fetches.get(prop);
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
@ -255,6 +262,10 @@ class Shard extends EventEmitter {
* @returns {Promise<*>} Result of the script execution * @returns {Promise<*>} Result of the script execution
*/ */
eval(script) { 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); if (this._evals.has(script)) return this._evals.get(script);
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
@ -323,18 +334,20 @@ class Shard extends EventEmitter {
// Shard is requesting a property fetch // Shard is requesting a property fetch
if (message._sFetchProp) { if (message._sFetchProp) {
this.manager.fetchClientValues(message._sFetchProp).then( const resp = { _sFetchProp: message._sFetchProp, _sFetchPropShard: message._sFetchPropShard };
results => this.send({ _sFetchProp: message._sFetchProp, _result: results }), this.manager.fetchClientValues(message._sFetchProp, message._sFetchPropShard).then(
err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }), results => this.send({ ...resp, _result: results }),
err => this.send({ ...resp, _error: Util.makePlainError(err) }),
); );
return; return;
} }
// Shard is requesting an eval broadcast // Shard is requesting an eval broadcast
if (message._sEval) { if (message._sEval) {
this.manager.broadcastEval(message._sEval).then( const resp = { _sEval: message._sEval, _sEvalShard: message._sEvalShard };
results => this.send({ _sEval: message._sEval, _result: results }), this.manager.broadcastEval(message._sEval, message._sEvalShard).then(
err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }), results => this.send({ ...resp, _result: results }),
err => this.send({ ...resp, _error: Util.makePlainError(err) }),
); );
return; return;
} }

View file

@ -79,6 +79,7 @@ class ShardClientUtil {
* Sends a message to the master process. * Sends a message to the master process.
* @param {*} message Message to send * @param {*} message Message to send
* @returns {Promise<void>} * @returns {Promise<void>}
* @emits Shard#message
*/ */
send(message) { send(message) {
return new Promise((resolve, reject) => { 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 * @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 * @example
* client.shard.fetchClientValues('guilds.cache.size') * client.shard.fetchClientValues('guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error); * .catch(console.error);
* @see {@link ShardingManager#fetchClientValues} * @see {@link ShardingManager#fetchClientValues}
*/ */
fetchClientValues(prop) { fetchClientValues(prop, shard) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const parent = this.parentPort || process; const parent = this.parentPort || process;
const listener = message => { const listener = message => {
if (!message || message._sFetchProp !== prop) return; if (!message || message._sFetchProp !== prop || message._sFetchPropShard !== shard) return;
parent.removeListener('message', listener); parent.removeListener('message', listener);
if (!message._error) resolve(message._result); if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error)); else reject(Util.makeError(message._error));
}; };
parent.on('message', listener); parent.on('message', listener);
this.send({ _sFetchProp: prop }).catch(err => { this.send({ _sFetchProp: prop, _sFetchPropShard: shard }).catch(err => {
parent.removeListener('message', listener); parent.removeListener('message', listener);
reject(err); 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 * @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 * @example
* client.shard.broadcastEval('this.guilds.cache.size') * client.shard.broadcastEval('this.guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error); * .catch(console.error);
* @see {@link ShardingManager#broadcastEval} * @see {@link ShardingManager#broadcastEval}
*/ */
broadcastEval(script) { broadcastEval(script, shard) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const parent = this.parentPort || process; const parent = this.parentPort || process;
script = typeof script === 'function' ? `(${script})(this)` : script; script = typeof script === 'function' ? `(${script})(this)` : script;
const listener = message => { const listener = message => {
if (!message || message._sEval !== script) return; if (!message || message._sEval !== script || message._sEvalShard !== shard) return;
parent.removeListener('message', listener); parent.removeListener('message', listener);
if (!message._error) resolve(message._result); if (!message._error) resolve(message._result);
else reject(Util.makeError(message._error)); else reject(Util.makeError(message._error));
}; };
parent.on('message', listener); parent.on('message', listener);
this.send({ _sEval: script }).catch(err => { this.send({ _sEval: script, _sEvalShard: shard }).catch(err => {
parent.removeListener('message', listener); parent.removeListener('message', listener);
reject(err); reject(err);
}); });
@ -223,6 +226,18 @@ class ShardClientUtil {
} }
return this._singleton; 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; module.exports = ShardClientUtil;

View file

@ -20,9 +20,7 @@ const Util = require('../util/Util');
class ShardingManager extends EventEmitter { class ShardingManager extends EventEmitter {
/** /**
* The mode to spawn shards with for a {@link ShardingManager}: either "process" to use child processes, or * 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" to use [Worker threads](https://nodejs.org/api/worker_threads.html).
* [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.
* @typedef {Object} ShardingManagerMode * @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 * @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) { broadcastEval(script, shard) {
const promises = []; return this._performOnShards('eval', [script], shard);
for (const shard of this.shards.values()) promises.push(shard.eval(script));
return Promise.all(promises);
} }
/** /**
* 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 * @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 * @example
* manager.fetchClientValues('guilds.cache.size') * manager.fetchClientValues('guilds.cache.size')
* .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`))
* .catch(console.error); * .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 === 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 (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 = []; 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); return Promise.all(promises);
} }

View file

@ -74,13 +74,21 @@ class APIMessage {
return this.target instanceof Message; 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. * Makes the content of this message.
* @returns {?(string|string[])} * @returns {?(string|string[])}
*/ */
makeContent() { makeContent() {
const GuildMember = require('./GuildMember');
let content; let content;
if (this.options.content === null) { if (this.options.content === null) {
content = ''; content = '';
@ -88,14 +96,16 @@ class APIMessage {
content = Util.resolveString(this.options.content); content = Util.resolveString(this.options.content);
} }
if (typeof content !== 'string') return content;
const disableMentions = const disableMentions =
typeof this.options.disableMentions === 'undefined' typeof this.options.disableMentions === 'undefined'
? this.target.client.options.disableMentions ? this.target.client.options.disableMentions
: this.options.disableMentions; : this.options.disableMentions;
if (disableMentions === 'all') { if (disableMentions === 'all') {
content = Util.removeMentions(content || ''); content = Util.removeMentions(content);
} else if (disableMentions === 'everyone') { } else if (disableMentions === 'everyone') {
content = (content || '').replace(/@([^<>@ ]*)/gmsu, (match, target) => { content = content.replace(/@([^<>@ ]*)/gmsu, (match, target) => {
if (target.match(/^[&!]?\d+$/)) { if (target.match(/^[&!]?\d+$/)) {
return `@${target}`; return `@${target}`;
} else { } else {
@ -108,29 +118,18 @@ class APIMessage {
const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false;
const splitOptions = isSplit ? { ...this.options.split } : undefined; const splitOptions = isSplit ? { ...this.options.split } : undefined;
let mentionPart = ''; if (content) {
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 (isCode) { if (isCode) {
const codeName = typeof this.options.code === 'string' ? this.options.code : ''; 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) { if (isSplit) {
splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`;
splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`;
} }
} else if (mentionPart) {
content = `${mentionPart}${content || ''}`;
} }
if (isSplit) { if (isSplit) {
content = Util.splitMessage(content || '', splitOptions); content = Util.splitMessage(content, splitOptions);
} }
} }
@ -149,8 +148,11 @@ class APIMessage {
let nonce; let nonce;
if (typeof this.options.nonce !== 'undefined') { if (typeof this.options.nonce !== 'undefined') {
nonce = parseInt(this.options.nonce); nonce = this.options.nonce;
if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); // eslint-disable-next-line max-len
if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') {
throw new RangeError('MESSAGE_NONCE_TYPE');
}
} }
const embedLikes = []; const embedLikes = [];
@ -174,6 +176,29 @@ class APIMessage {
if (this.isMessage) { if (this.isMessage) {
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield; 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 = { this.data = {
@ -184,8 +209,10 @@ class APIMessage {
embeds, embeds,
username, username,
avatar_url: avatarURL, avatar_url: avatarURL,
allowed_mentions: this.options.allowedMentions, allowed_mentions:
typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
flags, flags,
message_reference,
}; };
return this; return this;
} }
@ -239,8 +266,8 @@ class APIMessage {
data = { ...this.data, content: this.data.content[i] }; data = { ...this.data, content: this.data.content[i] };
opt = { ...this.options, content: this.data.content[i] }; opt = { ...this.options, content: this.data.content[i] };
} else { } else {
data = { 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 }; opt = { content: this.data.content[i], tts: this.data.tts, allowedMentions: this.options.allowedMentions };
} }
const apiMessage = new APIMessage(this.target, opt); 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). * Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models).
* @abstract
*/ */
class Base { class Base {
constructor(client) { constructor(client) {

View file

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

View file

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

View file

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

View file

@ -1,46 +1,15 @@
'use strict'; 'use strict';
const Base = require('./Base');
const Team = require('./Team'); const Team = require('./Team');
const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants'); const Application = require('./interfaces/Application');
const Snowflake = require('../util/Snowflake');
const AssetTypes = Object.keys(ClientApplicationAssetTypes);
/** /**
* Represents a Client OAuth2 Application. * Represents a Client OAuth2 Application.
* @extends {Base} * @extends {Application}
*/ */
class ClientApplication extends Base { class ClientApplication extends Application {
constructor(client, data) {
super(client);
this._patch(data);
}
_patch(data) { _patch(data) {
/** super._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;
/** /**
* The app's cover image * 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; 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; module.exports = ClientApplication;

View file

@ -94,11 +94,9 @@ class ClientUser extends Structures.get('User') {
* @property {PresenceStatusData} [status] Status of the user * @property {PresenceStatusData} [status] Status of the user
* @property {boolean} [afk] Whether the user is AFK * @property {boolean} [afk] Whether the user is AFK
* @property {Object} [activity] Activity the user is playing * @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 {string} [activity.name] Name of the activity
* @property {ActivityType|number} [activity.type] Type 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 * @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 * @typedef ActivityOptions
* @type {Object} * @type {Object}
* @property {string} [url] Twitch stream URL * @property {string} [url] Twitch / YouTube stream URL
* @property {ActivityType|number} [type] Type of the activity * @property {ActivityType|number} [type] Type of the activity
* @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on * @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. * Fetch this DMChannel.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<DMChannel>} * @returns {Promise<DMChannel>}
*/ */
fetch() { fetch(force = false) {
return this.recipient.createDM(); return this.recipient.createDM(force);
} }
/** /**

View file

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

View file

@ -1,12 +1,15 @@
'use strict'; 'use strict';
const { deprecate } = require('util');
const Base = require('./Base'); const Base = require('./Base');
const GuildAuditLogs = require('./GuildAuditLogs'); const GuildAuditLogs = require('./GuildAuditLogs');
const GuildPreview = require('./GuildPreview'); const GuildPreview = require('./GuildPreview');
const GuildTemplate = require('./GuildTemplate');
const Integration = require('./Integration'); const Integration = require('./Integration');
const Invite = require('./Invite'); const Invite = require('./Invite');
const VoiceRegion = require('./VoiceRegion'); const VoiceRegion = require('./VoiceRegion');
const Webhook = require('./Webhook'); const Webhook = require('./Webhook');
const { Error, TypeError } = require('../errors');
const GuildChannelManager = require('../managers/GuildChannelManager'); const GuildChannelManager = require('../managers/GuildChannelManager');
const GuildEmojiManager = require('../managers/GuildEmojiManager'); const GuildEmojiManager = require('../managers/GuildEmojiManager');
const GuildMemberManager = require('../managers/GuildMemberManager'); const GuildMemberManager = require('../managers/GuildMemberManager');
@ -15,6 +18,7 @@ const RoleManager = require('../managers/RoleManager');
const VoiceStateManager = require('../managers/VoiceStateManager'); const VoiceStateManager = require('../managers/VoiceStateManager');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const { const {
browser,
ChannelTypes, ChannelTypes,
DefaultMessageNotifications, DefaultMessageNotifications,
PartialTypes, PartialTypes,
@ -129,11 +133,17 @@ class Guild extends Base {
this.icon = data.icon; this.icon = data.icon;
/** /**
* The hash of the guild splash image (VIP only) * The hash of the guild invite splash image
* @type {?string} * @type {?string}
*/ */
this.splash = data.splash; 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 * The region the guild is located in
* @type {string} * @type {string}
@ -147,7 +157,7 @@ class Guild extends Base {
this.memberCount = data.member_count || this.memberCount; 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} * @type {boolean}
*/ */
this.large = Boolean('large' in data ? data.large : this.large); this.large = Boolean('large' in data ? data.large : this.large);
@ -157,15 +167,17 @@ class Guild extends Base {
* * ANIMATED_ICON * * ANIMATED_ICON
* * BANNER * * BANNER
* * COMMERCE * * COMMERCE
* * COMMUNITY
* * DISCOVERABLE * * DISCOVERABLE
* * FEATURABLE * * FEATURABLE
* * INVITE_SPLASH * * INVITE_SPLASH
* * PUBLIC
* * NEWS * * NEWS
* * PARTNERED * * PARTNERED
* * RELAY_ENABLED
* * VANITY_URL * * VANITY_URL
* * VERIFIED * * VERIFIED
* * VIP_REGIONS * * VIP_REGIONS
* * WELCOME_SCREEN_ENABLED
* @typedef {string} Features * @typedef {string} Features
*/ */
@ -202,6 +214,7 @@ class Guild extends Base {
/** /**
* Whether embedded images are enabled on this guild * Whether embedded images are enabled on this guild
* @type {boolean} * @type {boolean}
* @deprecated
*/ */
this.embedEnabled = data.embed_enabled; this.embedEnabled = data.embed_enabled;
@ -220,35 +233,38 @@ class Guild extends Base {
*/ */
this.premiumTier = data.premium_tier; 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') { if (typeof data.premium_subscription_count !== 'undefined') {
/**
* The total number of boosts for this server
* @type {?number}
*/
this.premiumSubscriptionCount = data.premium_subscription_count; this.premiumSubscriptionCount = data.premium_subscription_count;
} }
/** if (typeof data.widget_enabled !== 'undefined') {
* Whether widget images are enabled on this guild /**
* @type {?boolean} * Whether widget images are enabled on this guild
* @name Guild#widgetEnabled * @type {?boolean}
*/ */
if (typeof data.widget_enabled !== 'undefined') this.widgetEnabled = data.widget_enabled; this.widgetEnabled = data.widget_enabled;
}
/** if (typeof data.widget_channel_id !== 'undefined') {
* The widget channel ID, if enabled /**
* @type {?string} * The widget channel ID, if enabled
* @name Guild#widgetChannelID * @type {?string}
*/ */
if (typeof data.widget_channel_id !== 'undefined') this.widgetChannelID = data.widget_channel_id; this.widgetChannelID = data.widget_channel_id;
}
/** if (typeof data.embed_channel_id !== 'undefined') {
* The embed channel ID, if enabled /**
* @type {?string} * The embed channel ID, if enabled
* @name Guild#embedChannelID * @type {?string}
*/ * @deprecated
if (typeof data.embed_channel_id !== 'undefined') this.embedChannelID = data.embed_channel_id; */
this.embedChannelID = data.embed_channel_id;
}
/** /**
* The verification level of the guild * The verification level of the guild
@ -287,28 +303,64 @@ class Guild extends Base {
*/ */
this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze(); this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze();
/** if (typeof data.max_members !== 'undefined') {
* 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> * The maximum amount of members the guild can have
* @type {?number} * @type {?number}
* @name Guild#maximumMembers */
*/ this.maximumMembers = data.max_members;
if (typeof data.max_members !== 'undefined') this.maximumMembers = data.max_members || 250000; } 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 * The vanity invite code of the guild, if any
* <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
* @type {?string} * @type {?string}
*/ */
this.vanityURLCode = data.vanity_url_code; 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 * The description of the guild, if any
* @type {?string} * @type {?string}
@ -327,18 +379,22 @@ class Guild extends Base {
/** /**
* The ID of the rules channel for the guild * The ID of the rules channel for the guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?Snowflake} * @type {?Snowflake}
*/ */
this.rulesChannelID = data.rules_channel_id; this.rulesChannelID = data.rules_channel_id;
/** /**
* The ID of the public updates channel for the guild * The ID of the community updates channel for the guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?Snowflake} * @type {?Snowflake}
*/ */
this.publicUpdatesChannelID = data.public_updates_channel_id; 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) { if (data.channels) {
this.channels.cache.clear(); this.channels.cache.clear();
for (const rawChannel of data.channels) { for (const rawChannel of data.channels) {
@ -463,11 +519,14 @@ class Guild extends Base {
* @readonly * @readonly
*/ */
get nameAcronym() { 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 * @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string} * @returns {?string}
*/ */
@ -476,6 +535,16 @@ class Guild extends Base {
return this.client.rest.cdn.Splash(this.id, this.splash, format, size); 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 * The owner of the guild
* @type {?GuildMember} * @type {?GuildMember}
@ -521,6 +590,7 @@ class Guild extends Base {
* Embed channel for this guild * Embed channel for this guild
* @type {?TextChannel} * @type {?TextChannel}
* @readonly * @readonly
* @deprecated
*/ */
get embedChannel() { get embedChannel() {
return this.client.channels.cache.get(this.embedChannelID) || null; return this.client.channels.cache.get(this.embedChannelID) || null;
@ -528,7 +598,6 @@ class Guild extends Base {
/** /**
* Rules channel for this guild * Rules channel for this guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?TextChannel} * @type {?TextChannel}
* @readonly * @readonly
*/ */
@ -538,7 +607,6 @@ class Guild extends Base {
/** /**
* Public updates channel for this guild * Public updates channel for this guild
* <info>This is only available on guilds with the `PUBLIC` feature</info>
* @type {?TextChannel} * @type {?TextChannel}
* @readonly * @readonly
*/ */
@ -569,18 +637,6 @@ class Guild extends Base {
return this.voiceStates.cache.get(this.client.user.id); 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. * Fetches this guild.
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
@ -588,7 +644,7 @@ class Guild extends Base {
fetch() { fetch() {
return this.client.api return this.client.api
.guilds(this.id) .guilds(this.id)
.get() .get({ query: { with_counts: true } })
.then(data => { .then(data => {
this._patch(data); this._patch(data);
return this; return this;
@ -642,6 +698,8 @@ class Guild extends Base {
/** /**
* Fetches a collection of integrations to this guild. * Fetches a collection of integrations to this guild.
* Resolves with a collection mapping integrations by their ids. * 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>>} * @returns {Promise<Collection<string, Integration>>}
* @example * @example
* // Fetch integrations * // Fetch integrations
@ -649,10 +707,14 @@ class Guild extends Base {
* .then(integrations => console.log(`Fetched ${integrations.size} integrations`)) * .then(integrations => console.log(`Fetched ${integrations.size} integrations`))
* .catch(console.error); * .catch(console.error);
*/ */
fetchIntegrations() { fetchIntegrations({ includeApplications = false } = {}) {
return this.client.api return this.client.api
.guilds(this.id) .guilds(this.id)
.integrations.get() .integrations.get({
query: {
include_applications: includeApplications,
},
})
.then(data => .then(data =>
data.reduce( data.reduce(
(collection, integration) => collection.set(integration.id, new Integration(this.client, integration, this)), (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. * The data for creating an integration.
* @typedef {Object} IntegrationData * @typedef {Object} IntegrationData
@ -681,6 +757,19 @@ class Guild extends Base {
.then(() => this); .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. * Fetches a collection of invites to this guild.
* Resolves with a collection mapping invites by their codes. * 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>} * @returns {Promise<GuildPreview>}
*/ */
fetchPreview() { fetchPreview() {
@ -725,6 +814,7 @@ class Guild extends Base {
* Fetches the vanity url invite code to this guild. * Fetches the vanity url invite code to this guild.
* Resolves with a string matching the vanity url invite code, not the full url. * Resolves with a string matching the vanity url invite code, not the full url.
* @returns {Promise<string>} * @returns {Promise<string>}
* @deprecated
* @example * @example
* // Fetch invites * // Fetch invites
* guild.fetchVanityCode() * guild.fetchVanityCode()
@ -734,13 +824,36 @@ class Guild extends Base {
* .catch(console.error); * .catch(console.error);
*/ */
fetchVanityCode() { 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')) { if (!this.features.includes('VANITY_URL')) {
return Promise.reject(new Error('VANITY_URL')); throw new Error('VANITY_URL');
} }
return this.client.api const data = await this.client.api.guilds(this.id, 'vanity-url').get();
.guilds(this.id, 'vanity-url') this.vanityURLUses = data.uses;
.get()
.then(res => res.code); return data;
} }
/** /**
@ -779,15 +892,23 @@ class Guild extends Base {
} }
/** /**
* The Guild Embed object * Data for the Guild Widget object
* @typedef {Object} GuildEmbedData * @typedef {Object} GuildWidget
* @property {boolean} enabled Whether the embed is enabled * @property {boolean} enabled Whether the widget is enabled
* @property {?GuildChannel} channel The embed channel * @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. * Fetches the guild embed.
* @returns {Promise<GuildEmbedData>} * @returns {Promise<GuildWidget>}
* @deprecated
* @example * @example
* // Fetches the guild embed * // Fetches the guild embed
* guild.fetchEmbed() * guild.fetchEmbed()
@ -795,13 +916,26 @@ class Guild extends Base {
* .catch(console.error); * .catch(console.error);
*/ */
fetchEmbed() { fetchEmbed() {
return this.client.api return this.fetchWidget();
.guilds(this.id) }
.embed.get()
.then(data => ({ /**
enabled: data.enabled, * Fetches the guild widget.
channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null, * @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`) * @param {boolean} [options.deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`)
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
addMember(user, options) { async addMember(user, options) {
user = this.client.users.resolveID(user); user = this.client.users.resolveID(user);
if (!user) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable')); if (!user) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
if (this.members.cache.has(user)) return Promise.resolve(this.members.cache.get(user)); if (this.members.cache.has(user)) return this.members.cache.get(user);
options.access_token = options.accessToken; options.access_token = options.accessToken;
if (options.roles) { if (options.roles) {
const roles = []; const roles = [];
for (let role of options.roles instanceof Collection ? options.roles.values() : options.roles) { for (let role of options.roles instanceof Collection ? options.roles.values() : options.roles) {
role = this.roles.resolve(role); role = this.roles.resolve(role);
if (!role) { if (!role) {
return Promise.reject( throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true);
new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true),
);
} }
roles.push(role.id); roles.push(role.id);
} }
options.roles = roles; options.roles = roles;
} }
return this.client.api const data = await this.client.api.guilds(this.id).members(user).put({ data: options });
.guilds(this.id) // Data is an empty buffer if the member is already part of the guild.
.members(user) return data instanceof (browser ? ArrayBuffer : Buffer) ? this.members.fetch(user) : this.members.add(data);
.put({ data: options })
.then(data => this.members.add(data));
} }
/** /**
@ -885,10 +1015,14 @@ class Guild extends Base {
* @property {number} [afkTimeout] The AFK timeout of the guild * @property {number} [afkTimeout] The AFK timeout of the guild
* @property {Base64Resolvable} [icon] The icon of the guild * @property {Base64Resolvable} [icon] The icon of the guild
* @property {GuildMemberResolvable} [owner] The owner 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 {Base64Resolvable} [banner] The banner of the guild
* @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications * @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications
* @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild * @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 (typeof data.icon !== 'undefined') _data.icon = data.icon;
if (data.owner) _data.owner_id = this.client.users.resolveID(data.owner); if (data.owner) _data.owner_id = this.client.users.resolveID(data.owner);
if (data.splash) _data.splash = data.splash; if (data.splash) _data.splash = data.splash;
if (data.discoverySplash) _data.discovery_splash = data.discoverySplash;
if (data.banner) _data.banner = data.banner; if (data.banner) _data.banner = data.banner;
if (typeof data.explicitContentFilter !== 'undefined') { if (typeof data.explicitContentFilter !== 'undefined') {
_data.explicit_content_filter = _data.explicit_content_filter =
@ -941,6 +1076,13 @@ class Guild extends Base {
if (typeof data.systemChannelFlags !== 'undefined') { if (typeof data.systemChannelFlags !== 'undefined') {
_data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); _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 return this.client.api
.guilds(this.id) .guilds(this.id)
.patch({ data: _data, reason }) .patch({ data: _data, reason })
@ -987,7 +1129,7 @@ class Guild extends Base {
* @example * @example
* // Edit the guild name * // Edit the guild name
* guild.setName('Discord Guild') * 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); * .catch(console.error);
*/ */
setName(name, reason) { setName(name, reason) {
@ -1100,9 +1242,9 @@ class Guild extends Base {
} }
/** /**
* Sets a new guild splash screen. * Sets a new guild invite splash image.
* @param {Base64Resolvable|BufferResolvable} splash The new splash screen of the guild * @param {Base64Resolvable|BufferResolvable} splash The new invite splash image of the guild
* @param {string} [reason] Reason for changing the guild's splash screen * @param {string} [reason] Reason for changing the guild's invite splash image
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
* // Edit the guild splash * // Edit the guild splash
@ -1114,6 +1256,21 @@ class Guild extends Base {
return this.edit({ splash: await DataResolver.resolveImage(splash), reason }); 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. * Sets a new guild banner.
* @param {Base64Resolvable|BufferResolvable} banner The new banner of the guild * @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 }); 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. * The data needed for updating a channel's position.
* @typedef {Object} ChannelPosition * @typedef {Object} ChannelPosition
@ -1202,23 +1404,51 @@ class Guild extends Base {
/** /**
* Edits the guild's embed. * 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 * @param {string} [reason] Reason for changing the guild's embed
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @deprecated
*/ */
setEmbed(embed, reason) { 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 return this.client.api
.guilds(this.id) .guilds(this.id)
.embed.patch({ .widget.patch({
data: { data: {
enabled: embed.enabled, enabled: widget.enabled,
channel_id: this.channels.resolveID(embed.channel), channel_id: this.channels.resolveID(widget.channel),
}, },
reason, reason,
}) })
.then(() => this); .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. * Leaves the guild.
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
@ -1267,6 +1497,7 @@ class Guild extends Base {
this.id === guild.id && this.id === guild.id &&
this.available === guild.available && this.available === guild.available &&
this.splash === guild.splash && this.splash === guild.splash &&
this.discoverySplash === guild.discoverySplash &&
this.region === guild.region && this.region === guild.region &&
this.name === guild.name && this.name === guild.name &&
this.memberCount === guild.memberCount && this.memberCount === guild.memberCount &&
@ -1311,6 +1542,7 @@ class Guild extends Base {
}); });
json.iconURL = this.iconURL(); json.iconURL = this.iconURL();
json.splashURL = this.splashURL(); json.splashURL = this.splashURL();
json.discoverySplashURL = this.discoverySplashURL();
json.bannerURL = this.bannerURL(); json.bannerURL = this.bannerURL();
return json; return json;
} }
@ -1333,9 +1565,24 @@ class Guild extends Base {
_sortedChannels(channel) { _sortedChannels(channel) {
const category = channel.type === ChannelTypes.CATEGORY; const category = channel.type === ChannelTypes.CATEGORY;
return Util.discordSort( 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; module.exports = Guild;

View file

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

View file

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

View file

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

View file

@ -1,13 +1,12 @@
'use strict'; 'use strict';
const Base = require('./Base'); const Base = require('./Base');
const { Presence } = require('./Presence');
const Role = require('./Role'); const Role = require('./Role');
const VoiceState = require('./VoiceState');
const TextBasedChannel = require('./interfaces/TextBasedChannel'); const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { Error } = require('../errors'); const { Error } = require('../errors');
const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager'); const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
let Structures;
/** /**
* Represents a member of a guild on Discord. * Represents a member of a guild on Discord.
@ -29,13 +28,6 @@ class GuildMember extends Base {
*/ */
this.guild = guild; 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 * The timestamp the member joined the guild at
* @type {?number} * @type {?number}
@ -66,23 +58,29 @@ class GuildMember extends Base {
*/ */
this.deleted = false; this.deleted = false;
/**
* The nickname of this member, if they have one
* @type {?string}
*/
this.nickname = null;
this._roles = []; this._roles = [];
if (data) this._patch(data); if (data) this._patch(data);
} }
_patch(data) { _patch(data) {
/** if ('user' in data) {
* The nickname of this member, if they have one /**
* @type {?string} * The user that this guild member instance represents
* @name GuildMember#nickname * @type {User}
*/ */
if (typeof data.nick !== 'undefined') this.nickname = data.nick; this.user = this.client.users.add(data.user, true);
}
if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); if ('nick' in data) this.nickname = data.nick;
if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime(); 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 (data.user) this.user = this.guild.client.users.add(data.user); if ('roles' in data) this._roles = data.roles;
if (data.roles) this._roles = data.roles;
} }
_clone() { _clone() {
@ -125,6 +123,8 @@ class GuildMember extends Base {
* @readonly * @readonly
*/ */
get voice() { 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 }); 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 * @readonly
*/ */
get presence() { get presence() {
if (!Structures) Structures = require('../util/Structures');
const Presence = Structures.get('Presence');
return ( return (
this.guild.presences.cache.get(this.id) || this.guild.presences.cache.get(this.id) ||
new Presence(this.client, { 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 * The nickname of this member, or their username if they don't have one
* @type {string} * @type {?string}
* @readonly * @readonly
*/ */
get displayName() { get displayName() {
@ -265,7 +267,8 @@ class GuildMember extends Base {
*/ */
hasPermission(permission, { checkAdmin = true, checkOwner = true } = {}) { hasPermission(permission, { checkAdmin = true, checkOwner = true } = {}) {
if (checkOwner && this.user.id === this.guild.ownerID) return 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. * Bans this guild member.
* @param {Object} [options] Options for the 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 * @param {string} [options.reason] Reason for banning
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
* @example * @example
@ -371,10 +374,11 @@ class GuildMember extends Base {
/** /**
* Fetches this GuildMember. * Fetches this GuildMember.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
fetch() { fetch(force = false) {
return this.guild.members.fetch(this.id, true); 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'); 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} * @extends {Base}
*/ */
class GuildPreview extends Base { class GuildPreview extends Base {
@ -18,37 +18,37 @@ class GuildPreview extends Base {
} }
/** /**
* Builds the public guild with the provided data. * Builds the guild with the provided data.
* @param {*} data The raw data of the public guild * @param {*} data The raw data of the guild
* @private * @private
*/ */
_patch(data) { _patch(data) {
/** /**
* The id of this public guild * The id of this guild
* @type {string} * @type {string}
*/ */
this.id = data.id; this.id = data.id;
/** /**
* The name of this public guild * The name of this guild
* @type {string} * @type {string}
*/ */
this.name = data.name; this.name = data.name;
/** /**
* The icon of this public guild * The icon of this guild
* @type {?string} * @type {?string}
*/ */
this.icon = data.icon; this.icon = data.icon;
/** /**
* The splash icon of this public guild * The splash icon of this guild
* @type {?string} * @type {?string}
*/ */
this.splash = data.splash; this.splash = data.splash;
/** /**
* The discovery splash icon of this public guild * The discovery splash icon of this guild
* @type {?string} * @type {?string}
*/ */
this.discoverySplash = data.discovery_splash; this.discoverySplash = data.discovery_splash;
@ -60,26 +60,26 @@ class GuildPreview extends Base {
this.features = data.features; this.features = data.features;
/** /**
* The approximate count of members in this public guild * The approximate count of members in this guild
* @type {number} * @type {number}
*/ */
this.approximateMemberCount = data.approximate_member_count; 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} * @type {number}
*/ */
this.approximatePresenceCount = data.approximate_presence_count; this.approximatePresenceCount = data.approximate_presence_count;
/** /**
* The description for this public guild * The description for this guild
* @type {?string} * @type {?string}
*/ */
this.description = data.description; this.description = data.description || null;
if (!this.emojis) { if (!this.emojis) {
/** /**
* Collection of emojis belonging to this public guild * Collection of emojis belonging to this guild
* @type {Collection<Snowflake, GuildPreviewEmoji>} * @type {Collection<Snowflake, GuildPreviewEmoji>}
*/ */
this.emojis = new Collection(); 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 * @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string} * @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 * @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string} * @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 * @param {ImageURLOptions} [options={}] Options for the Image URL
* @returns {?string} * @returns {?string}
*/ */
@ -122,7 +122,7 @@ class GuildPreview extends Base {
} }
/** /**
* Fetches this public guild. * Fetches this guild.
* @returns {Promise<GuildPreview>} * @returns {Promise<GuildPreview>}
*/ */
fetch() { 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'; 'use strict';
const Base = require('./Base'); const Base = require('./Base');
const IntegrationApplication = require('./IntegrationApplication');
/** /**
* The information account for an integration * The information account for an integration
@ -58,11 +59,15 @@ class Integration extends Base {
*/ */
this.role = this.guild.roles.cache.get(data.role_id); this.role = this.guild.roles.cache.get(data.role_id);
/** if (data.user) {
* The user for this integration /**
* @type {User} * The user for this integration
*/ * @type {?User}
this.user = this.client.users.add(data.user); */
this.user = this.client.users.add(data.user);
} else {
this.user = null;
}
/** /**
* The account integration information * The account integration information
@ -90,6 +95,20 @@ class Integration extends Base {
* @type {number} * @type {number}
*/ */
this.expireGracePeriod = data.expire_grace_period; 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 { MessageTypes } = require('../util/Constants');
const MessageFlags = require('../util/MessageFlags'); const MessageFlags = require('../util/MessageFlags');
const Permissions = require('../util/Permissions'); const Permissions = require('../util/Permissions');
const SnowflakeUtil = require('../util/Snowflake');
const Util = require('../util/Util'); const Util = require('../util/Util');
/** /**
@ -23,14 +24,14 @@ class Message extends Base {
/** /**
* @param {Client} client The instantiating client * @param {Client} client The instantiating client
* @param {Object} data The data for the message * @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) { constructor(client, data, channel) {
super(client); super(client);
/** /**
* The channel that the message was sent in * The channel that the message was sent in
* @type {TextChannel|DMChannel} * @type {TextChannel|DMChannel|NewsChannel}
*/ */
this.channel = channel; this.channel = channel;
@ -50,35 +51,62 @@ class Message extends Base {
*/ */
this.id = data.id; this.id = data.id;
/** if ('type' in data) {
* The type of the message /**
* @type {MessageType} * The type of the message
*/ * @type {?MessageType}
this.type = MessageTypes[data.type]; */
this.type = MessageTypes[data.type];
/** /**
* The content of the message * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {string} * @type {?boolean}
*/ */
this.content = data.content; this.system = data.type !== 0;
} else if (typeof this.type !== 'string') {
this.system = null;
this.type = null;
}
/** if ('content' in data) {
* The author of the message /**
* @type {?User} * The content of the message
*/ * @type {?string}
this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null; */
this.content = data.content;
} else if (typeof this.content !== 'string') {
this.content = null;
}
/** if ('author' in data) {
* Whether or not this message is pinned /**
* @type {boolean} * The author of the message
*/ * @type {?User}
this.pinned = data.pinned; */
this.author = this.client.users.add(data.author, !data.webhook_id);
} else if (!this.author) {
this.author = null;
}
/** if ('pinned' in data) {
* Whether or not the message was Text-To-Speech /**
* @type {boolean} * Whether or not this message is pinned
*/ * @type {?boolean}
this.tts = data.tts; */
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 * A random number or string used for checking message delivery
@ -86,13 +114,7 @@ class Message extends Base {
* lost if re-fetched</warn> * lost if re-fetched</warn>
* @type {?string} * @type {?string}
*/ */
this.nonce = data.nonce; this.nonce = 'nonce' in data ? data.nonce : null;
/**
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {boolean}
*/
this.system = data.type !== 0;
/** /**
* A list of embeds in the message - e.g. YouTube Player * 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 * The timestamp the message was sent at
* @type {number} * @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) * The timestamp the message was last edited at (if applicable)
* @type {?number} * @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 * A manager of the reactions belonging to this message
@ -183,11 +205,11 @@ class Message extends Base {
this.flags = new MessageFlags(data.flags).freeze(); 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 * @typedef {Object} MessageReference
* @property {string} channelID ID of the channel the message was crossposted from * @property {string} channelID ID of the channel the message was referenced
* @property {?string} guildID ID of the guild the message was crossposted from * @property {?string} guildID ID of the guild the message was referenced
* @property {?string} messageID ID of the message that was crossposted * @property {?string} messageID ID of the message that was referenced
*/ */
/** /**
@ -201,6 +223,10 @@ class Message extends Base {
messageID: data.message_reference.message_id, messageID: data.message_reference.message_id,
} }
: null; : 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 * @param {Object} data Raw Discord message update data
* @returns {Message}
* @private * @private
*/ */
patch(data) { patch(data) {
const clone = this._clone(); 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 ('edited_timestamp' in data) this.editedTimestamp = new Date(data.edited_timestamp).getTime();
if ('content' in data) this.content = data.content; if ('content' in data) this.content = data.content;
@ -240,12 +271,14 @@ class Message extends Base {
this.mentions = new Mentions( this.mentions = new Mentions(
this, this,
'mentions' in data ? data.mentions : this.mentions.users, '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_everyone' in data ? data.mention_everyone : this.mentions.everyone,
'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels, 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels,
); );
this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze();
return clone;
} }
/** /**
@ -255,7 +288,7 @@ class Message extends Base {
* @readonly * @readonly
*/ */
get member() { 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 * @readonly
*/ */
get url() { 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. * Options that can be passed into editMessage.
* @typedef {Object} MessageEditOptions * @typedef {Object} MessageEditOptions
* @property {string} [content] Content to be edited * @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 {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>} * @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 return this.client.api
.channels(this.channel.id) .channels(this.channel.id)
.pins(this.id) .pins(this.id)
.put() .put(options)
.then(() => this); .then(() => this);
} }
/** /**
* Unpins this message from the channel's pinned messages. * 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>} * @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 return this.client.api
.channels(this.channel.id) .channels(this.channel.id)
.pins(this.id) .pins(this.id)
.delete() .delete(options)
.then(() => this); .then(() => this);
} }
@ -492,12 +585,12 @@ class Message extends Base {
* @returns {Promise<Message>} * @returns {Promise<Message>}
* @example * @example
* // Delete a message * // Delete a message
* message.delete() * message.delete({ timeout: 5000 })
* .then(msg => console.log(`Deleted message from ${msg.author.username}`)) * .then(msg => console.log(`Deleted message from ${msg.author.username} after 5 seconds`))
* .catch(console.error); * .catch(console.error);
*/ */
delete(options = {}) { 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; const { timeout = 0, reason } = options;
if (timeout <= 0) { if (timeout <= 0) {
return this.channel.messages.delete(this.id, reason).then(() => this); 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 {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[]>} * @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) { reply(content, options) {
return this.channel.send( return this.channel.send(
content instanceof APIMessage content instanceof APIMessage
? content ? content
: APIMessage.transformOptions(content, options, { reply: this.member || this.author }), : APIMessage.transformOptions(content, options, {
replyTo: this,
}),
); );
} }
/** /**
* Fetch this message. * Fetch this message.
* @param {boolean} [force=false] Whether to skip the cache check and request the API
* @returns {Promise<Message>} * @returns {Promise<Message>}
*/ */
fetch() { fetch(force = false) {
return this.channel.messages.fetch(this.id, true); 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._handleChannelDeletion = this._handleChannelDeletion.bind(this);
this._handleGuildDeletion = this._handleGuildDeletion.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_CREATE, this.handleCollect);
this.client.on(Events.MESSAGE_DELETE, this.handleDispose); this.client.on(Events.MESSAGE_DELETE, this.handleDispose);
this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); 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.MESSAGE_BULK_DELETE, bulkDeleteListener);
this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); 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.) * Represents an embed in a message (image/video preview, rich embed, etc.)
*/ */
class MessageEmbed { class MessageEmbed {
/**
* @name MessageEmbed
* @kind constructor
* @memberof MessageEmbed
* @param {MessageEmbed|Object} [data={}] MessageEmbed to clone or raw embed data
*/
constructor(data = {}, skipValidation = false) { constructor(data = {}, skipValidation = false) {
this.setup(data, skipValidation); this.setup(data, skipValidation);
} }
@ -22,39 +29,40 @@ class MessageEmbed {
* * `link` - a link embed * * `link` - a link embed
* @type {string} * @type {string}
*/ */
this.type = data.type; this.type = data.type || 'rich';
/** /**
* The title of this embed * The title of this embed
* @type {?string} * @type {?string}
*/ */
this.title = data.title; this.title = 'title' in data ? data.title : null;
/** /**
* The description of this embed * The description of this embed
* @type {?string} * @type {?string}
*/ */
this.description = data.description; this.description = 'description' in data ? data.description : null;
/** /**
* The URL of this embed * The URL of this embed
* @type {?string} * @type {?string}
*/ */
this.url = data.url; this.url = 'url' in data ? data.url : null;
/** /**
* The color of this embed * The color of this embed
* @type {?number} * @type {?number}
*/ */
this.color = Util.resolveColor(data.color); this.color = 'color' in data ? Util.resolveColor(data.color) : null;
/** /**
* The timestamp of this embed * The timestamp of this embed
* @type {?number} * @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 * @typedef {Object} EmbedField
* @property {string} name The name of this field * @property {string} name The name of this field
* @property {string} value The value 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 * @typedef {Object} MessageEmbedThumbnail
* @property {string} url URL for this thumbnail * @property {string} url URL for this thumbnail
* @property {string} proxyURL ProxyURL for this thumbnail * @property {string} proxyURL ProxyURL for this thumbnail
@ -92,6 +101,7 @@ class MessageEmbed {
: null; : null;
/** /**
* Represents the image of a MessageEmbed
* @typedef {Object} MessageEmbedImage * @typedef {Object} MessageEmbedImage
* @property {string} url URL for this image * @property {string} url URL for this image
* @property {string} proxyURL ProxyURL for this image * @property {string} proxyURL ProxyURL for this image
@ -113,6 +123,7 @@ class MessageEmbed {
: null; : null;
/** /**
* Represents the video of a MessageEmbed
* @typedef {Object} MessageEmbedVideo * @typedef {Object} MessageEmbedVideo
* @property {string} url URL of this video * @property {string} url URL of this video
* @property {string} proxyURL ProxyURL for this video * @property {string} proxyURL ProxyURL for this video
@ -135,6 +146,7 @@ class MessageEmbed {
: null; : null;
/** /**
* Represents the author field of a MessageEmbed
* @typedef {Object} MessageEmbedAuthor * @typedef {Object} MessageEmbedAuthor
* @property {string} name The name of this author * @property {string} name The name of this author
* @property {string} url URL of this author * @property {string} url URL of this author
@ -156,6 +168,7 @@ class MessageEmbed {
: null; : null;
/** /**
* Represents the provider of a MessageEmbed
* @typedef {Object} MessageEmbedProvider * @typedef {Object} MessageEmbedProvider
* @property {string} name The name of this provider * @property {string} name The name of this provider
* @property {string} url URL of this provider * @property {string} url URL of this provider
@ -173,6 +186,7 @@ class MessageEmbed {
: null; : null;
/** /**
* Represents the footer field of a MessageEmbed
* @typedef {Object} MessageEmbedFooter * @typedef {Object} MessageEmbedFooter
* @property {string} text The text of this footer * @property {string} text The text of this footer
* @property {string} iconURL URL of the icon for 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>} * @type {?Collection<Snowflake, GuildMember>}
* @private * @private
*/ */
this._members = null; this._members = null;
/** /**
* Cached channels for {@link MessageMention#channels} * Cached channels for {@link MessageMentions#channels}
* @type {?Collection<Snowflake, GuildChannel>} * @type {?Collection<Snowflake, GuildChannel>}
* @private * @private
*/ */
@ -138,7 +138,7 @@ class MessageMentions {
if (!this.guild) return null; if (!this.guild) return null;
this._members = new Collection(); this._members = new Collection();
this.users.forEach(user => { 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); if (member) this._members.set(member.user.id, member);
}); });
return this._members; return this._members;
@ -164,7 +164,7 @@ class MessageMentions {
/** /**
* Checks if a user, guild member, role, or channel is mentioned. * Checks if a user, guild member, role, or channel is mentioned.
* Takes into account user mentions, role mentions, and @everyone/@here mentions. * 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 {Object} [options] Options
* @param {boolean} [options.ignoreDirect=false] - Whether to ignore direct mentions to the item * @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 * @param {boolean} [options.ignoreRoles=false] - Whether to ignore role mentions to a guild member
@ -179,7 +179,11 @@ class MessageMentions {
} }
if (!ignoreDirect) { 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); return this.users.has(id) || this.channels.has(id) || this.roles.has(id);
} }

View file

@ -28,12 +28,6 @@ class MessageReaction {
*/ */
this.message = message; 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 * A manager of the users that have given this reaction
* @type {ReactionUserManager} * @type {ReactionUserManager}
@ -46,13 +40,20 @@ class MessageReaction {
} }
_patch(data) { _patch(data) {
/**
* The number of people that have given the same reaction
* @type {?number}
* @name MessageReaction#count
*/
// eslint-disable-next-line eqeqeq // 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