Compare commits

..

317 Commits
11.3.0 ... v11

Author SHA1 Message Date
SpaceEEC
da39e858a1 chore(Release): version 11.6.4 2020-04-05 19:09:15 +02:00
SpaceEEC
747d76de10 fix(APIRequest): group reaction requests into one route per channel (#4017)
* fix(APIRequest): group all reactions into the same route per channel

* refactor(APIRequest): make route a const
2020-04-05 18:59:16 +02:00
iCrawl
de0cacdf32 chore(release): version 2020-03-20 09:06:58 +01:00
izexi
bb4cb3e7fe fix: messageReactionRemove emission (#3966)
* fix: handler event name for MessageReactionRemoveEmoji

* fix: typo in WSEvents key
2020-03-20 08:53:47 +01:00
SpaceEEC
08865a98cd chore(release): publish 2020-03-08 19:38:05 +01:00
SpaceEEC
20075e306b fix(ReactionCollector): only modify users and total on collect (#3905) 2020-03-08 19:33:18 +01:00
SpaceEEC
d72172744e v11.6.1 2020-02-29 19:13:53 +01:00
Anish Shobith P S
34d352dcbe docs: bump version to 11.6.0 (#3863)
* docs: Bump version to 11.6.0

* fix: typo
2020-02-29 19:11:42 +01:00
SpaceEEC
b3931eaebb v11.6.0 2020-02-29 15:28:37 +01:00
SpaceEEC
08e7328b86 docs(readme): remove mention of uws 2020-02-29 15:26:27 +01:00
SpaceEEC
97457e1de2 feat(RichEmbed): add toJSON returning an api-compatible object
This backports:
PR: https://github.com/discordjs/discord.js/pull/3813
Commit: 4ec01ddef5
2020-02-28 18:29:41 +01:00
Souji
6eaf63fb7c feat(RichEmbed): backport spliceFields and normalizeField (#3762)
* backport: RichEmbed#checkField, Util#resolveString

* backport: RichEmbed#spliceFields

* fix: typo

* chore: use util.resolveString everywhere

* chore: rename EmbedFIeld to EmbedFieldData

* consistency with v12

* chore: rename checkField to normalizeField

* consistency with v12

* fix: EmbedField instead of EmbedFieldData

* fix(typings): EmbedFIeld#inline is guaranteed

* fix(docs): add JSDocs typedef for EmbedFieldData

* fix(typings): EmbedFIeldData#name/#value

* should be StringResolvable

* refactor(RichEmbed): do not duplicate field prop checking

* docs(RichEmbed): document default for inline

* fix(RichEmbed): pass correct parameters to normalizeField

* typings(RichEmbed): add missing spaces

Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-02-28 18:16:19 +01:00
Ryan Munro
cf646b5394 fix(typings): MessageOptions#split (#3834) 2020-02-26 09:46:19 +01:00
Sugden
b0aed050e3 feat(Guild): add rulesChannel and publicUpdatesChannel (#3810)
* add rulesChannel* & publicUpdatesChannel*

* update typings
2020-02-22 13:14:11 +01:00
Sugden
b0d0b81c61 feat: add new MessageTypes (14 and 15) (#3812)
* document types

* typings

* move comment
2020-02-22 12:52:31 +01:00
SpaceEEC
7e9c995566 feat(Message*): add missing fields, add support for flag editing (#3795) 2020-02-22 12:38:43 +01:00
SpaceEEC
330c410796 feat(Guild): add support for system channel flags (#3793) 2020-02-22 12:36:59 +01:00
SpaceEEC
ab866d6b2e feat(GuildChannel): add support for clone options, deprecate old signature (#3792) 2020-02-22 12:35:22 +01:00
SpaceEEC
544b14a5ed docs(PermissionResolvable): move definition outside of class
Otherwise it won't appear in the docs for some reason
2020-02-16 13:05:47 +01:00
SpaceEEC
46e8bc44fc feat(BitField): add BitField base class (#3759)
* feat(BitField): add BitField base class

* fix(Permissions): properly deprecate the getters/setters
2020-02-12 22:23:48 +01:00
BorgerKing
b7ccf9a53e docs: info tag for ActivityType regarding CUSTOM_STATUS (#3758) 2020-02-12 22:23:35 +01:00
Ryan Munro
dbdb49ee1c feat(GuildAuditLogs): handle new event types (#3760)
* Define new AuditLogActions

* Backport constructor rewrite

* Typings

* fix(GuildAuditLogEntry): switch on correct property, coerce to numbers, simplify extra for deleted entities

Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-02-12 18:42:57 +01:00
SpaceEEC
83bc6e0779 fix(Guild): update premiumSinceTimestamp on guild member update 2020-02-07 19:13:02 +01:00
Souji
364914fd35 fix(GuildMember): manageable - let owner override (#3766)
This backports #3765
2020-02-07 18:27:56 +01:00
SpaceEEC
c955fd00c7 feat(Integration): add guild integrations (#3756) 2020-02-02 11:11:31 +01:00
SpaceEEC
a12e1e87ee typings(Constants): add CUSTOM_STATUS to ActivityTypes 2020-02-01 21:14:02 +01:00
SpaceEEC
2589db6633 feat(Constants): add CUSTOM_STATUS to ActivityTypes 2020-02-01 21:12:58 +01:00
SpaceEEC
17b8b23b80 feat(Presence/Game): multiple activities and custom status (#3747)
* feat(Presence): add activities

* feat(Game): add created* and emoji
2020-02-01 18:27:20 +01:00
SpaceEEC
ccd60438df feat(Collector): add idle option (#3746) 2020-02-01 18:23:56 +01:00
SpaceEEC
fbcd363ec9 fix(Voice*): fix speaking event and voice receive (#3749)
* fix(Voice*): synthesize speaking event from UDP packets

* fix(VoiceReceiver): skip over undocumented Discord byte

See #3555

* fix(VoiceConnection): play frame silence before emitting ready

* typings: account for changes in private api
2020-01-31 22:37:11 +01:00
SpaceEEC
6d7e1e4953 fix: remove for..in in favor of Object.keys (#3745) 2020-01-31 11:38:47 +00:00
Ryan Munro
ab7f9e80b4 feat(MessageReaction): backport removeAll and MessageReactionRemoveEmoji event (#3741)
* Add new action and websocket handler

* Add REST method for removing reaction emoji

* Update Message#_removeReaction to handle removing whole emoji

* Add MessageReaction#removeAll and update typings

* Apply uncached user fix
2020-01-25 15:36:35 +01:00
PLASMAchicken
6b297b8776 chore: bump version to 11.6.0-dev (#3731)
* Update package.json

* Change Version String after amishshah's  suggestion

Co-Authored-By: Amish Shah <amishshah.2k@gmail.com>

Co-authored-by: Amish Shah <amishshah.2k@gmail.com>
2020-01-25 15:09:30 +01:00
SpaceEEC
099a1a47e8 fix(*Collector): always run postCheck, remove 'translatation' of message collector options (#3718)
* fix(*Collector): always run postCheck, correctly 'translate' message collector options

* fix(MessageCollector): remove translation, fix postCheck conditions
2020-01-24 16:56:04 +01:00
SpaceEEC
30adb378fc feat(Webhook): backport missing properties (#3710)
* feat(Webhook): add avatarURL getter

This backports: https://github.com/discordjs/discord.js/pull/3625

* feat(Webhook): add type, createAt, and createdTimestamp

This backports: https://github.com/discordjs/discord.js/pull/3585

* feat(Webhook): add url getter

This backports: https://github.com/discordjs/discord.js/pull/3178

* docs(Webhook): add missing type and readonly tags
2020-01-24 16:52:52 +01:00
SpaceEEC
88b675d38a feat(MessageReaction): backport animated, client, created*, and url (#3711) 2020-01-24 16:50:16 +01:00
SpaceEEC
4ca18647ba feat(MessageAttachment): add spoiler getter (#3713) 2020-01-24 16:45:52 +01:00
SpaceEEC
a505a55e03 fix(RichPresenceAssets): add Twitch preview link for largeImageURL (#3715) 2020-01-24 16:43:16 +01:00
SpaceEEC
903f6ca75f fix: only setMaxListeners when max listeners is not 0 (#3716) 2020-01-24 16:41:37 +01:00
Ryan Munro
40afbc1d7e feat(Client): backport INVITE_CREATE and INVITE_DELETE events (#3728)
* Backport INVITE_CREATE and INVITE_DELETE

* Register events to Websocket

* Dont create an Invite if the guild is null

* Null check channel too
2020-01-24 16:34:59 +01:00
Ryan Munro
17237c70c8 typings(TextChannel): topic can be null (#3687)
* Mark topic as nullable for TextChannel

* Backport separate NewsChannel typings

* Ensure NewsChannel#rateLimitPerUser is undefined

* Revert rateLimitPerUser, considered breaking

* Add rateLimitPerUser back to typings

* Linting

* Revert NewsChannel extends TextBasedChannel
2020-01-24 16:33:19 +01:00
SpaceEEC
464ef25898 fix(ClientDataResolver): return a user in resolveUser when passing guild (#3719) 2020-01-20 22:02:28 +01:00
Souji
d8419ac2c7 docs(MessageMentions): backport mention order notice (#3712) 2020-01-19 13:09:33 +01:00
SpaceEEC
c5d2b96524 fix(VoiceConnection): use Client#clearTimeout to clear timeouts (#3709) 2020-01-19 13:08:49 +01:00
SpaceEEC
01826aeefe feat(Guild): add setBanner method and banner to edit (#3708) 2020-01-19 13:07:09 +01:00
Ryan Munro
0f49d67e2e feat(Message/Mentions): implement caching of members (#3684)
* Convert message#member to a getter

*  Try to cache members from data in message payloads

* Cache mentioned members

* Revert Message#member getter - breaking change

* Revise member caching

* Revise member mention caching

* Pass member to _addMember correctly

* Use message.guild instead of this.guild

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

* Merge if's onto one line

* fix(Message): use this.author.id to check cache

Discord does not send an id in the member data here

* chore(Message): reindent equals

Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
2020-01-19 12:27:57 +01:00
Amish Shah
6ab46491c8 Add internal support for @discordjs/opus to v11 (#3700)
* Add internal support for @discordjs/opus

* Remove redundant try/catch

* fix: use setBitrate method in @discordjs/opus

* chore: tidy up opus imports

* fix: correct imports for DiscordJsOpusEngine

* chore: update docs to prefer @discordjs/opus

* chore: bump prism-media to 0.0.4 to allow ffmpeg-static
2020-01-17 20:58:49 +00:00
SpaceEEC
36c0496ea5 fix(Guild): assign GuildMember#selfStream, if present, when adding a member 2020-01-13 21:43:42 +01:00
Ryan Munro
07996d12a2 feat(Constants): backport VerificationLevels and missing APIError codes (#3688)
* Add VerificationLevel constants

* Update APIError constants
2020-01-13 20:47:55 +01:00
Vlad Frangu
684bb1bf36 src: Remove _trace from different places in the WS (#3679)
* src: Remove `ws._trace` from READY

* src: Remove `ws._trace` from RESUME

* lint: Fix lint by removing unused packet (#7)

Co-authored-by: bdistin <bdistin@gmail.com>
2020-01-13 17:54:15 +00:00
Ryan Munro
f6d1db6a24 Backport documentation fixes (#3683)
* Presence does not extend Base, therefore presence.client was undocumented

* Document Client#fetchVoiceRegions returning a promise
2020-01-13 14:00:13 +00:00
Vlad Frangu
5556b05241 src: add deprecation warning related to removel of uws (#3648)
* src: Add deprecation warning related to uws

* lint: Fix lint

* src: Simplify code
2020-01-12 15:16:27 +01:00
SpaceEEC
fbe9bc499b feat(Webhook): add ability to change channel and specify reason to edit (#3587)
* feat(Webhook): add ability to change channel and specify reason to edit

* fix(RESTMethods): update channelID of the webhook too
2020-01-05 18:34:00 +01:00
SpaceEEC
d1d0d75d4a fix(ChannelDelete): mark messages of a deleted channel as deleted (#3572) 2020-01-05 18:29:14 +01:00
SpaceEEC
367c80070f feat(Permissions): add any method (#3571)
* feat(Permissions): add any method

* typings: add Permissions#any

* fix(Permissions): resolve doesn't take a checkAdmin parameter

Co-Authored-By: bdistin <bdistin@gmail.com>

* docs(Permissions): remove trailing space, add returns annotation

Co-authored-by: bdistin <bdistin@gmail.com>
2020-01-05 18:24:08 +01:00
SpaceEEC
cbabc1663c fix(Voice*): internally disconnect and cleanup when forcibly disconnected (#3597) 2020-01-05 18:10:20 +01:00
λtlas
1d6606293a docs(Client): clarify whose ToS are being violated (#3580) 2019-11-19 21:50:45 +01:00
SpaceEEC
96037e107f feat(GuildMember): add selfStream (#3522) 2019-10-27 10:27:43 +01:00
SpaceEEC
f91ad7023b feat(GuildMember): filter out duplicate roles when updating (#3502) 2019-10-27 10:27:01 +01:00
SpaceEEC
18613526bd docs(VoiceStatus): document name -> value, link in VoiceConnection#status (#3500) 2019-10-27 10:26:08 +01:00
SpaceEEC
91600a6946 fix(VoiceReceiver): delete opus encoder from map in stoppedSpeaking (#3499) 2019-10-27 10:24:35 +01:00
Souji
7011c512fb fix: document ChannelData#reason (#3549)
* fix: document ChannelData#reason

* update respective typings
* closes #3548

* update: add note creation only

Co-Authored-By: SpaceEEC <spaceeec@yahoo.com>
2019-10-22 21:17:56 +02:00
Souji
2610bf57ae feat(GuildChannel): backport permissionsLocked getter (#3507)
* backport(GuildChannel): GuildChannel#permissionsLocked

* typings: GuildChannel#permissionsLocked

* fix(typings): mark permissionsLocked getter as readonly
2019-10-04 16:43:12 +02:00
rei2hu
8ddd0616a9 fix(Util): make arraysEqual avoid mutating the input arrays (#3506) 2019-10-04 16:39:56 +02:00
SpaceEEC
a8e365743c typings: optional reason for setNSFW and add deleted properties (#3505)
* typings: optional reason for setNSFW and add deleted properties

* typings(Guild): setDefaultMessageNotifications' reason is also optional
2019-10-04 11:20:56 +02:00
izexi
94ce19dd1a typings: mark GuildMember#nickname as nullable (#3517) 2019-10-04 11:19:55 +02:00
Souji
2a78b00454 fix(typings): GuildChannel#parent and #parentID are nullable (#3509) 2019-10-02 14:26:53 +02:00
Ryan Munro
505df2ebb3 backport(Guild): createChannel's default type incorrectly set (#3497)
Backports #3496 (a03e439d6b) to the 11.5-dev branch
2019-10-01 10:59:02 +02:00
Vlad Frangu
748555036d typings: Add missing rateLimitPerUser property (#3480) 2019-09-22 17:07:44 +02:00
Vlad Frangu
43c0a794e1 fix(GuildAuditLogsEntry): default to object with id for deleted targets (#3373) 2019-08-28 11:22:25 +02:00
Ryan Munro
dcee09c308 backport(Permissions): backport STREAM permission from #3309 (#3447)
* Backport the STREAM permission

* Update typings and default
2019-08-28 11:09:45 +02:00
SpaceEEC
1121b2f7bf fix(GuildChannel): return GuildChannel in setPosition instead of Guild
fixes #3413
2019-07-30 17:46:22 +02:00
SpaceEEC
0cd7556934 feat(Teams): backport support for teams (#3357)
* feat(Teams): backport support for teams

PR #3350
Commit: a22aabf6a8

* fix(TeamMember): fix name of client property

* refactor(OAuth2Application): make team nullable instead of optional

* typings(OAuth2Application): make team nullable instable of optional

* docs(OAuth2Application): deprecate and add an info to team property
2019-07-11 13:10:54 +02:00
SpaceEEC
c355236f7f feat(Emoji): backport delete method (#3343)
This backports #1877 (c93c4ad21f) in a semver-minor manner.
2019-06-16 10:07:32 +02:00
SpaceEEC
b8924369ea feat(Guild): add support for premium/boosting (#3332)
Backports:
PR: #3316
Commit: c87758086b
2019-06-13 19:03:36 +02:00
SpaceEEC
e6a378b361 feat(Guild): backport misc properties and setRolePositions (#3337)
* feat(Guild): backport misc properties and setRolePositions

PRs:
* #3168
* #3317

* typings
2019-06-13 18:33:07 +02:00
SpaceEEC
6f49aadf4f fix(Guild): allow fetchMember to be used with an uncached user (#3333) 2019-06-08 10:39:03 +02:00
Gryffon Bellish
0c6101901d fix(docs): backport documentation for Presence#clientStatus (#3315)
* backport documentation for Presence

* capitalize o

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

* remove extra space

* backport ClientPresenceStatus

* Change to Client Presence Status
2019-06-01 20:24:22 +02:00
Amish Shah
5dd9181497 v11.5.1 2019-05-29 21:23:16 +01:00
SpaceEEC
db492e66e2 chore: explicitly mark everything deprecated as @ deprecated (#3307) 2019-05-29 22:18:14 +02:00
bdistin
8c213e9088 fix(Message#pinnable): you can't pin system messages (#3279) 2019-05-18 19:06:15 +02:00
SpaceEEC
06b72ee19f fix(GuildMember): do not create a channel key when editing
This is to not break GuildMember#setNickname for the current user
2019-05-18 14:08:50 +02:00
Fong Jian Ping
e3d03adcf8 docs(ChannelCreationOverwrites): revert incorrect rename of "id" property (#3273)
Reverts one incorrect change made in #2734 (3021e5ce7f (diff-e5c54069adfa0d32480eb3cc9faeee20L979))

* Fix incorrect docs in Guild.js

* Update src/structures/Guild.js

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

* Update Guild.js
2019-05-16 13:06:07 +02:00
anandre
363cec31bc docs(TextChanne): specify unit of rateLimitPerUser (#3272)
* Update TextChannel.js

Update `setRateLimitPerUser` description to specify the `number` is in seconds, per the Discord docs

* Update TextChannel.js

Add unit to the rateLimitPerUser property

* Update GuildChannel.js
2019-05-15 22:42:39 +02:00
SpaceEEC
c844a7b4e2 docs(Guild): fix typo in example of createChannel 2019-05-14 20:12:57 +02:00
Schuyler Cebulskie
e82633fb00 Bump version to 11.5.0 2019-05-11 19:05:10 -04:00
Schuyler Cebulskie
b2f89e4594 Add testing for Node.js 8-12 2019-05-11 19:00:03 -04:00
Schuyler Cebulskie
8fba786765 Update typings for news/store channels 2019-05-11 18:56:14 -04:00
Schuyler Cebulskie
43f2485c9c Document news/store channel types on Guild#createChannel 2019-05-11 18:11:34 -04:00
Schuyler Cebulskie
8a086e04ab Add news/store channel support to CHANNEL_CREATE 2019-05-11 18:11:19 -04:00
Schuyler Cebulskie
f30019dd4f Remove unnecessary import 2019-05-11 17:24:25 -04:00
Schuyler Cebulskie
5e4654ee07 Backport news/store channels 2019-05-11 14:58:46 -04:00
SpaceEEC
ee42bdfd76 feat(GuildMember): add support for voice kicking (#3246)
This backports e64773e21b (#3242)
2019-05-06 19:18:29 +02:00
SpaceEEC
67da457c0a fix/docs(Client): type status and do not throw an error if accessed before login 2019-05-05 13:44:34 +02:00
SpaceEEC
60013b7a10 fix(WebSocketConnection): stringify data when no websocket is available
[object Object] is not really descriptive
2019-05-04 21:00:49 +02:00
SpaceEEC
12e041bc2b typings(GuildChannel): add manageable getter 2019-04-17 21:39:46 +02:00
izexi
3cd224dc76 docs(Collection): fix findKey jsdoc (#3204) 2019-04-14 15:49:32 +02:00
SpaceEEC
923c945b4b fix(Guild): sort roles with the same position in the correct order (#3184) 2019-04-08 14:06:50 +02:00
Ryan Munro
831f988fe2 typings(Collection): add typings for partition (#3166) 2019-04-01 09:35:17 +02:00
izexi
5cd6d8d380 feat(Guild): add fetchBan and withReasons to fetchBans (#3170)
* add Guild#fetchBan() & backport BanInfo

* and the typings

* requested changes

* typings overloads

Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com>

* nullable reason typings

Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com>
2019-04-01 09:07:52 +02:00
Amish Shah
745e18b823 fix #3150: can now create dispatcher with 0 volume 2019-03-22 12:43:51 +00:00
Souji
be2f78851f docs/typings: RateLimitInfo#limit instead of requestLimit (#3132)
* fix doc for ratelimit data, fixes #3131

* adapt typings

* typings: unindent #region comments
2019-03-07 16:34:23 +01:00
Amish Shah
6e761eb030 fix typo 2019-02-26 13:10:39 +00:00
SpaceEEC
32ad56a562 docs(Guild): update createChannel examples to not use deprecated overload 2019-02-25 11:09:46 +01:00
Cam Cope
f61c044b26 Mark peer dependencies as optional (#3066) 2019-02-24 08:43:42 +00:00
SpaceEEC
45a17e7ebd fix(Emoji): reject explicit error when MANAGE_EMOJI permissions are missing (#3063) 2019-02-12 10:16:23 +01:00
SpaceEEC
49e8bd9edd feat(RichEmbed): add timestamp support for setTimestamp (#3061) 2019-02-12 10:15:37 +01:00
SpaceEEC
1618829cc6 fix(Util): splitMessage throws an error if a chunk is too large (#3060) 2019-02-12 10:14:49 +01:00
SpaceEEC
a0ff72b556 fix(GuildMember): add explicit channel resolve error to member edit (#3059) 2019-02-12 10:13:37 +01:00
SpaceEEC
7bc2e231cf feat(Guild): add position to createChannel's implementation (#3058) 2019-02-12 10:12:25 +01:00
SpaceEEC
890b1be714 feat(RichEmbed): add length getter (#3057) 2019-02-12 10:11:44 +01:00
SpaceEEC
a2a0c05102 feat(Presence): add clientStatus (#3056) 2019-02-12 10:10:33 +01:00
SpaceEEC
5272cec6c8 feat(Util): add WHITE as color resolvable (#3062) 2019-02-09 23:52:38 +00:00
Kyra
359ddaf1df feat(Constants): add error code 50020 (#2953)
* feat(Constants): Add error code 50020

Which is throw when using the vanity-url endpoint: https://github.com/discordapp/discord-api-docs/pull/748/

* docs: Document the new code
2019-02-06 22:09:00 +01:00
Lucas Kellar
8b602ebed4 typings(SnowflakeUtil): add optional "timestamp" parameter to generate (#2998) 2019-02-06 19:14:52 +01:00
SpaceEEC
73aaab5106 fix(Guild): ignore voice states referencing an invalid channel
This was causing an uncaught exception on startup (or whenever receiving such a payload) which is crashing the process.
2019-01-17 11:34:10 +01:00
izexi
3b7b282b69 docs(Client): add missing example tag and closing parenthesis (#3024) 2019-01-17 09:58:04 +01:00
Darqam
5ed2a95856 docs(Client): add missing parenthesis in fetchInvite example (#3023)
This is already fixed in master, resolves #3022
2019-01-16 19:17:19 +01:00
SpaceEEC
46fd7b093c docs(Guild): use AuditLogAction for fetchAuditLogs' type option 2019-01-10 13:05:33 +01:00
SpaceEEC
17ca83663f typings(TextBasedChannel): fix create(Message)Collector's options type 2019-01-06 17:51:27 +01:00
SpaceEEC
9d83516918 typings(Guild): fix typos in method names
Fixes #3009
2018-12-31 18:21:22 +01:00
SpaceEEC
89a9b93cdc docs(Webhook): add mising '@name' to Webhook#token's docstring 2018-12-30 12:56:33 +01:00
Drahcirius
7186c91063 fix(TextBasedChannel): added missing lastMessage functionality in textchannels (#2999) 2018-12-23 22:16:50 -06:00
izexi
2aa8e1d9c1 docs:(TextChannel): add documentation for messages and lastMessage (#2986)
* [docs] add missing docs for <TextChannel>.messages

* add missing doc for <TextChannel>.lastMessage
2018-12-22 08:25:24 +01:00
SpaceEEC
351f0a32bf typings(RichEmbed): add MessageEmbed as valid data in constructor
See #2970
2018-11-26 18:08:31 +01:00
SpaceEEC
691aaef07e backport(Guild): support for rateLimitPerUser when creating a channel
PR: #2878
Commit: 8ec3b5134d
2018-11-17 17:24:48 +01:00
SpaceEEC
6aa7792097 docs(GuildChannel): add rateLimitPerUser to ChannelData typdef 2018-11-17 17:19:04 +01:00
Souji
980d71f307 fix:(GuilChannel): clone method not taking overwrites into account (#2932) 2018-11-06 20:01:48 +01:00
Amish Shah
b3f459091f Fix #2928 (member not being removed from voice channel after leaving guild) 2018-11-04 12:26:06 +00:00
Isabella
950abd4ac3 fix: revert #2768 (#2848)
* fix: revert #2768

* fix merge
2018-10-14 11:44:02 -05:00
SpaceEEC
7ea88adeca backport(Guild): support for createChannel with options object (#2888) 2018-10-10 10:05:32 +02:00
SpaceEEC
ea3e575546 backport(TextBasedChannel): add lastPinTimestamp and lastPinAt (#2870)
And clarify Client#channelPinsUpdate's 'time' parameter.
2018-10-10 10:01:04 +02:00
SpaceEEC
fcf4745a43 typings: fix lint script and linter errors 2018-10-04 19:29:12 +02:00
SpaceEEC
b92f8d9c06 docs(Game): document possible values for type property
See #2865
2018-10-04 13:31:26 +02:00
SpaceEEC
c6201ee41b backport(Guild): add fetchVanityCode (#2871) 2018-10-03 17:31:20 -05:00
Isabella
1e85887229 backport: rateLimitPerUser (#2874) 2018-10-03 17:21:26 -05:00
SpaceEEC
e0f522a745 backport(ClientOptions): add retryLimit (#2869) 2018-10-03 17:20:53 -05:00
SpaceEEC
9de3e098da docs(User): clarify what User#tag represents
Closes #2828
2018-09-10 18:11:09 +02:00
Crawl
641ee86105 build(peer-deps): use uws fork backport (#2782)
* build(peer-deps): uws backport

* chore: update to 149 for uws
2018-08-30 17:55:31 -05:00
Souji
da2d4d7230 docs: correct Guild#memberCount (#2812) 2018-08-30 00:15:05 -05:00
Kyra
cd58599caf fix(Guild#deleteEmoji): reject non emojis / emoji IDs (#2793)
* fix(Guild#deleteEmoji): Performing wrong checks

* fix: requested changes

`a string` -> `an id`

* fix: requested changes

`id` -> `ID`
2018-08-29 08:45:27 +02:00
Kyra
b83fdbfefe docs: Added url to Invite's warning comment (#2804)
And added the [serial comma](https://en.wikipedia.org/wiki/Serial_comma)
2018-08-28 19:12:12 -05:00
Kyra
89986ae293 backport: UNKNOWN_WEBHOOK (#2777) 2018-08-28 10:35:34 -05:00
Kyra
091b4fc214 backport: Guild#{fetchEmbed,setEmbed} (#2778)
*  backport: Guild Embeds

* fix: Added missing return

* docs: Updated typings
2018-08-28 10:33:51 -05:00
Kyra
3345c77ce2 backport: GUILD_INTEGRATIONS_UPDATE event (#2794)
* backport: Client#on{guildIntegrationsUpdate,webhookUpdate}

misc: Update Constants.WSEvents and WSEventType

backport: Add guildIntegrationsUpdate event handler

* docs: Updated typings
2018-08-28 10:25:44 -05:00
Ash
1fc84a95d0 fix(GuildChannel#lockPermissions): Properties allow and deny always returning undefined (#2800)
* fix undefined properties

* requested changes
2018-08-26 13:11:00 -05:00
Kyra
58ba2c7b14 backport: Deprecate allowed/denied as #2765 (#2792) 2018-08-26 13:03:02 -05:00
Lewdcario
2e2c9c4b9a typings: clean up permissionResolvable 2018-08-26 11:55:10 -06:00
Lewdcario
bd14d5d2fa typings: add WEBHOOKS_UPDATE 2018-08-26 11:07:51 -06:00
Kyra
453098117f backport: WEBHOOKS_UPDATE event (#2779) 2018-08-26 11:59:15 -05:00
Lewdcario
93bf430fc7 fix: Guild#addMember incorrectly resolving userID 2018-08-26 09:46:55 -06:00
Kyra
469fbe2889 docs(Emoji): fix typo of the word "emoji" (#2791) 2018-08-24 18:31:43 +02:00
Kyra
911e289b55 backport: User#dmChannel perf enhancement (#2780) 2018-08-22 11:52:21 +02:00
Crawl
7684ad3ca6 fix(webpack): properly emitting process deprecation warnings 2018-08-22 04:54:09 +02:00
Isabella
552323363b fix: disable getter-return 2018-08-21 12:40:49 -05:00
Isabella
d3e7dbc738 fix: pin & update deps 2018-08-21 11:37:08 -05:00
Isabella
4ee3cf0b55 fix: WebhookClient should handle ratelimits properly 2018-08-21 11:12:02 -05:00
Kyra
dc8cf08de9 backport: handle async stacktraces correctly (#2768) 2018-08-21 10:31:32 +02:00
SpaceEEC
f5b3dff7f5 typings: make PermissionResolvable recursive 2018-08-15 15:13:08 +02:00
SpaceEEC
5d889be6db fix(Permissions): Permissions itself is a valid PermissionResolvable
fixes #2753
2018-08-15 13:55:04 +02:00
SpaceEEC
616e0dd398 fix(Message): properly check for an edited_timestamp in patch
Fixes #2750
2018-08-15 09:18:17 +02:00
zajrik
efc8445260 refactor: Merge typings into 11.4-dev branch 2018-08-13 22:20:33 -05:00
Amish Shah
ebfbf20f07 Bump to 11.4.2 2018-08-13 15:19:21 +01:00
Souji
3021e5ce7f Docs: document ChannelData.parent and .permissionOverwrites, fix typedefs to not include Collection of ChannelCreationOverwrite (#2734)
* document ChannelData.parent

* document ChannelData.permissionOverwrites

* Overwrites also take other things, account for it

* note to self: stop copy-pasting

* remove eslint madness, fix param defs

* fix property slip
2018-08-13 15:17:09 +01:00
Schuyler Cebulskie
b5df8603e8 Update version in web builds example 2018-08-12 17:01:32 -04:00
Kolkies
8b1bb95b1a docs: update for 11.4.0 (#2727)
* Update for 11.4.0 docs
2018-08-12 13:20:08 -05:00
Lewdcario
6775684ff0 chore: update typings & bump version 2018-08-12 13:14:52 -05:00
Lewdcario
d772bff632 fix(ClientDataManager): replacing channels unecessarily 2018-08-12 11:50:01 -05:00
SpaceEEC
8adfc97409 docs(Client): actually fix rateLimit's event params 2018-08-11 09:38:58 +02:00
Amish Shah
72bb9cb532 voice deps: fix prism-media requirement 2018-08-09 22:40:31 +01:00
Lewdcario
0e370d7a4c docs: fix client#rateLimit parameters 2018-08-09 14:58:17 -05:00
Lewdcario
7126cade45 fix: richEmbed#_apiTransform fields 2018-08-09 13:52:50 -05:00
Lewdcario
bef07152c8 chore: update typings & bump version 2018-08-09 11:47:52 -05:00
Amish Shah
16331d9e85 chore: update typings 2018-08-09 17:34:59 +01:00
Souji
bafbee9677 docs: small changes regarding permissions/overwrites (#2718)
* Add Collection as param to GuildChannel#replacePermissionOverwrites

* Add example for null to PermissionOverwriteOptions

* eslint-disable max-len

* PermissionOverwriteOptions desc. change default to unset

* deprecated allow/deny in favor of allowed/denied
2018-08-09 10:52:59 -05:00
Lewdcario
6da423fc07 backport: Permissions#toArray 2018-08-08 22:17:08 -05:00
Lewdcario
1e5b5b83c8 backport: GuildChannel#permissionsFor(role) 2018-08-08 22:16:46 -05:00
Lewdcario
c76f3048af backport: TextChannel#bulkDelete accepts Snowflake[] 2018-08-08 17:14:52 -05:00
Souji
af6d649510 docs: change for GuildChannel#replacePermissionOverwrites (#2714) 2018-08-08 16:09:39 -05:00
SpaceEEC
c7f379f51d docs(RichEmbed): timestamp is actually a Date 2018-08-07 21:46:54 +02:00
SpaceEEC
4a48a7d621 docs(ShardingManager): respawnAll actually resolves with a collection of shards keyed by numbers 2018-08-07 21:36:03 +02:00
Lewdcario
c33ab1ea61 backport: add PRIORITY_SPEAKER permission 2018-08-03 19:09:09 -06:00
Lewdcario
87b4b23596 backport: fix circular conversion when editing RichEmbed 2018-08-03 15:33:51 -06:00
Lewdcario
b63948e14e backport: RichEmbed#attachFiles 2018-08-02 15:06:27 -06:00
Lewdcario
4a9c2f8884 fix(Emoji#fetchAuthor): reject with Error rather than TypeError 2018-07-26 14:35:26 -06:00
Lewdcario
41f6eaa635 backport: Message#url getter 2018-07-26 11:22:29 -06:00
Lewdcario
95a2d25b7d chore: deprecate userbot methods 2018-07-26 11:18:41 -06:00
Lewdcario
d685e39af4 backport: add rejection for Emoji#fetchAuthor if managed
Signed-off-by: Lewdcario <isabellafj97@gmail.com>
2018-07-26 10:24:26 -06:00
Lewdcario
96011cf68f backport: make Webhook token not enumerable 2018-07-26 09:51:47 -06:00
Isabella
488b1eb4ee backport: Sharding utility methods (#2672) 2018-07-25 12:08:08 -04:00
Lewdcario
6d70da5b1e backport: fix circular conversion in RichEmbed 2018-07-23 22:41:07 -06:00
Lewdcario
e4da97e058 eslint: re-enable eslint after disabling max-len 2018-07-23 14:34:38 -06:00
SpaceEEC
fbbd8f43b3 chore: update typing submodule 2018-07-22 20:37:38 +02:00
SpaceEEC
886c21223e fix(GuildAuditLogs): default to PartialGuildChannel if channel deleted (#2605) 2018-07-18 11:38:59 +02:00
Gymnophoria
a97b5040e6 docs(Client): clarify messageReactionRemove event's user description (#2657)
* Clarify messageReactionRemove user description

* Update MessageReactionRemove.js

* wait one more word difference lol
2018-07-18 11:37:36 +02:00
Lewdcario
524a15df0b backport: Permissions improvements 2018-07-17 21:49:21 -05:00
Lewdcario
0702a0fcda backport: DefaultMessageNotifications 2018-07-17 21:32:24 -05:00
SpaceEEC
1d9edec567 fix(Message): keep reply option when replying with an embed or attachment
Fixes #2651
2018-07-08 21:40:12 +02:00
Amish Shah
d81441f627 voice: null-check UDP socket 2018-07-03 14:15:27 +01:00
Amish Shah
695ff1e70f voice: fix #2635 (channel bitrate not being set) 2018-07-02 19:45:35 +01:00
Lewdcario
a667e44c4d fix(Client): login catch 2018-07-01 10:05:56 -05:00
Lewdcario
3d82ca901b docs: improvements in various places
* Client#login example consistency

* add warning to Client#fetchApplication

* incorrect WEBHOOK_DELETE value
2018-06-29 19:38:20 -05:00
Lewdcario
448f38429d fix(Client): login not properly rejecting on invalid token 2018-06-29 19:38:06 -05:00
Lewdcario
3fa9ed1f42 backport: deleted property 2018-06-29 19:11:50 -05:00
Lewdcario
72346fb47e fix(TextBasedChannel): bulkDelete should not return non-Promise 2018-06-29 17:32:56 -05:00
Camzure
7ce1d1642c docs: only cached messages emit reaction events (#2607)
* wording improvement

* wording improvement for docs

* docs: wording

* wording

* user account only: docs

* Edited

* Edited

Signed-off-by: SpaceEEC <spaceeec@yahoo.com>
2018-06-21 21:40:52 +02:00
SpaceEEC
8a3ae875bb fix: do not cache webhook users (#2611)
Goal in mind is to not save the name and avatar used by webhooks because
those can change between messages without any other update.
2018-06-21 21:34:30 +02:00
SpaceEEC
bd3d8d48c0 docs(GuildMember): joinedAt is nullable
See: ecf6e2b62d
2018-06-21 21:24:53 +02:00
RumbleFrog
493ba73037 fix(ShardClientUtil): send method's promise erroneously rejecting (#2604)
Patches the remaining ones that were missed in eef4a4ad17
2018-06-17 08:21:51 +02:00
reeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
507cce3ff4 docs: document lastMessageID for TextChannel and (Group)DMChannel (#2602)
* Adds JSDocs for TextChannel.lastMessageID

* oops

* docs: document lastMessageID in (Group)DMChannel as well
2018-06-16 22:10:24 +02:00
Lewdcario
6de5acbac3 backport: RichPresence 2018-06-09 15:37:01 -05:00
Lewdcario
ecf6e2b62d fix(GuildMember): mark joined as nullable 2018-06-09 15:15:08 -05:00
Lewdcario
caa4104b95 docs: GuildMember#speaking
closes #2540
also improves consistency
2018-06-05 00:31:06 -05:00
Lewdcario
f238883046 fix(MessageReaction): fetchUsers inconsistency
Unlike TextChannel#fetchMessages, this method returned the cache rather than the fetched items, so in interests of consistency, this does as well now
closes #2574
2018-06-05 00:31:03 -05:00
Lewdcario
eef4a4ad17 fix(Shard): erroneously returning false
The node documentation fails to correctly document when the backlog of unsent messages exceeds a certain threshhold the function will return false. This does not mean it will refuse to send- simply that it will take time. Issue in point: https://github.com/nodejs/node/issues/7657#issuecomment-240581726
2018-06-05 00:31:00 -05:00
Lewdcario
c699888780 docs: add examples & improve notices 2018-06-05 00:30:50 -05:00
Lewdcario
0387d34ab4 fix(VoiceChannel): deletable erroneously returning true 2018-06-04 17:18:27 -05:00
Adrien Brignon
13880fe7de docs(Guild): handle error in example of fetchInvites correctly (#2568) 2018-05-27 20:59:12 +02:00
SpaceEEC
f23b61794c fix(ClientUser): correctly resolve nicks to an object in createGroupDM 2018-05-25 17:23:09 +02:00
SpaceEEC
f456f4c3c0 feat(Collection): backport partition method
Commit: a732402c95
PR: #2511
2018-05-25 16:49:10 +02:00
SpaceEEC
f921261f3f fix(docs): remove duplicated Collection#findAll docstring
Also added intended @ deprecated decorators
2018-05-25 16:44:49 +02:00
Lewdcario
6070b22382 docs: using deprecated version of find 2018-05-13 04:41:58 -05:00
Lewdcario
b2d1cb6a3d docs: permissions 2018-05-12 00:51:08 -05:00
Lewdcario
09ddbcb88a chore: deprecations 2018-05-12 00:40:12 -05:00
Lewdcario
6f02be2b2e docs: rateLimit event 2018-05-11 23:21:39 -05:00
Lewdcario
0d90798c6c backport: rateLimit event 2018-05-11 20:55:31 -05:00
Lewdcario
379061987c fix: burst request mode hanging permanently 2018-05-11 19:27:34 -05:00
1Computer1
de7d90ada3 feat(Collection): add tap method (#2507)
* Add Collection#inspect

* No u

* Rename to tap

* Rename variable

* Do it here too
2018-05-09 16:46:54 +02:00
bdistin
2b6592ed54 feat(Collection): add sweep method (#2528) 2018-05-09 16:42:28 +02:00
SpaceEEC
9bb8831619 feat(GuildMember): add manageable getter 2018-05-09 16:38:28 +02:00
SpaceEEC
99041671f0 feat(GuildChannel): add fetchInvites method
Backported from commit: 47bc0fc51e
PR: #2339
2018-05-09 16:28:40 +02:00
SpaceEEC
dd7eedbd48 feat(Emoji): add fetchAuthor method
Backported from commit: e0cbf0bb60
PR: #2315
2018-05-09 16:20:41 +02:00
SpaceEEC
c0ca73a40c feat(Game): add toString method
Backported from commit: 016526486c
PR: #2313
2018-05-09 16:08:59 +02:00
SpaceEEC
15a8e17710 fix(Guild): memberCount not decrementing when an uncached member leaves
Backported from commit: 93e083da4f
2018-05-09 16:07:20 +02:00
SpaceEEC
54913d9edb feat(GuildChannel): add setNSFW method
Backported from commit: 0fc9459450
PR: #2050
2018-05-09 16:04:49 +02:00
Gus Caplan
9169958264 feat(Guild): add verified getter (#2027) 2018-05-09 15:52:00 +02:00
SpaceEEC
96b115ef7b feat(ClientDataResolver): add 2 basic role colors
Commit: 3a3ca96b0d
PR: #2303
2018-05-08 22:35:35 +02:00
Will Nelson
2d831269ab feat(Permissions): add valueOf method (#2363) 2018-05-08 22:30:27 +02:00
Jonah Snider
e5e59cce32 docs(Role): Change 'the this' to this (typo) (#2377)
Commit: 8b679913a4
2018-05-08 22:27:58 +02:00
Pascal
ae28f52e0d fix(ClientDataResolver): always resolve with a buffer when fetching a file
Commit: 85413481ed
2018-05-08 22:25:49 +02:00
SpaceEEC
9264511092 docs(ClientUser): correct warning for createGuild method
Backported from: 2bf68dcdfb
PR: #2453
2018-05-08 22:06:07 +02:00
SpaceEEC
2f2e28183b fix(Role): allow role color to be removed
Backported from: f985b6bef3
PR: #2447
2018-05-08 22:05:56 +02:00
SpaceEEC
54fa5f644f docs(GroupDMChannel): fix top 'an user' -> 'a user' 2018-05-08 20:14:23 +02:00
bdistin
b757f9ef2d docs(Collection): fix spelling of 'behavior' (#2529) 2018-05-08 19:07:49 +02:00
Kyra
bd9c9ce4e0 refactor(VolumeInterface): remove usage of deprecated new Buffer (#2531) 2018-05-08 18:55:52 +02:00
SpaceEEC
60288d0704 fix(Collector): increase and decrease max listeners dynamically 2018-05-08 11:57:12 +02:00
SpaceEEC
ed8ab91782 feat(Emoji): add deletable getter
Backport for commit: fca6d3be56
From PR: #2535
2018-05-08 11:35:14 +02:00
SpaceEEC
21326f67a0 feat(ClientDataResolver): account for discord.gg/invite/<code> invites 2018-05-04 18:49:53 +02:00
Kyra
8700e965c5 feat(DiscordAPIError): backport method property (#2536) 2018-05-03 22:47:50 +02:00
Kyra
a89de09855 fix(RESTMethods): remove listener from correct event (#2534) 2018-05-03 17:56:10 +02:00
SpaceEEC
a85d801a12 fix(ClientUser): setActivity should resolve with a Presence 2018-05-01 20:27:05 +02:00
Amish Shah
5f50d9e627 voice: backport null key fix 2018-04-29 18:53:50 +01:00
SpaceEEC
33a4232652 fix(OpusEngineLinst): throw a descriptive error when not funding an opus engine 2018-04-28 14:30:14 +02:00
SpaceEEC
d9a091f674 feat(SnowflakeUtil): allow snowflakes to be generated dynamically 2018-04-27 20:34:48 +02:00
SpaceEEC
44fefdfa49 fix(Util): reject with a meaningful error instead of throwing one 2018-04-27 20:16:46 +02:00
Amish Shah
b05622766b voice: start using provided IP rather than manually resolving 2018-04-27 15:25:05 +01:00
Isabella
49ad8cc2cc feat(GuildChannel): add manageable getter (#2439)
* Adds GuildChannel.manageable

* Resolve requested changes

* fix eslint max-len error

* Fix for nullable permissionsFor()

* Indent fixes
2018-04-26 01:31:52 -05:00
bdistin
7b9e84dff5 feat(Guild): add mfaLevel property (#2451) 2018-04-26 01:28:59 -05:00
Lewdcario
384e96d51e backport: docs improvements 2018-04-26 01:25:44 -05:00
Isabella
d7e7803178 docs: (backport) Bring the main doc pages up to date, and add more examples (#2094) 2018-04-26 01:13:44 -05:00
bdistin
feb0991c46 fix: use Object.keys instead of Object.values for node 6 (#2487) 2018-04-20 21:11:27 +02:00
SpaceEEC
ff671b2f3c fix(RestMethods): typo timeout -> timed 2018-04-19 13:53:44 +02:00
SpaceEEC
b60ee25038 fix(MessageEmbed): avoid throwing error when accessing colorless hexColor 2018-04-19 13:14:20 +02:00
SpaceEEC
3ba26ad972 fix(Message): do not update editedTimestamp when there is none in the payload
Fixes #2307
2018-04-19 13:13:50 +02:00
SpaceEEC
de78a8d0b4 fix(RESTMethods): verify correct member in add and remove role listeners
Fixes #2480
2018-04-19 13:13:28 +02:00
SpaceEEC
7c37a0d386 fix(MessageDeleteBulkAction): remove bulkDeleted messages from cache
Fixes #2382
2018-04-19 11:50:30 +02:00
Lewdcario
c387e96078 fix: Client#generateInvite resolving permissions incorrectly 2018-04-18 20:30:24 -05:00
Lewdcario
7c0b6173dd fix: Role#setPermissions resolving & docs 2018-04-18 19:57:16 -05:00
Lewdcario
92b421607e fix: GuildChannel#setTopic not nullable 2018-04-18 19:35:28 -05:00
iDroid
f3ae7fd638 Fixed JSDoc owner thing (#2473)
Added a `?` before `GuildMember`, the `owner` property may be undefined in some cases.
2018-04-18 19:25:28 -05:00
sekwah41
6e5b674338 Fixed updateChannel being too protective (#2460)
If I am not mistaken the only way atm to remove a channels parent atm is to do this to get the null value through the code.

channel.client.rest.methods.updateChannel({id:channel.id, name:channel.name,
            topic:channel.topic,position:channel.position,
            bitrate:channel.bitrate,userLimit:channel.userLimit,parent:{id:null}}, {});

This fixes the method allowing channel.setParent(null); to work
2018-04-13 14:03:18 +02:00
Amish Shah
e5bd6ec150 11.3.2 2018-03-04 18:08:54 +00:00
Gus Caplan
c8f78b2bf0 fix(ws): set correct ratelimit remaining after reset or destroy (#1806) 2018-03-04 12:44:31 +01:00
Schuyler Cebulskie
419a2eea3f Bump version to 11.3.1 2018-03-03 15:38:24 -05:00
Schuyler Cebulskie
a3cec3bc1f We must not be like Todd Howard 2018-03-03 15:36:59 -05:00
iCrawl
ad93b90cd9 update typings 2018-03-03 02:53:24 +01:00
Lewdcario
8f9e911b5f fix: RichEmbed timestamp cloning 2018-03-02 18:52:43 -06:00
Lewdcario
363ead922a fix: bulkDelete discarding non-cached messages 2018-03-02 12:17:10 -06:00
Lewdcario
96e88f3cef docs: incorrect returns 2018-03-01 22:38:55 -06:00
Lewdcario
fcdffcf623 docs: improvements & examples 2018-03-01 20:47:18 -06:00
Lewdcario
acdf43a872 fix: GuildAuditLogs using Guild#fetchInvites 2018-03-01 20:37:19 -06:00
Lewdcario
38f5288be8 docs: Message#type 2018-03-01 20:12:14 -06:00
Lewdcario
f64e924f0d fix: export CategoryChannel 2018-03-01 20:09:05 -06:00
Pascal
f2c5714751 fix(StreamDispatcher): remove gratuitous parentheses 2018-03-01 19:12:15 +01:00
FireController1847
ced93fe826 Specify that Client#uptime is "in milliseconds" (#2288) 2018-03-01 18:25:17 +01:00
Gus Caplan
7f5c1038db fix websocket ratelimits (#2014) 2018-03-01 18:25:00 +01:00
Gus Caplan
af75e43900 proper fix for #1685 (#1805)
* Update WebSocketConnection.js

* Update WebSocketConnection.js

* Update WebSocketConnection.js

* Update RESTManager.js
2018-03-01 17:50:07 +01:00
Lewdcario
b79722a77b docs: remove trailing commas 2018-02-27 17:43:51 -06:00
Lewdcario
2b24b10246 docs: trailing commas 2018-02-27 11:13:56 -06:00
Isabella
af3517594f docs: improvements 2018-02-27 10:43:31 -06:00
Pascal
c6f92c1bc5 fix: if present, wait for libsodium-wrappers' ready to support v0.7.3 2018-02-26 11:35:51 +01:00
Kyra
b7851bad37 perf(Collection): Performance improvements (#2342)
* Update Collection.js

* ESLint
2018-02-21 22:03:40 +01:00
Sanctuary
0ec53c9d6f docs: Add links for the guide (#2346) 2018-02-21 21:42:35 +01:00
Pascal
dc92582eb4 fix(ReadyHandler): don't create new guild instances for already existing guilds
See: #2319, #2332
2018-02-21 21:41:02 +01:00
Pascal
be40858e32 chore: update typings submodule 2018-02-19 16:26:30 +01:00
iCrawl
5454a074ee revert: changes to node 8 2018-02-17 18:19:56 +01:00
iCrawl
2254a3ee11 fix: build with node 8 2018-02-17 18:13:01 +01:00
iCrawl
da14e33ff7 fix: add node 8 to build 2018-02-17 18:11:28 +01:00
iCrawl
a7b46be923 fix: webpack minified and bump deps 2018-02-17 18:01:20 +01:00
Lewdcario
40a2f093aa fix(VoiceChannel): setUserLimit defaulting to wrong value 2018-02-13 10:43:38 -06:00
David Siaw
8ee2788baf fix(StreamDispatcher): properly check that timestamp fits in 2^32-1 (#2325)
* fix a very strange bug caused by massive timestamps sent to discord

* remove 'gratituous' spaces
2018-02-11 08:35:46 +01:00
Pascal
3df3741922 backport/fix(GuildDelete): disconnect voice and cleanup GuildChannels 2018-02-05 13:03:43 +01:00
Pascal
96904b1826 fix(AudioPlayer): add opus to destructured key of stream options
This fixes #2079 (VoiceConnection#playOpusStream being broken)
2018-02-05 10:42:47 +01:00
Lasse Niermann
1f14758e0c docs(ClientUser): mark email field as user account only property (#2306)
* Store Mail - User Account Only

Added that info

* docs(ClientUser): mark email field as nullable
2018-02-01 20:06:59 +01:00
Lewdcario
52c402faea fix(resolveColor): not interpreting DEFAULT correctly 2018-01-28 19:04:55 -06:00
Lewdcario
e978253896 fix(Webhook#send): incorrect docs + return
Two things:
* Documentation doesn't account for possible raw object return
* Allows the return to be a Message object as well, like the docs originally intended
2018-01-24 13:06:05 -06:00
Pascal
8df1ac9920 fix(startTyping): return, to not overwrite already existing entries 2018-01-22 14:09:27 +01:00
Cynthia Lin
00e2f39ea1 fix(client#voiceConnections): Incorrect docs description (#2280) 2018-01-20 22:09:39 -06:00
Pascal
b5ff309bf9 fix(CategoryChannel): set the type to 'category' and document its type 2018-01-20 23:27:18 +01:00
Schuyler Cebulskie
6e10d258b6 Tweak readme and docs welcome page 2018-01-18 20:16:59 -05:00
Schuyler Cebulskie
aafb291ce2 Update typings submodule URL 2018-01-18 19:45:04 -05:00
Schuyler Cebulskie
51ddd6595c Update repository references 2018-01-18 14:40:02 -05:00
Schuyler Cebulskie
bb8ed98a85 Merge remote-tracking branch 'origin/11.3.1-dev' into 11.3-dev 2018-01-18 14:32:36 -05:00
Amish Shah
c49c5576d0 Point discord.js-docgen dependency to its new location 2018-01-18 17:56:46 +00:00
MaySoMusician
8cbefcc081 [v11.3.x] Fix param to setPresence in setActivity (#2270)
ClientUser#setPresence in master branch (latest) calls client.presences.setClientPresence, but that in v11.3 does not
so i change parameter sent to setPresence for clearing the game activity, from activity:null to game:null,
for now until setPresence gets updated
2018-01-18 02:39:54 -06:00
Isabella
932980e91f fix(guild#createRole): incorrect guild reference 2018-01-16 08:28:08 -06:00
Schuyler Cebulskie
2c8eb8a1ec Fix grammar 2018-01-14 19:03:21 -05:00
128 changed files with 7388 additions and 940 deletions

View File

@@ -77,6 +77,7 @@
"no-undef-init": "error",
"callback-return": "error",
"getter-return": "off",
"handle-callback-err": "error",
"no-mixed-requires": "error",
"no-new-require": "error",

View File

@@ -11,7 +11,7 @@ To get ready to work on the codebase, please do the following:
1. Fork & clone the repository, and make sure you're on the **master** branch
2. Run `npm install`
3. If you're working on voice, also run `npm install node-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!
5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid
6. [Submit a pull request](https://github.com/hydrabolt/discord.js/compare)
6. [Submit a pull request](https://github.com/discordjs/discord.js/compare)

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "typings"]
path = typings
url = https://github.com/zajrik/discord.js-typings

View File

@@ -24,3 +24,5 @@ webpack/
webpack.config.js
.github/
test/
tsconfig.json
tslint.json

View File

@@ -1,7 +1,9 @@
language: node_js
node_js:
- "6"
- "7"
- "8"
- "10"
- "12"
cache:
directories:
- node_modules

View File

@@ -8,8 +8,8 @@
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></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://david-dm.org/discordjs/discord.js"><img src="https://img.shields.io/david/discordjs/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
</p>
<p>
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
@@ -29,22 +29,21 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to
**Node.js 6.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js --save`
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
Without voice support: `npm install discord.js`
With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discord.js @discordjs/opus`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
### Audio engines
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
Using opusscript is only recommended for development environments where node-opus is tough to get working.
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus.
Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working.
For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers.
### Optional packages
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil --save`)
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack --save`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil`)
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium --save`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers --save`)
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`)
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
## Example usage
```js
@@ -52,31 +51,35 @@ const Discord = require('discord.js');
const client = new Discord.Client();
client.on('ready', () => {
console.log('I am ready!');
console.log(`Logged in as ${client.user.tag}!`);
});
client.on('message', message => {
if (message.content === 'ping') {
message.reply('pong');
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
}
});
client.login('your token');
client.login('token');
```
## Links
* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site))
* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
* [Documentation](https://discord.js.org/#/docs)
* [Discord.js server](https://discord.gg/bRCvFy9)
* [Discord API server](https://discord.gg/rV4BwdK)
* [GitHub](https://github.com/hydrabolt/discord.js)
* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
* [Discord.js Discord server](https://discord.gg/bRCvFy9)
* [Discord API Discord server](https://discord.gg/discord-api)
* [GitHub](https://github.com/discordjs/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js)
* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc))
* [Related libraries](https://discordapi.com/unofficial/libs.html)
### Extensions
* [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs).
See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle

View File

@@ -0,0 +1,163 @@
# Sending Attachments
In here you'll see a few examples showing how you can send an attachment using discord.js.
## Sending an attachment using a URL
There are a few ways you can do this, but we'll show you the easiest.
The following examples use [Attachment](/#/docs/main/stable/class/Attachment).
```js
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
// Create an instance of a Discord client
const client = new Client();
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
client.on('message', message => {
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using Attachment
const attachment = new Attachment('https://i.imgur.com/w3duR07.png');
// Send the attachment in the message channel
message.channel.send(attachment);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
And here is the result:
![Image showing the result](/static/attachment-example1.png)
But what if you want to send an attachment with a message content? Fear not, for it is easy to do that too! We'll recommend reading [the TextChannel's "send" function documentation](/#/docs/main/stable/class/TextChannel?scrollTo=send) to see what other options are available.
```js
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
// Create an instance of a Discord client
const client = new Client();
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
client.on('message', message => {
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using Attachment
const attachment = new Attachment('https://i.imgur.com/w3duR07.png');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author},`, attachment);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
And here's the result of this one:
![Image showing the result](/static/attachment-example2.png)
## Sending a local file or buffer
Sending a local file isn't hard either! We'll be using [Attachment](/#/docs/main/stable/class/Attachment) for these examples too.
```js
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
// Create an instance of a Discord client
const client = new Client();
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
client.on('message', message => {
// If the message is '!rip'
if (message.content === '!rip') {
// Create the attachment using Attachment
const attachment = new Attachment('./rip.png');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author},`, attachment);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
The results are the same as the URL examples:
![Image showing result](/static/attachment-example1.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!
In the following example, we'll be getting the buffer from a `memes.txt` file, and send it in the message channel.
You can use any buffer you want, and send it. Just make sure to overwrite the filename if it isn't an image!
```js
// Extract the required classes from the discord.js module
const { Client, Attachment } = require('discord.js');
// Import the native fs module
const fs = require('fs');
// Create an instance of a Discord client
const client = new Client();
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
client.on('message', message => {
// If the message is '!memes'
if (message.content === '!memes') {
// Get the buffer from the 'memes.txt', assuming that the file exists
const buffer = fs.readFileSync('./memes.txt');
/**
* Create the attachment using Attachment,
* overwritting the default file name to 'memes.txt'
* Read more about it over at
* http://discord.js.org/#/docs/main/stable/class/Attachment
*/
const attachment = new Attachment(buffer, 'memes.txt');
// Send the attachment in the message channel with a content
message.channel.send(`${message.author}, here are your memes!`, attachment);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
And of course, the results are:
![Attachment File example 3](/static/attachment-example3.png)

View File

@@ -1,6 +1,6 @@
/*
Send a user a link to their avatar
*/
/**
* Send a user a link to their avatar
*/
// Import the discord.js module
const Discord = require('discord.js');
@@ -8,11 +8,10 @@ const Discord = require('discord.js');
// Create an instance of a Discord client
const client = new Discord.Client();
// The token of your bot - https://discordapp.com/developers/applications/me
const token = 'your bot token here';
// The ready event is vital, it means that your bot will only start reacting to information
// from Discord _after_ ready is emitted
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
@@ -26,5 +25,5 @@ client.on('message', message => {
}
});
// Log our bot in
client.login(token);
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');

38
docs/examples/embed.js Normal file
View File

@@ -0,0 +1,38 @@
/**
* An example of how you can send embeds
*/
// Extract the required classes from the discord.js module
const { Client, RichEmbed } = require('discord.js');
// Create an instance of a Discord client
const client = new Client();
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
client.on('message', message => {
// If the message is "how to embed"
if (message.content === 'how to embed') {
// We can create embeds using the MessageEmbed constructor
// Read more about all that you can do with the constructor
// over at https://discord.js.org/#/docs/main/stable/class/RichEmbed
const embed = new RichEmbed()
// Set the title of the field
.setTitle('A slick little embed')
// Set the color of the embed
.setColor(0xFF0000)
// Set the main content of the embed
.setDescription('Hello, this is a slick embed!');
// Send the embed to the same channel as the message
message.channel.send(embed);
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');

View File

@@ -1,6 +1,6 @@
/*
A bot that welcomes new guild members when they join
*/
/**
* A bot that welcomes new guild members when they join
*/
// Import the discord.js module
const Discord = require('discord.js');
@@ -8,11 +8,10 @@ const Discord = require('discord.js');
// Create an instance of a Discord client
const client = new Discord.Client();
// The token of your bot - https://discordapp.com/developers/applications/me
const token = 'your bot token here';
// The ready event is vital, it means that your bot will only start reacting to information
// from Discord _after_ ready is emitted
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
@@ -20,12 +19,12 @@ client.on('ready', () => {
// Create an event listener for new guild members
client.on('guildMemberAdd', member => {
// Send the message to a designated channel on a server:
const channel = member.guild.channels.find('name', 'member-log');
const channel = member.guild.channels.find(ch => ch.name === 'member-log');
// Do nothing if the channel wasn't found on this server
if (!channel) return;
// Send the message, mentioning the member
channel.send(`Welcome to the server, ${member}`);
});
// Log our bot in
client.login(token);
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');

145
docs/examples/moderation.md Normal file
View File

@@ -0,0 +1,145 @@
# Moderation
In here, you'll see some basic examples for kicking and banning a member.
## Kicking a member
Let's say you have a member that you'd like to kick. Here is an example of how you *can* do it.
```js
// Import the discord.js module
const Discord = require('discord.js');
// Create an instance of a Discord client
const client = new Discord.Client();
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
client.on('message', message => {
// Ignore messages that aren't from a guild
if (!message.guild) return;
// If the message content starts with "!kick"
if (message.content.startsWith('!kick')) {
// Assuming we mention someone in the message, this will return the user
// Read more about mentions over at https://discord.js.org/#/docs/main/stable/class/MessageMentions
const user = message.mentions.users.first();
// If we have a user mentioned
if (user) {
// Now we get the member from the user
const member = message.guild.member(user);
// If the member is in the guild
if (member) {
/**
* Kick the member
* Make sure you run this on a member, not a user!
* There are big differences between a user and a member
*/
member.kick('Optional reason that will display in the audit logs').then(() => {
// We let the message author know we were able to kick the person
message.reply(`Successfully kicked ${user.tag}`);
}).catch(err => {
// An error happened
// This is generally due to the bot not being able to kick the member,
// either due to missing permissions or role hierarchy
message.reply('I was unable to kick the member');
// Log the error
console.error(err);
});
} else {
// The mentioned user isn't in this guild
message.reply('That user isn\'t in this guild!');
}
// Otherwise, if no user was mentioned
} else {
message.reply('You didn\'t mention the user to kick!');
}
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
And the result is:
![Image showing the result](/static/kick-example.png)
## Banning a member
Banning works the same way as kicking, but it has slightly more options that can be changed.
```js
// Import the discord.js module
const Discord = require('discord.js');
// Create an instance of a Discord client
const client = new Discord.Client();
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
client.on('message', message => {
// Ignore messages that aren't from a guild
if (!message.guild) return;
// if the message content starts with "!ban"
if (message.content.startsWith('!ban')) {
// Assuming we mention someone in the message, this will return the user
// Read more about mentions over at https://discord.js.org/#/docs/main/stable/class/MessageMentions
const user = message.mentions.users.first();
// If we have a user mentioned
if (user) {
// Now we get the member from the user
const member = message.guild.member(user);
// If the member is in the guild
if (member) {
/**
* Ban the member
* Make sure you run this on a member, not a user!
* There are big differences between a user and a member
* Read more about what ban options there are over at
* https://discord.js.org/#/docs/main/stable/class/GuildMember?scrollTo=ban
*/
member.ban({
reason: 'They were bad!',
}).then(() => {
// We let the message author know we were able to ban the person
message.reply(`Successfully banned ${user.tag}`);
}).catch(err => {
// An error happened
// This is generally due to the bot not being able to ban the member,
// either due to missing permissions or role hierarchy
message.reply('I was unable to ban the member');
// Log the error
console.error(err);
});
} else {
// The mentioned user isn't in this guild
message.reply('That user isn\'t in this guild!');
}
} else {
// Otherwise, if no user was mentioned
message.reply('You didn\'t mention the user to ban!');
}
}
});
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');
```
And the result is:
![Image showing the result](/static/ban-example.png)

View File

@@ -1,6 +1,6 @@
/*
A ping pong bot, whenever you send "ping", it replies "pong".
*/
/**
* A ping pong bot, whenever you send "ping", it replies "pong".
*/
// Import the discord.js module
const Discord = require('discord.js');
@@ -8,11 +8,10 @@ const Discord = require('discord.js');
// Create an instance of a Discord client
const client = new Discord.Client();
// The token of your bot - https://discordapp.com/developers/applications/me
const token = 'your bot token here';
// The ready event is vital, it means that your bot will only start reacting to information
// from Discord _after_ ready is emitted
/**
* The ready event is vital, it means that only _after_ this will your bot start reacting to information
* received from Discord
*/
client.on('ready', () => {
console.log('I am ready!');
});
@@ -26,5 +25,5 @@ client.on('message', message => {
}
});
// Log our bot in
client.login(token);
// Log our bot in using the token from https://discordapp.com/developers/applications/me
client.login('your token here');

View File

@@ -1,6 +1,6 @@
/*
Send a message using a webhook
*/
/**
* Send a message using a webhook
*/
// Import the discord.js module
const Discord = require('discord.js');

View File

@@ -8,16 +8,16 @@ Update to Node.js 6.0.0 or newer.
## How do I get voice working?
- Install FFMPEG.
- Install either the `node-opus` package or the `opusscript` package.
node-opus is greatly preferred, due to it having significantly better performance.
- Install either the `@discordjs/opus` package or the `opusscript` package.
@discordjs/opus is greatly preferred, due to it having significantly better performance.
## How do I install FFMPEG?
- **npm:** `npm install --save ffmpeg-binaries`
- **npm:** `npm install ffmpeg-binaries`
- **Ubuntu 16.04:** `sudo apt install ffmpeg`
- **Ubuntu 14.04:** `sudo apt-get install libav-tools`
- **Windows:** See the [FFMPEG section of AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md#download-ffmpeg).
- **Windows:** `npm install ffmpeg-binaries` or see the [FFMPEG section of AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md#download-ffmpeg).
## How do I set up node-opus?
- **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats!
## How do I set up @discordjs/opus?
- **Ubuntu:** Simply run `npm install @discordjs/opus`, and it's done. Congrats!
- **Windows:** Run `npm install --global --production windows-build-tools` in an admin command prompt or PowerShell.
Then, running `npm install node-opus` in your bot's directory should successfully build it. Woo!
Then, running `npm install @discordjs/opus` in your bot's directory should successfully build it. Woo!

View File

@@ -1,19 +1,31 @@
# Version 11.6.0
v11.6.0 backports new features from the in-development v12, and fixes bugs in the v11.5.x releases.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.6.0) for a full list of changes, including information about deprecations.
# Version 11.5.0
v11.5.0 backports new features from the in-development v12, and fixes bugs in the v11.4.x releases.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.5.0) for a full list of changes, including information about deprecations.
# Version 11.4.0
v11.4.0 backports many new features such as Rich Presence and bugfixes from v11.3.0.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.4.0) for a full list of changes, including information about deprecations.
# Version 11.3.0
v11.3.0 backports many new features and bug fixes from the in-development v12.
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.3.0) for a full list of changes, including information about deprecations.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.3.0) for a full list of changes, including information about deprecations.
# Version 11.2.0
v11.2.0 fixes a lot of bugs we encountered along the 11.1.0 release, as well as support for new features such as Message Attachments and UserGuildSettings.
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.2.0) for a full list of changes, including information about deprecations.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.2.0) for a full list of changes, including information about deprecations.
# Version 11.1.0
v11.1.0 features improved voice and gateway stability, as well as support for new features such as audit logs and searching for messages.
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.1.0) for a full list of changes, including
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.1.0) for a full list of changes, including
information about deprecations.
# Version 11
Version 11 contains loads of new and improved features, optimisations, and bug fixes.
See [the changelog](https://github.com/hydrabolt/discord.js/releases/tag/11.0.0) for a full list of changes.
See [the changelog](https://github.com/discordjs/discord.js/releases/tag/11.0.0) for a full list of changes.
## Significant additions
* Message Reactions and Embeds (rich text)

View File

@@ -8,8 +8,9 @@
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
<a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></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://david-dm.org/discordjs/discord.js"><img src="https://img.shields.io/david/discordjs/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
<a href="https://www.patreon.com/discordjs"><img src="https://img.shields.io/badge/donate-patreon-F96854.svg" alt="Patreon" /></a>
</p>
<p>
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
@@ -17,14 +18,14 @@
</div>
# Welcome!
Welcome to the discord.js v11.3 documentation.
The v11.3 release contains backports of many features and bug fixes from the in-development v12, such as categories and animated emoji support.
Welcome to the discord.js v11.6 documentation.
The v11.6 release contains bugfixes from v11.5 and backports features from the in-development v12.
v12 is still very much a work-in-progress, as we're aiming to make it the best it can possibly be before releasing.
If you are flike to live life on the bleeding-edge, check out the master branch.
If you are fond of living life on the bleeding-edge, check out the master branch.
## About
discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the
discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to interact with the
[Discord API](https://discordapp.com/developers/docs/intro) very easily.
- Object-oriented
@@ -36,22 +37,22 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to
**Node.js 6.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.
Without voice support: `npm install discord.js --save`
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
Without voice support: `npm install discord.js`
With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discord.js @discordjs/opus`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`
### Audio engines
The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus.
Using opusscript is only recommended for development environments where node-opus is tough to get working.
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus.
Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working.
For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers.
### Optional packages
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil --save`)
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack --save`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil`)
- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack`)
- One of the following packages can be installed for faster voice packet encryption and decryption:
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium --save`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers --save`)
- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`)
- [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`)
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
- [uws](https://www.npmjs.com/package/@discordjs/uws) for a much faster WebSocket connection (`npm install @discordjs/uws`)
## Example usage
```js
@@ -59,31 +60,35 @@ const Discord = require('discord.js');
const client = new Discord.Client();
client.on('ready', () => {
console.log('I am ready!');
console.log(`Logged in as ${client.user.tag}!`);
});
client.on('message', message => {
if (message.content === 'ping') {
message.reply('pong');
client.on('message', msg => {
if (msg.content === 'ping') {
msg.reply('pong');
}
});
client.login('your token');
client.login('token');
```
## Links
* [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site))
* [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website))
* [Documentation](https://discord.js.org/#/docs)
* [Discord.js server](https://discord.gg/bRCvFy9)
* [Discord API server](https://discord.gg/rV4BwdK)
* [GitHub](https://github.com/hydrabolt/discord.js)
* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide))
* [Discord.js Discord server](https://discord.gg/bRCvFy9)
* [Discord API Discord server](https://discord.gg/discord-api)
* [GitHub](https://github.com/discordjs/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js)
* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc))
* [Related libraries](https://discordapi.com/unofficial/libs.html)
### Extensions
* [RPC](https://www.npmjs.com/package/discord-rpc) ([source](https://github.com/discordjs/RPC))
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](https://discord.js.org/#/docs).
See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
See [the contribution guide](https://github.com/discordjs/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR.
## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle

View File

@@ -18,7 +18,13 @@
path: ping.js
- name: Avatars
path: avatars.js
- name: Attachments
path: attachments.md
- name: Server greeting
path: greeting.js
- name: Message Embed
path: embed.js
- name: Moderation
path: moderation.md
- name: Webhook
path: webhook.js

View File

@@ -4,12 +4,16 @@ Voice in discord.js can be used for many things, such as music bots, recording o
In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a `VoiceConnection`, where you can start streaming and receiving audio.
To get started, make sure you have:
* ffmpeg - `npm install ffmpeg-binaries`
* FFmpeg - `npm install ffmpeg-binaries`
* an opus encoder, choose one from below:
* `npm install opusscript`
* `npm install node-opus`
* `npm install @discordjs/opus`
* a good network connection
The preferred opus engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus.
Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working.
For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers.
## Joining a voice channel
The example below reacts to a message and joins the sender's voice channel, catching any errors. This is important
as it allows us to obtain a `VoiceConnection` that we can start to stream audio with.

View File

@@ -1,10 +1,10 @@
# Web builds
In addition to your usual Node applications, discord.js has special distributions available that are capable of running in web browsers.
This is useful for client-side web apps that need to interact with the Discord API.
[Webpack 2](https://webpack.js.org/) is used to build these.
[Webpack 3](https://webpack.js.org/) is used to build these.
## Usage
You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/hydrabolt/discord.js/tree/webpack) of the GitHub repository.
You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/discordjs/discord.js/tree/webpack) of the GitHub repository.
There is a file for each branch and version of the library, and the ones ending in `.min.js` are minified to substantially reduce the size of the source code.
Include the file on the page just as you would any other JS library, like so:
@@ -23,7 +23,7 @@ The usage of the API isn't any different from using it in Node.js.
## Example
```html
<script type="text/javascript" src="discord.11.3.0.min.js"></script>
<script type="text/javascript" src="discord.11.6.4.min.js"></script>
<script type="text/javascript">
const client = new Discord.Client();

View File

@@ -1,6 +1,6 @@
{
"name": "discord.js",
"version": "11.3.0",
"version": "11.6.4",
"description": "A powerful library for interacting with the Discord API",
"main": "./src/index",
"types": "./typings/index.d.ts",
@@ -10,11 +10,12 @@
"docs:test": "docgen --source src --custom docs/index.yml",
"lint": "eslint src",
"lint:fix": "eslint --fix src",
"lint:typings": "tslint typings/index.d.ts typings/discord.js-test.ts",
"webpack": "parallel-webpack"
},
"repository": {
"type": "git",
"url": "git+https://github.com/hydrabolt/discord.js.git"
"url": "git+https://github.com/discordjs/discord.js.git"
},
"keywords": [
"discord",
@@ -27,33 +28,63 @@
"author": "Amish Shah <amishshah.2k@gmail.com>",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/hydrabolt/discord.js/issues"
"url": "https://github.com/discordjs/discord.js/issues"
},
"homepage": "https://github.com/hydrabolt/discord.js#readme",
"homepage": "https://github.com/discordjs/discord.js#readme",
"runkitExampleFilename": "./docs/examples/ping.js",
"dependencies": {
"long": "^3.2.0",
"prism-media": "^0.0.1",
"snekfetch": "^3.6.1",
"long": "^4.0.0",
"prism-media": "^0.0.4",
"snekfetch": "^3.6.4",
"tweetnacl": "^1.0.0",
"ws": "^4.0.0"
"ws": "^6.0.0"
},
"peerDependencies": {
"bufferutil": "^3.0.3",
"@discordjs/uws": "^10.149.0",
"bufferutil": "^4.0.0",
"erlpack": "discordapp/erlpack",
"libsodium-wrappers": "^0.7.3",
"@discordjs/opus": "^0.1.0",
"node-opus": "^0.2.7",
"opusscript": "^0.0.6",
"sodium": "^2.0.3",
"libsodium-wrappers": "^0.5.4",
"uws": "^9.14.0"
"sodium": "^2.0.3"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"erlpack": {
"optional": true
},
"@discordjs/opus": {
"optional": true
},
"node-opus": {
"optional": true
},
"opusscript": {
"optional": true
},
"sodium": {
"optional": true
},
"libsodium-wrappers": {
"optional": true
},
"uws": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^8.5.7",
"discord.js-docgen": "hydrabolt/discord.js-docgen",
"eslint": "^4.15.0",
"parallel-webpack": "^2.2.0",
"uglifyjs-webpack-plugin": "^1.1.6",
"webpack": "^3.10.0"
"@types/node": "^9.4.6",
"discord.js-docgen": "discordjs/docgen",
"eslint": "^5.4.0",
"parallel-webpack": "^2.3.0",
"tslint": "^3.15.1",
"tslint-config-typings": "^0.2.4",
"typescript": "^3.0.1",
"uglifyjs-webpack-plugin": "^1.3.0",
"webpack": "^4.17.0"
},
"engines": {
"node": ">=6.0.0"
@@ -61,10 +92,12 @@
"browser": {
"ws": false,
"uws": false,
"@discordjs/uws": false,
"erlpack": false,
"prism-media": false,
"opusscript": false,
"node-opus": false,
"@discordjs/opus": false,
"tweetnacl": false,
"sodium": false,
"src/sharding/Shard.js": false,
@@ -73,6 +106,7 @@
"src/client/voice/dispatcher/StreamDispatcher.js": false,
"src/client/voice/opus/BaseOpusEngine.js": false,
"src/client/voice/opus/NodeOpusEngine.js": false,
"src/client/voice/opus/DiscordJsOpusEngine.js": false,
"src/client/voice/opus/OpusEngineList.js": false,
"src/client/voice/opus/OpusScriptEngine.js": false,
"src/client/voice/pcm/ConverterEngine.js": false,

View File

@@ -116,6 +116,7 @@ class Client extends EventEmitter {
* Presences that have been received for the client user's friends, mapped by user IDs
* <warn>This is only filled when using a user account.</warn>
* @type {Collection<Snowflake, Presence>}
* @deprecated
*/
this.presences = new Collection();
@@ -186,15 +187,15 @@ class Client extends EventEmitter {
/**
* Current status of the client's connection to Discord
* @type {?number}
* @type {Status}
* @readonly
*/
get status() {
return this.ws.connection.status;
return this.ws.connection ? this.ws.connection.status : Constants.Status.IDLE;
}
/**
* How long it has been since the client last entered the `READY` state
* How long it has been since the client last entered the `READY` state in milliseconds
* @type {?number}
* @readonly
*/
@@ -212,7 +213,7 @@ class Client extends EventEmitter {
}
/**
* All active voice connections that have been established, mapped by channel ID
* All active voice connections that have been established, mapped by guild ID
* @type {Collection<Snowflake, VoiceConnection>}
* @readonly
*/
@@ -266,14 +267,16 @@ class Client extends EventEmitter {
* Logs the client in, establishing a websocket connection to Discord.
* <info>Both bot and regular user accounts are supported, but it is highly recommended to use a bot account whenever
* possible. User accounts are subject to harsher ratelimits and other restrictions that don't apply to bot accounts.
* Bot accounts also have access to many features that user accounts cannot utilise. User accounts that are found to
* be abusing/overusing the API will be banned, locking you out of Discord entirely.</info>
* Bot accounts also have access to many features that user accounts cannot utilise. Automating a user account is
* considered a violation of Discord's ToS.</info>
* @param {string} token Token of the account to log in with
* @returns {Promise<string>} Token of the account used
* @example
* client.login('my token');
* client.login('my token')
* .then(console.log)
* .catch(console.error);
*/
login(token) {
login(token = this.token) {
return this.rest.methods.login(token);
}
@@ -294,6 +297,7 @@ class Client extends EventEmitter {
* <info>This can be done automatically every 30 seconds by enabling {@link ClientOptions#sync}.</info>
* <warn>This is only available when using a user account.</warn>
* @param {Guild[]|Collection<Snowflake, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
* @deprecated
*/
syncGuilds(guilds = this.guilds) {
if (this.user.bot) return;
@@ -319,6 +323,10 @@ class Client extends EventEmitter {
* Obtains an invite from Discord.
* @param {InviteResolvable} invite Invite code or URL
* @returns {Promise<Invite>}
* @example
* client.fetchInvite('https://discord.gg/bRCvFy9')
* .then(invite => console.log(`Obtained invite with code: ${invite.code}`))
* .catch(console.error);
*/
fetchInvite(invite) {
const code = this.resolver.resolveInviteCode(invite);
@@ -330,6 +338,10 @@ class Client extends EventEmitter {
* @param {Snowflake} id ID of the webhook
* @param {string} [token] Token for the webhook
* @returns {Promise<Webhook>}
* @example
* client.fetchWebhook('id', 'token')
* .then(webhook => console.log(`Obtained webhook with name: ${webhook.name}`))
* .catch(console.error);
*/
fetchWebhook(id, token) {
return this.rest.methods.getWebhook(id, token);
@@ -337,7 +349,11 @@ class Client extends EventEmitter {
/**
* Obtains the available voice regions from Discord.
* @returns {Collection<string, VoiceRegion>}
* @returns {Promise<Collection<string, VoiceRegion>>}
* @example
* client.fetchVoiceRegions()
* .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`))
* .catch(console.error);
*/
fetchVoiceRegions() {
return this.rest.methods.fetchVoiceRegions();
@@ -367,12 +383,9 @@ class Client extends EventEmitter {
if (!channel.messages) continue;
channels++;
for (const message of channel.messages.values()) {
if (now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs) {
channel.messages.delete(message.id);
messages++;
}
}
messages += channel.messages.sweep(
message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs
);
}
this.emit('debug', `Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`);
@@ -381,30 +394,31 @@ class Client extends EventEmitter {
/**
* Obtains the OAuth Application of the bot from Discord.
* <warn>Bots can only fetch their own profile.</warn>
* @param {Snowflake} [id='@me'] ID of application to fetch
* @returns {Promise<OAuth2Application>}
* @example
* client.fetchApplication()
* .then(application => console.log(`Obtained application with name: ${application.name}`))
* .catch(console.error);
*/
fetchApplication(id = '@me') {
if (id !== '@me') process.emitWarning('fetchApplication: use "@me" as an argument', 'DeprecationWarning');
return this.rest.methods.getApplication(id);
}
/**
* Generates a link that can be used to invite the bot to a guild.
* <warn>This is only available when using a bot account.</warn>
* @param {PermissionResolvable[]|number} [permissions] Permissions to request
* @param {PermissionResolvable} [permissions] Permissions to request
* @returns {Promise<string>}
* @example
* client.generateInvite(['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);
*/
generateInvite(permissions) {
if (permissions) {
if (permissions instanceof Array) permissions = Permissions.resolve(permissions);
} else {
permissions = 0;
}
permissions = Permissions.resolve(permissions);
return this.fetchApplication().then(application =>
`https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot`
);
@@ -479,7 +493,7 @@ class Client extends EventEmitter {
this.presences.get(id).update(presence);
return;
}
this.presences.set(id, new Presence(presence));
this.presences.set(id, new Presence(presence, this));
}
/**
@@ -498,7 +512,7 @@ class Client extends EventEmitter {
* @param {ClientOptions} [options=this.options] Options to validate
* @private
*/
_validateOptions(options = this.options) {
_validateOptions(options = this.options) { // eslint-disable-line complexity
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) {
throw new TypeError('The shardCount option must be a number.');
}
@@ -529,6 +543,9 @@ class Client extends EventEmitter {
throw new TypeError('The restWsBridgeTimeout option must be a number.');
}
if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.');
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
throw new TypeError('The retryLimit options must be a number.');
}
}
}

View File

@@ -2,12 +2,14 @@ const Constants = require('../util/Constants');
const Util = require('../util/Util');
const Guild = require('../structures/Guild');
const User = require('../structures/User');
const CategoryChannel = require('../structures/CategoryChannel');
const DMChannel = require('../structures/DMChannel');
const Emoji = require('../structures/Emoji');
const GuildChannel = require('../structures/GuildChannel');
const TextChannel = require('../structures/TextChannel');
const VoiceChannel = require('../structures/VoiceChannel');
const GuildChannel = require('../structures/GuildChannel');
const CategoryChannel = require('../structures/CategoryChannel');
const NewsChannel = require('../structures/NewsChannel');
const StoreChannel = require('../structures/StoreChannel');
const DMChannel = require('../structures/DMChannel');
const GroupDMChannel = require('../structures/GroupDMChannel');
class ClientDataManager {
@@ -39,10 +41,10 @@ class ClientDataManager {
return guild;
}
newUser(data) {
newUser(data, cache = true) {
if (this.client.users.has(data.id)) return this.client.users.get(data.id);
const user = new User(this.client, data);
this.client.users.set(user.id, user);
if (cache) this.client.users.set(user.id, user);
return user;
}
@@ -55,17 +57,28 @@ class ClientDataManager {
channel = new GroupDMChannel(this.client, data);
} else {
guild = guild || this.client.guilds.get(data.guild_id);
if (guild) {
if (data.type === Constants.ChannelTypes.TEXT) {
channel = new TextChannel(guild, data);
guild.channels.set(channel.id, channel);
} else if (data.type === Constants.ChannelTypes.VOICE) {
channel = new VoiceChannel(guild, data);
guild.channels.set(channel.id, channel);
} else if (data.type === Constants.ChannelTypes.CATEGORY) {
channel = new CategoryChannel(guild, data);
guild.channels.set(channel.id, channel);
if (already) {
channel = this.client.channels.get(data.id);
} else if (guild) {
switch (data.type) {
case Constants.ChannelTypes.TEXT:
channel = new TextChannel(guild, data);
break;
case Constants.ChannelTypes.VOICE:
channel = new VoiceChannel(guild, data);
break;
case Constants.ChannelTypes.CATEGORY:
channel = new CategoryChannel(guild, data);
break;
case Constants.ChannelTypes.NEWS:
channel = new NewsChannel(guild, data);
break;
case Constants.ChannelTypes.STORE:
channel = new StoreChannel(guild, data);
break;
}
guild.channels.set(channel.id, channel);
}
}

View File

@@ -46,7 +46,7 @@ class ClientDataResolver {
if (typeof user === 'string') return this.client.users.get(user) || null;
if (user instanceof GuildMember) return user.user;
if (user instanceof Message) return user.author;
if (user instanceof Guild) return user.owner;
if (user instanceof Guild) return this.resolveUser(user.ownerID);
return null;
}
@@ -171,7 +171,7 @@ class ClientDataResolver {
* @returns {string}
*/
resolveInviteCode(data) {
const inviteRegex = /discord(?:app\.com\/invite|\.gg)\/([\w-]{2,255})/i;
const inviteRegex = /discord(?:app\.com\/invite|\.gg(?:\/invite)?)\/([\w-]{2,255})/i;
const match = inviteRegex.exec(data);
if (match && match[1]) return match[1];
return data;
@@ -251,27 +251,22 @@ class ClientDataResolver {
if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertToBuffer(resource));
if (typeof resource === 'string') {
if (/^https?:\/\//.test(resource)) {
return snekfetch.get(resource).then(res => res.body instanceof Buffer ? res.body : Buffer.from(res.text));
}
return new Promise((resolve, reject) => {
if (/^https?:\/\//.test(resource)) {
snekfetch.get(resource)
.end((err, res) => {
if (err) return reject(err);
if (!(res.body instanceof Buffer)) return reject(new TypeError('The response body isn\'t a Buffer.'));
return resolve(res.body);
});
} else {
const file = path.resolve(resource);
fs.stat(file, (err, stats) => {
if (err) return reject(err);
if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`));
fs.readFile(file, (err2, data) => {
if (err2) reject(err2); else resolve(data);
});
return null;
const file = path.resolve(resource);
fs.stat(file, (err, stats) => {
if (err) return reject(err);
if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`));
fs.readFile(file, (err2, data) => {
if (err2) reject(err2);
else resolve(data);
});
}
return null;
});
});
} else if (resource.pipe && typeof resource.pipe === 'function') {
} else if (resource && resource.pipe && typeof resource.pipe === 'function') {
return new Promise((resolve, reject) => {
const buffers = [];
resource.once('error', reject);
@@ -312,10 +307,12 @@ class ClientDataResolver {
* ```
* [
* 'DEFAULT',
* 'WHITE',
* 'AQUA',
* 'GREEN',
* 'BLUE',
* 'PURPLE',
* 'LUMINOUS_VIVID_PINK',
* 'GOLD',
* 'ORANGE',
* 'RED',
@@ -326,6 +323,7 @@ class ClientDataResolver {
* 'DARK_GREEN',
* 'DARK_BLUE',
* 'DARK_PURPLE',
* 'DARK_VIVID_PINK',
* 'DARK_GOLD',
* 'DARK_ORANGE',
* 'DARK_RED',
@@ -351,6 +349,7 @@ class ClientDataResolver {
static resolveColor(color) {
if (typeof color === 'string') {
if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1));
if (color === 'DEFAULT') return 0;
color = Constants.Colors[color] || parseInt(color.replace('#', ''), 16);
} else if (color instanceof Array) {
color = (color[0] << 16) + (color[1] << 8) + color[2];

View File

@@ -43,6 +43,7 @@ class ClientManager {
const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`;
this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`);
this.client.ws.connect(gateway);
this.client.ws.connection.once('error', reject);
this.client.ws.connection.once('close', event => {
if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN));
if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD));

View File

@@ -8,6 +8,7 @@ class ActionsManager {
this.register(require('./MessageUpdate'));
this.register(require('./MessageReactionAdd'));
this.register(require('./MessageReactionRemove'));
this.register(require('./MessageReactionRemoveEmoji'));
this.register(require('./MessageReactionRemoveAll'));
this.register(require('./ChannelCreate'));
this.register(require('./ChannelDelete'));
@@ -20,6 +21,8 @@ class ActionsManager {
this.register(require('./GuildRoleCreate'));
this.register(require('./GuildRoleDelete'));
this.register(require('./GuildRoleUpdate'));
this.register(require('./InviteCreate'));
this.register(require('./InviteDelete'));
this.register(require('./UserGet'));
this.register(require('./UserUpdate'));
this.register(require('./UserNoteUpdate'));

View File

@@ -1,4 +1,5 @@
const Action = require('./Action');
const DMChannel = require('../../structures/DMChannel');
class ChannelDeleteAction extends Action {
constructor(client) {
@@ -17,6 +18,14 @@ class ChannelDeleteAction extends Action {
} else {
channel = this.deleted.get(data.id) || null;
}
if (channel) {
if (channel.messages && !(channel instanceof DMChannel)) {
for (const message of channel.messages.values()) {
message.deleted = true;
}
}
channel.deleted = true;
}
return { channel };
}

View File

@@ -1,15 +1,55 @@
const Action = require('./Action');
const TextChannel = require('../../structures/TextChannel');
const VoiceChannel = require('../../structures/VoiceChannel');
const CategoryChannel = require('../../structures/CategoryChannel');
const NewsChannel = require('../../structures/NewsChannel');
const StoreChannel = require('../../structures/StoreChannel');
const Constants = require('../../util/Constants');
const ChannelTypes = Constants.ChannelTypes;
const Util = require('../../util/Util');
class ChannelUpdateAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get(data.id);
let channel = client.channels.get(data.id);
if (channel) {
const oldChannel = Util.cloneObject(channel);
channel.setup(data);
// If the channel is changing types, we need to follow a different process
if (ChannelTypes[channel.type.toUpperCase()] !== data.type) {
// Determine which channel class we're changing to
let channelClass;
switch (data.type) {
case ChannelTypes.TEXT:
channelClass = TextChannel;
break;
case ChannelTypes.VOICE:
channelClass = VoiceChannel;
break;
case ChannelTypes.CATEGORY:
channelClass = CategoryChannel;
break;
case ChannelTypes.NEWS:
channelClass = NewsChannel;
break;
case ChannelTypes.STORE:
channelClass = StoreChannel;
break;
}
// Create the new channel instance and copy over cached data
const newChannel = new channelClass(channel.guild, data);
if (channel.messages && newChannel.messages) {
for (const [id, message] of channel.messages) newChannel.messages.set(id, message);
}
channel = newChannel;
this.client.channels.set(channel.id, channel);
} else {
channel.setup(data);
}
client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel);
return {
old: oldChannel,

View File

@@ -28,6 +28,9 @@ class GuildDeleteAction extends Action {
};
}
for (const channel of guild.channels.values()) this.client.channels.delete(channel.id);
if (guild.voiceConnection) guild.voiceConnection.disconnect();
// Delete guild
client.guilds.delete(guild.id);
this.deleted.set(guild.id, guild);
@@ -35,6 +38,7 @@ class GuildDeleteAction extends Action {
} else {
guild = this.deleted.get(data.id) || null;
}
if (guild) guild.deleted = true;
return { guild };
}

View File

@@ -4,6 +4,7 @@ class GuildEmojiDeleteAction extends Action {
handle(emoji) {
const client = this.client;
client.dataManager.killEmoji(emoji);
emoji.deleted = true;
return { emoji };
}
}

View File

@@ -13,8 +13,8 @@ class GuildMemberRemoveAction extends Action {
let member = null;
if (guild) {
member = guild.members.get(data.user.id);
guild.memberCount--;
if (member) {
guild.memberCount--;
guild._removeMember(member);
this.deleted.set(guild.id + data.user.id, member);
if (client.status === Constants.Status.READY) client.emit(Constants.Events.GUILD_MEMBER_REMOVE, member);
@@ -22,6 +22,7 @@ class GuildMemberRemoveAction extends Action {
} else {
member = this.deleted.get(guild.id + data.user.id) || null;
}
if (member) member.deleted = true;
}
return { guild, member };
}

View File

@@ -22,6 +22,7 @@ class GuildRoleDeleteAction extends Action {
} else {
role = this.deleted.get(guild.id + data.role_id) || null;
}
if (role) role.deleted = true;
}
return { role };

View File

@@ -0,0 +1,29 @@
'use strict';
const Action = require('./Action');
const Invite = require('../../structures/Invite');
const { Events } = require('../../util/Constants');
class InviteCreateAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const channel = client.channels.get(data.channel_id);
if (guild && channel) {
const inviteData = Object.assign(data, { guild, channel });
const invite = new Invite(client, inviteData);
/**
* Emitted when an invite is created.
* <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
* or `MANAGE_CHANNEL` permissions for the channel.</info>
* @event Client#inviteCreate
* @param {Invite} invite The invite that was created
*/
client.emit(Events.INVITE_CREATE, invite);
return { invite };
}
return { invite: null };
}
}
module.exports = InviteCreateAction;

View File

@@ -0,0 +1,25 @@
const Action = require('./Action');
const Invite = require('../../structures/Invite');
const { Events } = require('../../util/Constants');
class InviteDeleteAction extends Action {
handle(data) {
const client = this.client;
const guild = client.guilds.get(data.guild_id);
const channel = client.channels.get(data.channel_id);
if (guild && channel) {
const inviteData = Object.assign(data, { guild, channel });
const invite = new Invite(client, inviteData);
/**
* Emitted when an invite is deleted.
* <info> This event only triggers if the client has `MANAGE_GUILD` permissions for the guild,
* or `MANAGE_CHANNEL` permissions for the channel.</info>
* @event Client#inviteDelete
* @param {Invite} invite The invite that was deleted
*/
client.emit(Events.INVITE_DELETE, invite);
}
}
}
module.exports = InviteDeleteAction;

View File

@@ -16,7 +16,6 @@ class MessageCreateAction extends Action {
}
const lastMessage = messages[messages.length - 1];
channel.lastMessageID = lastMessage.id;
channel.lastMessage = lastMessage;
if (user) {
user.lastMessageID = lastMessage.id;
user.lastMessage = lastMessage;
@@ -31,7 +30,6 @@ class MessageCreateAction extends Action {
} else {
const message = channel._cacheMessage(new Message(channel, data, client));
channel.lastMessageID = data.id;
channel.lastMessage = message;
if (user) {
user.lastMessageID = data.id;
user.lastMessage = message;

View File

@@ -20,6 +20,7 @@ class MessageDeleteAction extends Action {
} else {
message = this.deleted.get(channel.id + data.id) || null;
}
if (message) message.deleted = true;
}
return { message };

View File

@@ -4,17 +4,21 @@ const Constants = require('../../util/Constants');
class MessageDeleteBulkAction extends Action {
handle(data) {
const client = this.client;
const channel = client.channels.get(data.channel_id);
const ids = data.ids;
const messages = new Collection();
for (const id of ids) {
const message = channel.messages.get(id);
if (message) messages.set(message.id, message);
const channel = this.client.channels.get(data.channel_id);
if (channel) {
for (const id of data.ids) {
const message = channel.messages.get(id);
if (message) {
message.deleted = true;
messages.set(message.id, message);
channel.messages.delete(id);
}
}
}
if (messages.size > 0) client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages);
if (messages.size > 0) this.client.emit(Constants.Events.MESSAGE_BULK_DELETE, messages);
return { messages };
}
}

View File

@@ -28,7 +28,7 @@ class MessageReactionAdd extends Action {
}
/**
* Emitted whenever a reaction is added to a message.
* Emitted whenever a reaction is added to a cached message.
* @event Client#messageReactionAdd
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user that applied the emoji or reaction emoji

View File

@@ -28,10 +28,10 @@ class MessageReactionRemove extends Action {
}
/**
* Emitted whenever a reaction is removed from a message.
* Emitted whenever a reaction is removed from a cached message.
* @event Client#messageReactionRemove
* @param {MessageReaction} messageReaction The reaction object
* @param {User} user The user that removed the emoji or reaction emoji
* @param {User} user The user whose emoji or reaction emoji was removed
*/
module.exports = MessageReactionRemove;

View File

@@ -17,7 +17,7 @@ class MessageReactionRemoveAll extends Action {
}
/**
* Emitted whenever all reactions are removed from a message.
* Emitted whenever all reactions are removed from a cached message.
* @event Client#messageReactionRemoveAll
* @param {Message} message The message the reactions were removed from
*/

View File

@@ -0,0 +1,27 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
class MessageReactionRemoveEmoji extends Action {
handle(data) {
// Verify channel
const channel = this.client.channels.get(data.channel_id);
if (!channel || channel.type === 'voice') return false;
// Verify message
const message = channel.messages.get(data.message_id);
if (!message) return false;
if (!data.emoji) return false;
// Verify reaction
const reaction = message._removeReaction(data.emoji);
if (reaction) this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction);
return { message, reaction };
}
}
/**
* Emitted whenever a reaction emoji is removed from a cached message.
* @event Client#messageReactionRemoveEmoji
* @param {MessageReaction} messageReaction The reaction object
*/
module.exports = MessageReactionRemoveEmoji;

View File

@@ -15,13 +15,17 @@ class APIRequest {
}
getRoute(url) {
let route = url.split('?')[0];
if (route.includes('/channels/') || route.includes('/guilds/')) {
const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/');
const majorID = route.substring(startInd).split('/')[2];
route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID);
const route = url.split('?')[0].split('/');
const routeBucket = [];
for (let i = 0; i < route.length; i++) {
// Reactions routes and sub-routes all share the same bucket
if (route[i - 1] === 'reactions') break;
// Literal IDs should only be taken account if they are the Major ID (the Channel/Guild ID)
if (/\d{16,19}/g.test(route[i]) && !/channels|guilds/.test(route[i - 1])) routeBucket.push(':id');
// All other parts of the route should be considered as part of the bucket identifier
else routeBucket.push(route[i]);
}
return route;
return routeBucket.join('/');
}
getAuth() {

View File

@@ -3,7 +3,7 @@
* @extends Error
*/
class DiscordAPIError extends Error {
constructor(path, error) {
constructor(path, error, method) {
super();
const flattened = this.constructor.flattenErrors(error.errors || error).join('\n');
this.name = 'DiscordAPIError';
@@ -20,6 +20,12 @@ class DiscordAPIError extends Error {
* @type {number}
*/
this.code = error.code;
/**
* The HTTP method used for the request
* @type {string}
*/
this.method = method;
}
/**

View File

@@ -16,8 +16,9 @@ class RESTManager {
}
destroy() {
for (const handlerID in this.handlers) {
this.handlers[handlerID].destroy();
for (const handlerKey of Object.keys(this.handlers)) {
const handler = this.handlers[handlerKey];
if (handler.destroy) handler.destroy();
}
}
@@ -27,6 +28,7 @@ class RESTManager {
request: apiRequest,
resolve,
reject,
retries: 0,
});
});
}

View File

@@ -4,9 +4,10 @@ const Permissions = require('../../util/Permissions');
const Constants = require('../../util/Constants');
const Endpoints = Constants.Endpoints;
const Collection = require('../../util/Collection');
const Snowflake = require('../../util/Snowflake');
const Util = require('../../util/Util');
const resolvePermissions = require('../../structures/shared/resolvePermissions');
const RichEmbed = require('../../structures/RichEmbed');
const User = require('../../structures/User');
const GuildMember = require('../../structures/GuildMember');
const Message = require('../../structures/Message');
@@ -21,6 +22,8 @@ const Guild = require('../../structures/Guild');
const VoiceRegion = require('../../structures/VoiceRegion');
const GuildAuditLogs = require('../../structures/GuildAuditLogs');
const MessageFlags = require('../../util/MessageFlags');
class RESTMethods {
constructor(restManager) {
this.rest = restManager;
@@ -30,9 +33,12 @@ class RESTMethods {
login(token = this.client.token) {
return new Promise((resolve, reject) => {
if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN);
if (!token || typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN);
token = token.replace(/^Bot\s*/i, '');
this.client.manager.connectToWebSocket(token, resolve, reject);
}).catch(e => {
this.client.destroy();
return Promise.reject(e);
});
}
@@ -55,6 +61,13 @@ class RESTMethods {
});
}
fetchEmbed(guildID) {
return this.rest.makeRequest('get', Endpoints.Guild(guildID).embed, true).then(data => ({
enabled: data.enabled,
channel: data.channel_id ? this.client.channels.get(data.channel_id) : null,
}));
}
sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code, reply } = {}, files = null) {
return new Promise((resolve, reject) => { // eslint-disable-line complexity
if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content);
@@ -121,9 +134,11 @@ class RESTMethods {
});
}
updateMessage(message, content, { embed, code, reply } = {}) {
updateMessage(message, content, { flags, embed, code, reply } = {}) {
if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content);
if (typeof flags !== 'undefined') flags = MessageFlags.resolve(flags);
// Wrap everything in a code block
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true);
@@ -137,8 +152,10 @@ class RESTMethods {
content = `${mention}${content ? `, ${content}` : ''}`;
}
if (embed instanceof RichEmbed) embed = embed.toJSON();
return this.rest.makeRequest('patch', Endpoints.Message(message), true, {
content, embed,
content, embed, flags,
}).then(data => this.client.actions.MessageUpdate.handle(data).updated);
}
@@ -172,14 +189,9 @@ class RESTMethods {
return this.rest.makeRequest('post', Endpoints.Guild(guild).ack, true).then(() => guild);
}
bulkDeleteMessages(channel, messages, filterOld) {
if (filterOld) {
messages = messages.filter(id =>
Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000
);
}
bulkDeleteMessages(channel, messages) {
return this.rest.makeRequest('post', Endpoints.Channel(channel).messages.bulkDelete, true, {
messages,
messages: messages,
}).then(() =>
this.client.actions.MessageDeleteBulk.handle({
channel_id: channel.id,
@@ -230,7 +242,7 @@ class RESTMethods {
include_nsfw: options.nsfw,
};
for (const key in options) if (options[key] === undefined) delete options[key];
for (const key of Object.keys(options)) if (options[key] === undefined) delete options[key];
const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&');
let endpoint;
@@ -252,36 +264,33 @@ class RESTMethods {
});
}
createChannel(guild, channelName, channelType, overwrites, reason) {
if (overwrites instanceof Collection || overwrites instanceof Array) {
overwrites = overwrites.map(overwrite => {
let allow = overwrite.allow || overwrite._allowed;
let deny = overwrite.deny || overwrite._denied;
if (allow instanceof Array) allow = Permissions.resolve(allow);
if (deny instanceof Array) deny = Permissions.resolve(deny);
const role = this.client.resolver.resolveRole(this, overwrite.id);
if (role) {
overwrite.id = role.id;
overwrite.type = 'role';
} else {
overwrite.id = this.client.resolver.resolveUserID(overwrite.id);
overwrite.type = 'member';
}
return {
allow,
deny,
type: overwrite.type,
id: overwrite.id,
};
});
}
createChannel(guild, name, options) {
const {
type,
topic,
nsfw,
bitrate,
userLimit,
parent,
permissionOverwrites,
position,
rateLimitPerUser,
reason,
} = options;
return this.rest.makeRequest('post', Endpoints.Guild(guild).channels, true, {
name: channelName,
type: channelType ? Constants.ChannelTypes[channelType.toUpperCase()] : 'text',
permission_overwrites: overwrites,
}, undefined, reason).then(data => this.client.actions.ChannelCreate.handle(data).channel);
name,
topic,
type: type ? Constants.ChannelTypes[type.toUpperCase()] : Constants.ChannelTypes.TEXT,
nsfw,
bitrate,
user_limit: userLimit,
parent_id: parent instanceof Channel ? parent.id : parent,
permission_overwrites: resolvePermissions.call(this, permissionOverwrites, guild),
position,
rate_limit_per_user: rateLimitPerUser,
},
undefined,
reason).then(data => this.client.actions.ChannelCreate.handle(data).channel);
}
createDM(recipient) {
@@ -339,11 +348,16 @@ class RESTMethods {
updateChannel(channel, _data, reason) {
const data = {};
data.name = (_data.name || channel.name).trim();
data.topic = _data.topic || channel.topic;
data.topic = typeof _data.topic === 'undefined' ? channel.topic : _data.topic;
data.nsfw = typeof _data.nsfw === 'undefined' ? channel.nsfw : _data.nsfw;
data.position = _data.position || channel.position;
data.bitrate = _data.bitrate || (channel.bitrate ? channel.bitrate * 1000 : undefined);
data.user_limit = _data.userLimit || channel.userLimit;
data.parent_id = _data.parent || (channel.parent ? channel.parent.id : undefined);
data.user_limit = typeof _data.userLimit !== 'undefined' ? _data.userLimit : channel.userLimit;
data.parent_id = _data.parent instanceof Channel ? _data.parent.id : _data.parent;
data.permission_overwrites = _data.permissionOverwrites ?
resolvePermissions.call(this, _data.permissionOverwrites, channel.guild) : undefined;
data.rate_limit_per_user = typeof _data.rateLimitPerUser !== 'undefined' ?
_data.rateLimitPerUser : channel.rateLimitPerUser;
return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data, undefined, reason).then(newData =>
this.client.actions.ChannelUpdate.handle(newData).updated
);
@@ -420,12 +434,7 @@ class RESTMethods {
return this.rest.makeRequest(
'delete', Endpoints.Guild(guild).Member(member), true,
undefined, undefined, reason)
.then(() =>
this.client.actions.GuildMemberRemove.handle({
guild_id: guild.id,
user: member.user,
}).member
);
.then(() => member);
}
createGuildRole(guild, data, reason) {
@@ -482,7 +491,7 @@ class RESTMethods {
return this.rest.makeRequest('get', Endpoints.Channel(channel).Message(messageID), true);
}
putGuildMember(guild, user, options) {
putGuildMember(guild, userID, options) {
options.access_token = options.accessToken;
if (options.roles) {
const roles = options.roles;
@@ -490,12 +499,16 @@ class RESTMethods {
options.roles = roles.map(role => role.id);
}
}
return this.rest.makeRequest('put', Endpoints.Guild(guild).Member(user.id), true, options)
return this.rest.makeRequest('put', Endpoints.Guild(guild).Member(userID), true, options)
.then(data => this.client.actions.GuildMemberGet.handle(guild, data).member);
}
getGuildMember(guild, user, cache) {
return this.rest.makeRequest('get', Endpoints.Guild(guild).Member(user.id), true).then(data => {
getGuild(guild) {
return this.rest.makeRequest('get', Endpoints.Guild(guild), true);
}
getGuildMember(guild, userID, cache) {
return this.rest.makeRequest('get', Endpoints.Guild(guild).Member(userID), true).then(data => {
if (cache) return this.client.actions.GuildMemberGet.handle(guild, data).member;
else return new GuildMember(guild, data);
});
@@ -503,10 +516,17 @@ class RESTMethods {
updateGuildMember(member, data, reason) {
if (data.channel) {
data.channel_id = this.client.resolver.resolveChannel(data.channel).id;
data.channel = null;
const channel = this.client.resolver.resolveChannel(data.channel);
if (!channel || channel.guild.id !== member.guild.id || channel.type !== 'voice') {
return Promise.reject(new Error('Could not resolve channel to a guild voice channel.'));
}
data.channel_id = channel.id;
data.channel = undefined;
} else if (data.channel === null) {
data.channel_id = null;
data.channel = undefined;
}
if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role);
if (data.roles) data.roles = [...new Set(data.roles.map(role => role instanceof Role ? role.id : role))];
let endpoint = Endpoints.Member(member);
// Fix your endpoints, discord ;-;
@@ -527,19 +547,21 @@ class RESTMethods {
if (member._roles.includes(role.id)) return resolve(member);
const listener = (oldMember, newMember) => {
if (!oldMember._roles.includes(role.id) && newMember._roles.includes(role.id)) {
if (newMember.id === member.id && !oldMember._roles.includes(role.id) && newMember._roles.includes(role.id)) {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
resolve(newMember);
}
};
this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener);
const timeout = this.client.setTimeout(() =>
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener), 10e3);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
reject(new Error('Adding the role timed out.'));
}, 10e3);
return this.rest.makeRequest('put', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason)
.catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
@@ -551,19 +573,21 @@ class RESTMethods {
if (!member._roles.includes(role.id)) return resolve(member);
const listener = (oldMember, newMember) => {
if (oldMember._roles.includes(role.id) && !newMember._roles.includes(role.id)) {
if (newMember.id === member.id && oldMember._roles.includes(role.id) && !newMember._roles.includes(role.id)) {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
resolve(newMember);
}
};
this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener);
const timeout = this.client.setTimeout(() =>
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener), 10e3);
const timeout = this.client.setTimeout(() => {
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
reject(new Error('Removing the role timed out.'));
}, 10e3);
return this.rest.makeRequest('delete', Endpoints.Member(member).Role(role.id), true, undefined, undefined, reason)
.catch(err => {
this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener);
this.client.clearTimeout(timeout);
reject(err);
});
@@ -618,6 +642,14 @@ class RESTMethods {
});
}
getGuildBan(guild, user) {
const id = this.client.resolver.resolveUserID(user);
return this.rest.makeRequest('get', `${Endpoints.Guild(guild).bans}/${id}`, true).then(ban => ({
reason: ban.reason,
user: this.client.dataManager.newUser(ban.user),
}));
}
getGuildBans(guild) {
return this.rest.makeRequest('get', Endpoints.Guild(guild).bans, true).then(bans =>
bans.reduce((collection, ban) => {
@@ -634,11 +666,11 @@ class RESTMethods {
const data = {};
data.name = _data.name || role.name;
data.position = typeof _data.position !== 'undefined' ? _data.position : role.position;
data.color = this.client.resolver.resolveColor(_data.color || role.color);
data.color = _data.color === null ? null : this.client.resolver.resolveColor(_data.color || role.color);
data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable;
if (_data.permissions) data.permissions = Permissions.resolve(_data.permissions);
if (typeof _data.permissions !== 'undefined') data.permissions = Permissions.resolve(_data.permissions);
else data.permissions = role.permissions;
return this.rest.makeRequest('patch', Endpoints.Guild(role.guild).Role(role.id), true, data, undefined, reason)
@@ -696,6 +728,11 @@ class RESTMethods {
});
}
getGuildVanityCode(guild) {
return this.rest.makeRequest('get', Endpoints.Guild(guild).vanityURL, true)
.then(res => res.code);
}
pruneGuildMembers(guild, days, dry, reason) {
return this.rest.makeRequest(dry ?
'get' :
@@ -721,7 +758,7 @@ class RESTMethods {
deleteEmoji(emoji, reason) {
return this.rest.makeRequest('delete', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, undefined, reason)
.then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data);
.then(() => this.client.actions.GuildEmojiDelete.handle(emoji).emoji);
}
getGuildAuditLogs(guild, options = {}) {
@@ -768,13 +805,23 @@ class RESTMethods {
.then(data => new Webhook(this.client, data));
}
editWebhook(webhook, name, avatar) {
return this.rest.makeRequest('patch', Endpoints.Webhook(webhook.id, webhook.token), false, {
name,
avatar,
}).then(data => {
editWebhook(webhook, options, reason) {
let endpoint;
let auth;
// Changing the channel of a webhook or specifying a reason requires a bot token
if (options.channel_id || reason) {
endpoint = Endpoints.Webhook(webhook.id);
auth = true;
} else {
endpoint = Endpoints.Webhook(webhook.id, webhook.token);
auth = false;
}
return this.rest.makeRequest('patch', endpoint, auth, options, undefined, reason).then(data => {
webhook.name = data.name;
webhook.avatar = data.avatar;
webhook.channelID = data.channel_id;
return webhook;
});
}
@@ -806,7 +853,10 @@ class RESTMethods {
content,
tts,
embeds,
}, files).then(resolve, reject);
}, files).then(data => {
if (!this.client.channels) resolve(data);
else resolve(this.client.actions.MessageCreate.handle(data).message);
}, reject);
}
});
}
@@ -854,21 +904,11 @@ class RESTMethods {
.then(() => user);
}
updateChannelPositions(guildID, channels) {
const data = new Array(channels.length);
for (let i = 0; i < channels.length; i++) {
data[i] = {
id: this.client.resolver.resolveChannelID(channels[i].channel),
position: channels[i].position,
};
}
return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, data).then(() =>
this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: guildID,
channels,
}).guild
);
updateEmbed(guildID, embed, reason) {
return this.rest.makeRequest('patch', Endpoints.Guild(guildID).embed, true, {
enabled: embed.enabled,
channel_id: this.client.resolver.resolveChannelID(embed.channel),
}, undefined, reason);
}
setRolePositions(guildID, roles) {
@@ -909,6 +949,17 @@ class RESTMethods {
);
}
removeMessageReactionEmoji(message, emoji) {
const endpoint = Endpoints.Message(message).Reaction(emoji);
return this.rest.makeRequest('delete', endpoint, true).then(() =>
this.client.actions.MessageReactionRemoveEmoji.handle({
message_id: message.id,
emoji: Util.parseEmoji(emoji),
channel_id: message.channel.id,
}).reaction
);
}
removeMessageReactions(message) {
return this.rest.makeRequest('delete', Endpoints.Message(message).reactions, true)
.then(() => message);
@@ -962,6 +1013,55 @@ class RESTMethods {
patchClientUserGuildSettings(guildID, data) {
return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').Guild(guildID).settings, true, data);
}
getIntegrations(guild) {
return this.rest.makeRequest(
'get',
Constants.Endpoints.Guild(guild.id).integrations,
true
);
}
createIntegration(guild, data, reason) {
return this.rest.makeRequest(
'post',
Constants.Endpoints.Guild(guild.id).integrations,
true,
data,
undefined,
reason
);
}
syncIntegration(integration) {
return this.rest.makeRequest(
'post',
Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id),
true
);
}
editIntegration(integration, data, reason) {
return this.rest.makeRequest(
'patch',
Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id),
true,
data,
undefined,
reason
);
}
deleteIntegration(integration, reason) {
return this.rest.makeRequest(
'delete',
Constants.Endpoints.Guild(integration.guild.id).Integration(integration.id),
true,
undefined,
undefined,
reason
);
}
}
module.exports = RESTMethods;

View File

@@ -1,5 +1,6 @@
const RequestHandler = require('./RequestHandler');
const DiscordAPIError = require('../DiscordAPIError');
const { Events: { RATE_LIMIT } } = require('../../../util/Constants');
class BurstRequestHandler extends RequestHandler {
constructor(restManager, endpoint) {
@@ -41,16 +42,33 @@ class BurstRequestHandler extends RequestHandler {
this.resetTimeout = null;
}, Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
} else if (err.status >= 500 && err.status < 600) {
this.queue.unshift(item);
this.resetTimeout = this.client.setTimeout(() => {
if (item.retries === this.client.options.retryLimit) {
item.reject(err);
this.handle();
this.resetTimeout = null;
}, 1e3 + this.client.options.restTimeOffset);
} else {
item.retries++;
this.queue.unshift(item);
this.resetTimeout = this.client.setTimeout(() => {
this.handle();
this.resetTimeout = null;
}, 1e3 + this.client.options.restTimeOffset);
}
} else {
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
item.reject(err.status >= 400 && err.status < 500 ?
new DiscordAPIError(res.request.path, res.body, res.request.method) : err);
this.handle();
}
} else {
if (this.remaining === 0) {
if (this.client.listenerCount(RATE_LIMIT)) {
this.client.emit(RATE_LIMIT, {
limit: this.limit,
timeDifference: this.timeDifference,
path: item.request.path,
method: item.request.method,
});
}
}
this.globalLimit = false;
const data = res && res.body ? res.body : {};
item.resolve(data);
@@ -61,7 +79,8 @@ class BurstRequestHandler extends RequestHandler {
handle() {
super.handle();
if (this.remaining <= 0 || this.queue.length === 0 || this.globalLimit) return;
if (this.queue.length === 0) return;
if ((this.remaining <= 0 || this.globalLimit) && Date.now() - this.timeDifference < this.resetTime) return;
this.execute(this.queue.shift());
this.remaining--;
this.handle();

View File

@@ -1,5 +1,6 @@
const RequestHandler = require('./RequestHandler');
const DiscordAPIError = require('../DiscordAPIError');
const { Events: { RATE_LIMIT } } = require('../../../util/Constants');
/**
* Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto
@@ -16,6 +17,12 @@ class SequentialRequestHandler extends RequestHandler {
constructor(restManager, endpoint) {
super(restManager, endpoint);
/**
* The client that instantiated this handler
* @type {Client}
*/
this.client = restManager.client;
/**
* The endpoint that this handler is handling
* @type {string}
@@ -59,16 +66,23 @@ class SequentialRequestHandler extends RequestHandler {
if (err) {
if (err.status === 429) {
this.queue.unshift(item);
this.restManager.client.setTimeout(() => {
this.client.setTimeout(() => {
this.globalLimit = false;
resolve();
}, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
}, Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
if (res.headers['x-ratelimit-global']) this.globalLimit = true;
} else if (err.status >= 500 && err.status < 600) {
this.queue.unshift(item);
this.restManager.client.setTimeout(resolve, 1e3 + this.restManager.client.options.restTimeOffset);
if (item.retries === this.client.options.retryLimit) {
item.reject(err);
resolve();
} else {
item.retries++;
this.queue.unshift(item);
this.client.setTimeout(resolve, 1e3 + this.client.options.restTimeOffset);
}
} else {
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
item.reject(err.status >= 400 && err.status < 500 ?
new DiscordAPIError(res.request.path, res.body, res.request.method) : err);
resolve(err);
}
} else {
@@ -76,9 +90,26 @@ class SequentialRequestHandler extends RequestHandler {
const data = res && res.body ? res.body : {};
item.resolve(data);
if (this.requestRemaining === 0) {
this.restManager.client.setTimeout(
if (this.client.listenerCount(RATE_LIMIT)) {
/**
* Emitted when the client hits a rate limit while making a request
* @event Client#rateLimit
* @param {Object} rateLimitInfo Object containing the rate limit info
* @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint
* @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers
* @param {string} rateLimitInfo.path Path used for request that triggered this event
* @param {string} rateLimitInfo.method HTTP method used for request that triggered this event
*/
this.client.emit(RATE_LIMIT, {
limit: this.requestLimit,
timeDifference: this.timeDifference,
path: item.request.path,
method: item.request.method,
});
}
this.client.setTimeout(
() => resolve(data),
this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset
this.requestResetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset
);
} else {
resolve(data);

View File

@@ -30,10 +30,15 @@ class ClientVoiceManager {
onVoiceStateUpdate({ guild_id, session_id, channel_id }) {
const connection = this.connections.get(guild_id);
if (connection) {
connection.channel = this.client.channels.get(channel_id);
connection.setSessionID(session_id);
if (!connection) return;
if (!channel_id) {
connection._disconnect();
this.connections.delete(guild_id);
return;
}
connection.channel = this.client.channels.get(channel_id);
connection.setSessionID(session_id);
}
/**

View File

@@ -4,9 +4,14 @@ const Util = require('../../util/Util');
const Constants = require('../../util/Constants');
const AudioPlayer = require('./player/AudioPlayer');
const VoiceReceiver = require('./receiver/VoiceReceiver');
const SingleSilence = require('./util/SingleSilence');
const EventEmitter = require('events').EventEmitter;
const Prism = require('prism-media');
// The delay between packets when a user is considered to have stopped speaking
// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
const DISCORD_SPEAKING_DELAY = 250;
/**
* Represents a connection to a guild's voice server.
* ```js
@@ -53,7 +58,7 @@ class VoiceConnection extends EventEmitter {
/**
* The current status of the voice connection
* @type {number}
* @type {VoiceStatus}
*/
this.status = Constants.VoiceStatus.AUTHENTICATING;
@@ -101,12 +106,19 @@ class VoiceConnection extends EventEmitter {
});
/**
* Map SSRC to speaking values
* @type {Map<number, boolean>}
* Map SSRC to user id
* @type {Map<number, Snowflake>}
* @private
*/
this.ssrcMap = new Map();
/**
* Map user id to speaking timeout
* @type {Map<Snowflake, Timeout>}
* @private
*/
this.speakingTimeouts = new Map();
/**
* Object that wraps contains the `ws` and `udp` sockets of this voice connection
* @type {Object}
@@ -229,7 +241,7 @@ class VoiceConnection extends EventEmitter {
const { token, endpoint, sessionID } = this.authentication;
if (token && endpoint && sessionID) {
clearTimeout(this.connectTimeout);
this.client.clearTimeout(this.connectTimeout);
this.status = Constants.VoiceStatus.CONNECTING;
/**
* Emitted when we successfully initiate a voice connection.
@@ -246,7 +258,7 @@ class VoiceConnection extends EventEmitter {
* @private
*/
authenticateFailed(reason) {
clearTimeout(this.connectTimeout);
this.client.clearTimeout(this.connectTimeout);
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
/**
* Emitted when we fail to initiate a voice connection.
@@ -255,6 +267,11 @@ class VoiceConnection extends EventEmitter {
*/
this.emit('failed', new Error(reason));
} else {
/**
* Emitted whenever the connection encounters an error.
* @event VoiceConnection#error
* @param {Error} error The encountered error
*/
this.emit('error', new Error(reason));
}
this.status = Constants.VoiceStatus.DISCONNECTED;
@@ -307,6 +324,15 @@ class VoiceConnection extends EventEmitter {
this.sendVoiceStateUpdate({
channel_id: null,
});
this._disconnect();
}
/**
* Internally disconnects (doesn't send disconnect packet).
* @private
*/
_disconnect() {
this.player.destroy();
this.cleanup();
this.status = Constants.VoiceStatus.DISCONNECTED;
@@ -328,7 +354,8 @@ class VoiceConnection extends EventEmitter {
ws.removeAllListeners('error');
ws.removeAllListeners('ready');
ws.removeAllListeners('sessionDescription');
ws.removeAllListeners('speaking');
ws.removeAllListeners('startSpeaking');
ws.shutdown();
}
if (udp) udp.removeAllListeners('error');
@@ -359,7 +386,7 @@ class VoiceConnection extends EventEmitter {
udp.on('error', err => this.emit('error', err));
ws.on('ready', this.onReady.bind(this));
ws.on('sessionDescription', this.onSessionDescription.bind(this));
ws.on('speaking', this.onSpeaking.bind(this));
ws.on('startSpeaking', this.onStartSpeaking.bind(this));
}
/**
@@ -367,20 +394,11 @@ class VoiceConnection extends EventEmitter {
* @param {Object} data The received data
* @private
*/
onReady({ port, ssrc }) {
onReady({ port, ssrc, ip }) {
this.authentication.port = port;
this.authentication.ssrc = ssrc;
const udp = this.sockets.udp;
/**
* Emitted whenever the connection encounters an error.
* @event VoiceConnection#error
* @param {Error} error The encountered error
*/
udp.findEndpointAddress()
.then(address => {
udp.createUDPSocket(address);
}, e => this.emit('error', e));
this.sockets.udp.createUDPSocket(ip);
this.sockets.udp.socket.on('message', this.onUDPMessage.bind(this));
}
/**
@@ -394,12 +412,29 @@ class VoiceConnection extends EventEmitter {
this.authentication.secretKey = secret;
this.status = Constants.VoiceStatus.CONNECTED;
/**
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
* the connection will already be ready.
* @event VoiceConnection#ready
*/
this.emit('ready');
const ready = () => {
/**
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
* the connection will already be ready.
* @event VoiceConnection#ready
*/
this.emit('ready');
};
if (this.dispatcher) {
ready();
} else {
// This serves to provide support for voice receive, sending audio is required to receive it.
this.playOpusStream(new SingleSilence()).once('end', ready);
}
}
/**
* Invoked whenever a user initially starts speaking.
* @param {Object} data The speaking data
* @private
*/
onStartSpeaking({ user_id, ssrc }) {
this.ssrcMap.set(+ssrc, user_id);
}
/**
@@ -407,10 +442,9 @@ class VoiceConnection extends EventEmitter {
* @param {Object} data The received data
* @private
*/
onSpeaking({ user_id, ssrc, speaking }) {
onSpeaking({ user_id, speaking }) {
const guild = this.channel.guild;
const user = this.client.users.get(user_id);
this.ssrcMap.set(+ssrc, user);
if (!speaking) {
for (const receiver of this.receivers) {
receiver.stoppedSpeaking(user);
@@ -426,6 +460,35 @@ class VoiceConnection extends EventEmitter {
guild._memberSpeakUpdate(user_id, speaking);
}
/**
* Handles synthesizing of the speaking event.
* @param {Buffer} buffer Received packet from the UDP socket
* @private
*/
onUDPMessage(buffer) {
const ssrc = +buffer.readUInt32BE(8).toString(10);
const user = this.client.users.get(this.ssrcMap.get(ssrc));
if (!user) return;
let speakingTimeout = this.speakingTimeouts.get(ssrc);
if (typeof speakingTimeout === 'undefined') {
this.onSpeaking({ user_id: user.id, ssrc, speaking: true });
} else {
this.client.clearTimeout(speakingTimeout);
}
speakingTimeout = this.client.setTimeout(() => {
try {
this.onSpeaking({ user_id: user.id, ssrc, speaking: false });
this.client.clearTimeout(speakingTimeout);
this.speakingTimeouts.delete(ssrc);
} catch (ex) {
// Connection already closed, ignore
}
}, DISCORD_SPEAKING_DELAY);
this.speakingTimeouts.set(ssrc, speakingTimeout);
}
/**
* Options that can be passed to stream-playing methods:
* @typedef {Object} StreamOptions

View File

@@ -1,5 +1,4 @@
const udp = require('dgram');
const dns = require('dns');
const Constants = require('../../util/Constants');
const EventEmitter = require('events').EventEmitter;
@@ -65,23 +64,6 @@ class VoiceConnectionUDPClient extends EventEmitter {
return this.voiceConnection.authentication.port;
}
/**
* Tries to resolve the voice server endpoint to an address.
* @returns {Promise<string>}
*/
findEndpointAddress() {
return new Promise((resolve, reject) => {
dns.lookup(this.voiceConnection.authentication.endpoint, (error, address) => {
if (error) {
reject(error);
return;
}
this.discordAddress = address;
resolve(address);
});
});
}
/**
* Send a packet to the UDP client.
* @param {Object} packet The packet to send

View File

@@ -4,7 +4,7 @@ const EventEmitter = require('events').EventEmitter;
let WebSocket;
try {
WebSocket = require('uws');
WebSocket = require('@discordjs/uws');
} catch (err) {
WebSocket = require('ws');
}
@@ -184,9 +184,9 @@ class VoiceWebSocket extends EventEmitter {
/**
* Emitted whenever a speaking packet is received.
* @param {Object} data
* @event VoiceWebSocket#speaking
* @event VoiceWebSocket#startSpeaking
*/
this.emit('speaking', packet.d);
this.emit('startSpeaking', packet.d);
break;
default:
/**

View File

@@ -143,7 +143,7 @@ class StreamDispatcher extends VolumeInterface {
* @param {string} info The debug info
*/
this.setSpeaking(true);
while (repeats--) {
while (repeats-- && this.player.voiceConnection.sockets.udp) {
this.player.voiceConnection.sockets.udp.send(packet)
.catch(e => {
this.setSpeaking(false);
@@ -163,7 +163,7 @@ class StreamDispatcher extends VolumeInterface {
packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4);
packetBuffer.copy(nonce, 0, 0, 12);
buffer = secretbox.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key);
buffer = secretbox.methods.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key);
for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
return packetBuffer;
@@ -171,7 +171,7 @@ class StreamDispatcher extends VolumeInterface {
processPacket(packet) {
try {
if (this.destroyed) {
if (this.destroyed || !this.player.voiceConnection.authentication.secretKey) {
this.setSpeaking(false);
return;
}
@@ -284,7 +284,7 @@ class StreamDispatcher extends VolumeInterface {
const data = this.streamingData;
data.count++;
data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0;
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
data.timestamp = (data.timestamp + 960) < 4294967295 ? data.timestamp + 960 : 0;
}
destroy(type, reason) {

View File

@@ -0,0 +1,34 @@
const OpusEngine = require('./BaseOpusEngine');
class DiscordJsOpusEngine extends OpusEngine {
constructor(player) {
super(player);
const opus = require('@discordjs/opus');
this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
super.init();
}
setBitrate(bitrate) {
this.encoder.setBitrate(Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) {
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
}
setPLP(percent) {
this.encoder.applyEncoderCTL(this.ctl.PLP, Math.min(100, Math.max(0, percent * 100)));
}
encode(buffer) {
super.encode(buffer);
return this.encoder.encode(buffer, 1920);
}
decode(buffer) {
super.decode(buffer);
return this.encoder.decode(buffer, 1920);
}
}
module.exports = DiscordJsOpusEngine;

View File

@@ -1,15 +1,9 @@
const OpusEngine = require('./BaseOpusEngine');
let opus;
class NodeOpusEngine extends OpusEngine {
constructor(player) {
super(player);
try {
opus = require('node-opus');
} catch (err) {
throw err;
}
const opus = require('node-opus');
this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
super.init();
}

View File

@@ -1,4 +1,5 @@
const list = [
require('./DiscordJsOpusEngine'),
require('./NodeOpusEngine'),
require('./OpusScriptEngine'),
];
@@ -24,5 +25,5 @@ exports.fetch = engineOptions => {
if (fetched) return fetched;
}
throw new Error('OPUS_ENGINE_MISSING');
throw new Error('Couldn\'t find an Opus engine.');
};

View File

@@ -1,15 +1,9 @@
const OpusEngine = require('./BaseOpusEngine');
let OpusScript;
class OpusScriptEngine extends OpusEngine {
constructor(player) {
super(player);
try {
OpusScript = require('opusscript');
} catch (err) {
throw err;
}
const OpusScript = require('opusscript');
this.encoder = new OpusScript(this.samplingRate, this.channels);
super.init();
}

View File

@@ -156,8 +156,8 @@ class AudioPlayer extends EventEmitter {
return dispatcher;
}
createDispatcher(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
const options = { seek, volume, passes };
createDispatcher(stream, { seek = 0, volume = 1, passes = 1, opus } = {}) {
const options = { seek, volume, passes, opus };
const dispatcher = new StreamDispatcher(this, stream, options);
dispatcher.on('end', () => this.destroyCurrentStream());

View File

@@ -43,7 +43,7 @@ class VoiceReceiver extends EventEmitter {
this._listener = msg => {
const ssrc = +msg.readUInt32BE(8).toString(10);
const user = this.voiceConnection.ssrcMap.get(ssrc);
const user = connection.client.users.get(connection.ssrcMap.get(ssrc));
if (!user) {
if (!this.queues.has(ssrc)) this.queues.set(ssrc, []);
this.queues.get(ssrc).push(msg);
@@ -112,6 +112,7 @@ class VoiceReceiver extends EventEmitter {
}
if (opusEncoder) {
opusEncoder.destroy();
this.opusEncoders.delete(user.id);
}
}
@@ -132,7 +133,7 @@ class VoiceReceiver extends EventEmitter {
/**
* Creates a readable stream for a user that provides PCM data while the user is speaking. When the user
* stops speaking, the stream is destroyed. The stream is 32-bit signed stereo PCM at 48KHz.
* stops speaking, the stream is destroyed. The stream is 16-bit signed stereo PCM at 48KHz.
* @param {UserResolvable} user The user to create the stream for
* @returns {ReadableStream}
*/
@@ -147,7 +148,7 @@ class VoiceReceiver extends EventEmitter {
handlePacket(msg, user) {
msg.copy(nonce, 0, 0, 12);
let data = secretbox.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key);
let data = secretbox.methods.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key);
if (!data) {
/**
* Emitted whenever a voice packet experiences a problem.
@@ -174,9 +175,9 @@ class VoiceReceiver extends EventEmitter {
}
offset += 1 + (0b1111 & (byte >> 4));
}
while (data[offset] === 0) {
offset++;
}
// Skip over undocumented Discord byte
offset++;
data = data.slice(offset);
}

View File

@@ -9,7 +9,7 @@ class SecretKey {
* @type {Uint8Array}
*/
this.key = new Uint8Array(new ArrayBuffer(key.length));
for (const index in key) this.key[index] = key[index];
for (const index of Object.keys(key)) this.key[index] = key[index];
}
}

View File

@@ -13,10 +13,21 @@ const libs = {
}),
};
exports.methods = {};
for (const libName of Object.keys(libs)) {
try {
const lib = require(libName);
module.exports = libs[libName](lib);
if (libName === 'libsodium-wrappers' && lib.ready) {
lib.ready.then(() => {
exports.methods = libs[libName](lib);
}).catch(() => {
const tweetnacl = require('tweetnacl');
exports.methods = libs.tweetnacl(tweetnacl);
}).catch(() => undefined);
} else {
exports.methods = libs[libName](lib);
}
break;
} catch (err) {} // eslint-disable-line no-empty
}

View File

@@ -0,0 +1,16 @@
const { Readable } = require('stream');
const SILENCE_FRAME = Buffer.from([0xF8, 0xFF, 0xFE]);
/**
* A readable emitting silent opus frames.
* @extends {Readable}
* @private
*/
class Silence extends Readable {
_read() {
this.push(SILENCE_FRAME);
}
}
module.exports = Silence;

View File

@@ -0,0 +1,17 @@
const Silence = require('./Silence');
/**
* Only emits a single silent opus frame.
* This is used as a workaround for Discord now requiring
* silence to be sent before being able to receive audio.
* @extends {Silence}
* @private
*/
class SingleSilence extends Silence {
_read() {
super._read();
this.push(null);
}
}
module.exports = SingleSilence;

View File

@@ -5,9 +5,9 @@ const EventEmitter = require('events');
* @extends {EventEmitter}
*/
class VolumeInterface extends EventEmitter {
constructor({ volume = 0 } = {}) {
constructor({ volume = 1 } = {}) {
super();
this.setVolume(volume || 1);
this.setVolume(volume);
}
/**
@@ -41,7 +41,7 @@ class VolumeInterface extends EventEmitter {
volume = volume || this._volume;
if (volume === 1) return buffer;
const out = new Buffer(buffer.length);
const out = Buffer.alloc(buffer.length);
for (let i = 0; i < buffer.length; i += 2) {
if (i >= buffer.length - 1) break;
const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i))));

View File

@@ -16,7 +16,10 @@ const erlpack = (function findErlpack() {
const WebSocket = (function findWebSocket() {
if (browser) return window.WebSocket; // eslint-disable-line no-undef
try {
return require('uws');
const uws = require('@discordjs/uws');
process.emitWarning('uws support is being removed in the next version of discord.js',
'DeprecationWarning', findWebSocket);
return uws;
} catch (e) {
return require('ws');
}
@@ -59,7 +62,7 @@ class WebSocketConnection extends EventEmitter {
/**
* The current status of the client
* @type {number}
* @type {Status}
*/
this.status = Constants.Status.IDLE;
@@ -82,7 +85,9 @@ class WebSocketConnection extends EventEmitter {
this.ratelimit = {
queue: [],
remaining: 120,
resetTime: -1,
total: 120,
time: 60e3,
resetTimer: null,
};
this.connect(gateway);
@@ -189,11 +194,11 @@ class WebSocketConnection extends EventEmitter {
processQueue() {
if (this.ratelimit.remaining === 0) return;
if (this.ratelimit.queue.length === 0) return;
if (this.ratelimit.remaining === 120) {
this.ratelimit.resetTimer = setTimeout(() => {
this.ratelimit.remaining = 120;
if (this.ratelimit.remaining === this.ratelimit.total) {
this.ratelimit.resetTimer = this.client.setTimeout(() => {
this.ratelimit.remaining = this.ratelimit.total;
this.processQueue();
}, 120e3); // eslint-disable-line
}, this.ratelimit.time);
}
while (this.ratelimit.remaining > 0) {
const item = this.ratelimit.queue.shift();
@@ -210,7 +215,7 @@ class WebSocketConnection extends EventEmitter {
*/
_send(data) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.debug(`Tried to send packet ${data} but no WebSocket is available!`);
this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`);
return;
}
this.ws.send(this.pack(data));
@@ -223,7 +228,7 @@ class WebSocketConnection extends EventEmitter {
*/
send(data) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.debug(`Tried to send packet ${data} but no WebSocket is available!`);
this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`);
return;
}
this.ratelimit.queue.push(data);
@@ -275,6 +280,7 @@ class WebSocketConnection extends EventEmitter {
this.packetManager.handleQueue();
this.ws = null;
this.status = Constants.Status.DISCONNECTED;
this.ratelimit.remaining = this.ratelimit.total;
return true;
}

View File

@@ -31,6 +31,9 @@ class WebSocketPacketManager {
this.register(Constants.WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate'));
this.register(Constants.WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate'));
this.register(Constants.WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk'));
this.register(Constants.WSEvents.GUILD_INTEGRATIONS_UPDATE, require('./handlers/GuildIntegrationsUpdate'));
this.register(Constants.WSEvents.INVITE_CREATE, require('./handlers/InviteCreate'));
this.register(Constants.WSEvents.INVITE_DELETE, require('./handlers/InviteDelete'));
this.register(Constants.WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate'));
this.register(Constants.WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete'));
this.register(Constants.WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate'));
@@ -52,7 +55,9 @@ class WebSocketPacketManager {
this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, require('./handlers/RelationshipRemove'));
this.register(Constants.WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd'));
this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove'));
this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE_EMOJI, require('./handlers/MessageReactionRemoveEmoji'));
this.register(Constants.WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll'));
this.register(Constants.WSEvents.WEBHOOKS_UPDATE, require('./handlers/WebhooksUpdate'));
}
get client() {

View File

@@ -16,16 +16,22 @@ class ChannelPinsUpdate extends AbstractHandler {
const data = packet.d;
const channel = client.channels.get(data.channel_id);
const time = new Date(data.last_pin_timestamp);
if (channel && time) client.emit(Constants.Events.CHANNEL_PINS_UPDATE, channel, time);
if (channel && time) {
// Discord sends null for last_pin_timestamp if the last pinned message was removed
channel.lastPinTimestamp = time.getTime() || null;
client.emit(Constants.Events.CHANNEL_PINS_UPDATE, channel, time);
}
}
}
/**
* Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, not much information
* can be provided easily here - you need to manually check the pins yourself.
* <warn>The `time` parameter will be a Unix Epoch Date object when there are no pins left.</warn>
* @event Client#channelPinsUpdate
* @param {Channel} channel The channel that the pins update occured in
* @param {Date} time The time of the pins update
* @param {Date} time The time when the last pinned message was pinned
*/
module.exports = ChannelPinsUpdate;

View File

@@ -0,0 +1,19 @@
const AbstractHandler = require('./AbstractHandler');
const { Events } = require('../../../../util/Constants');
class GuildIntegrationsHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.guild_id);
if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild);
}
}
module.exports = GuildIntegrationsHandler;
/**
* Emitted whenever a guild integration is updated
* @event Client#guildIntegrationsUpdate
* @param {Guild} guild The guild whose integrations were updated
*/

View File

@@ -0,0 +1,11 @@
const AbstractHandler = require('./AbstractHandler');
class InviteCreateHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
client.actions.InviteCreate.handle(data);
}
}
module.exports = InviteCreateHandler;

View File

@@ -0,0 +1,11 @@
const AbstractHandler = require('./AbstractHandler');
class InviteDeleteHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
client.actions.InviteDelete.handle(data);
}
}
module.exports = InviteDeleteHandler;

View File

@@ -0,0 +1,11 @@
const AbstractHandler = require('./AbstractHandler');
class MessageReactionRemoveEmoji extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
client.actions.MessageReactionRemoveEmoji.handle(data);
}
}
module.exports = MessageReactionRemoveEmoji;

View File

@@ -17,7 +17,7 @@ class ReadyHandler extends AbstractHandler {
client.readyAt = new Date();
client.users.set(clientUser.id, clientUser);
for (const guild of data.guilds) client.dataManager.newGuild(guild);
for (const guild of data.guilds) if (!client.guilds.has(guild.id)) client.dataManager.newGuild(guild);
for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM);
for (const relation of data.relationships) {
@@ -36,7 +36,7 @@ class ReadyHandler extends AbstractHandler {
}
if (data.notes) {
for (const user in data.notes) {
for (const user of Object.keys(data.notes)) {
let note = data.notes[user];
if (!note.length) note = null;
@@ -63,19 +63,20 @@ class ReadyHandler extends AbstractHandler {
client.ws.connection.triggerReady();
}, 1200 * data.guilds.length);
client.setMaxListeners(data.guilds.length + 10);
const guildCount = data.guilds.length;
if (client.getMaxListeners() !== 0) client.setMaxListeners(client.getMaxListeners() + guildCount);
client.once('ready', () => {
client.syncGuilds();
client.setMaxListeners(10);
if (client.getMaxListeners() !== 0) client.setMaxListeners(client.getMaxListeners() - guildCount);
client.clearTimeout(t);
});
const ws = this.packetManager.ws;
ws.sessionID = data.session_id;
ws._trace = data._trace;
client.emit('debug', `READY ${ws._trace.join(' -> ')} ${ws.sessionID}`);
client.emit('debug', `READY ${ws.sessionID}`);
ws.checkIfReady();
}
}

View File

@@ -2,18 +2,16 @@ const AbstractHandler = require('./AbstractHandler');
const Constants = require('../../../../util/Constants');
class ResumedHandler extends AbstractHandler {
handle(packet) {
handle() {
const client = this.packetManager.client;
const ws = client.ws.connection;
ws._trace = packet.d._trace;
ws.status = Constants.Status.READY;
this.packetManager.handleQueue();
const replayed = ws.sequence - ws.closeSequence;
ws.debug(`RESUMED ${ws._trace.join(' -> ')} | replayed ${replayed} events.`);
ws.debug(`RESUMED | replayed ${replayed} events.`);
client.emit(Constants.Events.RESUME, replayed);
ws.heartbeat();
}

View File

@@ -20,7 +20,7 @@ class VoiceStateUpdateHandler extends AbstractHandler {
// If the member left the voice channel, unset their speaking property
if (!data.channel_id) member.speaking = null;
if (member.user.id === client.user.id && data.channel_id) {
if (member.user.id === client.user.id) {
client.emit('self.voiceStateUpdate', data);
}
@@ -34,6 +34,7 @@ class VoiceStateUpdateHandler extends AbstractHandler {
member.serverDeaf = data.deaf;
member.selfMute = data.self_mute;
member.selfDeaf = data.self_deaf;
member.selfStream = data.self_stream || false;
member.voiceSessionID = data.session_id;
member.voiceChannelID = data.channel_id;
client.emit(Constants.Events.VOICE_STATE_UPDATE, oldVoiceChannelMember, member);

View File

@@ -0,0 +1,19 @@
const AbstractHandler = require('./AbstractHandler');
const { Events } = require('../../../../util/Constants');
class WebhooksUpdate extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
const channel = client.channels.get(data.channel_id);
if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel);
}
}
/**
* Emitted whenever a guild text channel has its webhooks changed.
* @event Client#webhookUpdate
* @param {TextChannel} channel The channel that had a webhook update
*/
module.exports = WebhooksUpdate;

View File

@@ -9,13 +9,16 @@ module.exports = {
WebhookClient: require('./client/WebhookClient'),
// Utilities
BitField: require('./util/BitField'),
Collection: require('./util/Collection'),
Constants: require('./util/Constants'),
DiscordAPIError: require('./client/rest/DiscordAPIError'),
EvaluatedPermissions: require('./util/Permissions'),
MessageFlags: require('./util/MessageFlags'),
Permissions: require('./util/Permissions'),
Snowflake: require('./util/Snowflake'),
SnowflakeUtil: require('./util/Snowflake'),
SystemChannelFlags: require('./util/SystemChannelFlags'),
Util: Util,
util: Util,
version: require('../package').version,
@@ -23,10 +26,12 @@ module.exports = {
// Shortcuts to Util methods
escapeMarkdown: Util.escapeMarkdown,
fetchRecommendedShards: Util.fetchRecommendedShards,
resolveString: Util.resolveString,
splitMessage: Util.splitMessage,
// Structures
Attachment: require('./structures/Attachment'),
CategoryChannel: require('./structures/CategoryChannel'),
Channel: require('./structures/Channel'),
ClientUser: require('./structures/ClientUser'),
ClientUserSettings: require('./structures/ClientUserSettings'),
@@ -39,6 +44,7 @@ module.exports = {
GuildAuditLogs: require('./structures/GuildAuditLogs'),
GuildChannel: require('./structures/GuildChannel'),
GuildMember: require('./structures/GuildMember'),
Integration: require('./structures/Integration'),
Invite: require('./structures/Invite'),
Message: require('./structures/Message'),
MessageAttachment: require('./structures/MessageAttachment'),
@@ -46,6 +52,7 @@ module.exports = {
MessageEmbed: require('./structures/MessageEmbed'),
MessageMentions: require('./structures/MessageMentions'),
MessageReaction: require('./structures/MessageReaction'),
NewsChannel: require('./structures/NewsChannel'),
OAuth2Application: require('./structures/OAuth2Application'),
ClientOAuth2Application: require('./structures/OAuth2Application'),
PartialGuild: require('./structures/PartialGuild'),
@@ -56,6 +63,7 @@ module.exports = {
ReactionCollector: require('./structures/ReactionCollector'),
RichEmbed: require('./structures/RichEmbed'),
Role: require('./structures/Role'),
StoreChannel: require('./structures/StoreChannel'),
TextChannel: require('./structures/TextChannel'),
User: require('./structures/User'),
VoiceChannel: require('./structures/VoiceChannel'),

View File

@@ -1,17 +1,19 @@
const childProcess = require('child_process');
const EventEmitter = require('events');
const path = require('path');
const Util = require('../util/Util');
/**
* Represents a Shard spawned by the ShardingManager.
*/
class Shard {
class Shard extends EventEmitter {
/**
* @param {ShardingManager} manager The sharding manager
* @param {number} id The ID of this shard
* @param {Array} [args=[]] Command line arguments to pass to the script
*/
constructor(manager, id, args = []) {
super();
/**
* Manager that created the shard
* @type {ShardingManager}
@@ -35,19 +37,77 @@ class Shard {
});
/**
* Process of the shard
* @type {ChildProcess}
* Whether the shard's {@link Client} is ready
* @type {boolean}
*/
this.process = childProcess.fork(path.resolve(this.manager.file), args, {
env: this.env,
});
this.process.on('message', this._handleMessage.bind(this));
this.process.once('exit', () => {
if (this.manager.respawn) this.manager.createShard(this.id);
});
this.ready = false;
this._evals = new Map();
this._fetches = new Map();
/**
* Listener function for the {@link ChildProcess}' `exit` event
* @type {Function}
* @private
*/
this._exitListener = this._handleExit.bind(this, undefined);
/**
* Process of the shard
* @type {ChildProcess}
*/
this.process = null;
this.spawn(args);
}
/**
* Forks a child process for the shard.
* <warn>You should not need to call this manually.</warn>
* @param {Array} [args=this.manager.args] Command line arguments to pass to the script
* @param {Array} [execArgv=this.manager.execArgv] Command line arguments to pass to the process executable
* @returns {ChildProcess}
*/
spawn(args = this.manager.args, execArgv = this.manager.execArgv) {
this.process = childProcess.fork(path.resolve(this.manager.file), args, {
env: this.env, execArgv,
})
.on('exit', this._exitListener)
.on('message', this._handleMessage.bind(this));
/**
* Emitted upon the creation of the shard's child process.
* @event Shard#spawn
* @param {ChildProcess} process Child process that was created
*/
this.emit('spawn', this.process);
return new Promise((resolve, reject) => {
this.once('ready', resolve);
this.once('disconnect', () => reject(new Error(`Shard ${this.id}'s Client disconnected before becoming ready.`)));
this.once('death', () => reject(new Error(`Shard ${this.id}'s process exited before its Client became ready.`)));
setTimeout(() => reject(new Error(`Shard ${this.id}'s Client took too long to become ready.`)), 30000);
}).then(() => this.process);
}
/**
* Immediately kills the shard's process and does not restart it.
*/
kill() {
this.process.removeListener('exit', this._exitListener);
this.process.kill();
this._handleExit(false);
}
/**
* Kills and restarts the shard's process.
* @param {number} [delay=500] How long to wait between killing the process and restarting it (in milliseconds)
* @returns {Promise<ChildProcess>}
*/
respawn(delay = 500) {
this.kill();
if (delay > 0) return Util.delayFor(delay).then(() => this.spawn());
return this.spawn();
}
/**
@@ -57,10 +117,9 @@ class Shard {
*/
send(message) {
return new Promise((resolve, reject) => {
const sent = this.process.send(message, err => {
this.process.send(message, err => {
if (err) reject(err); else resolve(this);
});
if (!sent) throw new Error('Failed to send message to shard\'s process.');
});
}
@@ -70,9 +129,7 @@ class Shard {
* @returns {Promise<*>}
* @example
* shard.fetchClientValue('guilds.size')
* .then(count => {
* console.log(`${count} guilds in shard ${shard.id}`);
* })
* .then(count => console.log(`${count} guilds in shard ${shard.id}`))
* .catch(console.error);
*/
fetchClientValue(prop) {
@@ -133,6 +190,39 @@ class Shard {
*/
_handleMessage(message) {
if (message) {
// Shard is ready
if (message._ready) {
this.ready = true;
/**
* Emitted upon the shard's {@link Client#ready} event.
* @event Shard#ready
*/
this.emit('ready');
return;
}
// Shard has disconnected
if (message._disconnect) {
this.ready = false;
/**
* Emitted upon the shard's {@link Client#disconnect} event.
* @event Shard#disconnect
*/
this.emit('disconnect');
return;
}
// Shard is attempting to reconnect
if (message._reconnecting) {
this.ready = false;
/**
* Emitted upon the shard's {@link Client#reconnecting} event.
* @event Shard#reconnecting
*/
this.emit('reconnecting');
return;
}
// Shard is requesting a property fetch
if (message._sFetchProp) {
this.manager.fetchClientValues(message._sFetchProp).then(
@@ -159,6 +249,33 @@ class Shard {
* @param {*} message Message that was received
*/
this.manager.emit('message', this, message);
/**
* Emitted upon recieving a message from the child process.
* @event Shard#message
* @param {*} message Message that was received
*/
this.emit('message', message);
}
/**
* Handles the shard's process exiting.
* @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again
* @private
*/
_handleExit(respawn = this.manager.respawn) {
/**
* Emitted upon the shard's child process exiting.
* @event Shard#death
* @param {ChildProcess} process Child process that exited
*/
this.emit('death', this.process);
this.process = null;
this._evals.clear();
this._fetches.clear();
if (respawn) this.manager.createShard(this.id);
}
}

View File

@@ -10,6 +10,9 @@ class ShardClientUtil {
constructor(client) {
this.client = client;
process.on('message', this._handleMessage.bind(this));
client.on('ready', () => { process.send({ _ready: true }); });
client.on('disconnect', () => { process.send({ _disconnect: true }); });
client.on('reconnecting', () => { process.send({ _reconnecting: true }); });
}
/**
@@ -37,10 +40,9 @@ class ShardClientUtil {
*/
send(message) {
return new Promise((resolve, reject) => {
const sent = process.send(message, err => {
process.send(message, err => {
if (err) reject(err); else resolve();
});
if (!sent) throw new Error('Failed to send message to master process.');
});
}

View File

@@ -66,6 +66,12 @@ class ShardingManager extends EventEmitter {
*/
this.shardArgs = options.shardArgs;
/**
* Arguments for the shard's process executable
* @type {?string[]}
*/
this.execArgv = options.execArgv;
/**
* Token to use for obtaining the automatic shard count, and passing to shards
* @type {?string}
@@ -189,6 +195,26 @@ class ShardingManager extends EventEmitter {
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
return Promise.all(promises);
}
/**
* Kills all running shards and respawns them.
* @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds)
* @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it
* (in milliseconds)
* @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
* @param {number} [currentShardIndex=0] The shard index to start respawning at
* @returns {Promise<Collection<number, Shard>>}
*/
respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true, currentShardIndex = 0) {
let s = 0;
const shard = this.shards.get(currentShardIndex);
const promises = [shard.respawn(respawnDelay, waitForReady)];
if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay));
return Promise.all(promises).then(() => {
if (++currentShardIndex === this.shards.size) return this.shards;
return this.respawnAll(shardDelay, respawnDelay, waitForReady, currentShardIndex);
});
}
}
module.exports = ShardingManager;

View File

@@ -5,6 +5,10 @@ const GuildChannel = require('./GuildChannel');
* @extends {GuildChannel}
*/
class CategoryChannel extends GuildChannel {
constructor(guild, data) {
super(guild, data);
this.type = 'category';
}
/**
* The channels that are part of this category
* @type {?Collection<Snowflake, GuildChannel>}

View File

@@ -19,10 +19,19 @@ class Channel {
* * `group` - a Group DM channel
* * `text` - a guild text channel
* * `voice` - a guild voice channel
* * `category` - a guild category channel
* * `news` - a guild news channel
* * `store` - a guild store channel
* @type {string}
*/
this.type = null;
/**
* Whether the channel has been deleted
* @type {boolean}
*/
this.deleted = false;
if (data) this.setup(data);
}

View File

@@ -21,7 +21,9 @@ class ClientUser extends User {
/**
* The email of this account
* @type {string}
* <warn>This is only filled when using a user account.</warn>
* @type {?string}
* @deprecated
*/
this.email = data.email;
this.localPresence = {};
@@ -31,6 +33,7 @@ class ClientUser extends User {
* A Collection of friends for the logged in user
* <warn>This is only filled when using a user account.</warn>
* @type {Collection<Snowflake, User>}
* @deprecated
*/
this.friends = new Collection();
@@ -38,6 +41,7 @@ class ClientUser extends User {
* A Collection of blocked users for the logged in user
* <warn>This is only filled when using a user account.</warn>
* @type {Collection<Snowflake, User>}
* @deprecated
*/
this.blocked = new Collection();
@@ -45,6 +49,7 @@ class ClientUser extends User {
* A Collection of notes for the logged in user
* <warn>This is only filled when using a user account.</warn>
* @type {Collection<Snowflake, string>}
* @deprecated
*/
this.notes = new Collection();
@@ -52,20 +57,21 @@ class ClientUser extends User {
* If the user has Discord premium (nitro)
* <warn>This is only filled when using a user account.</warn>
* @type {?boolean}
* @deprecated
*/
this.premium = typeof data.premium === 'boolean' ? data.premium : null;
/**
* If the user has MFA enabled on their account
* <warn>This is only filled when using a user account.</warn>
* @type {?boolean}
* @type {boolean}
*/
this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null;
this.mfaEnabled = data.mfa_enabled;
/**
* If the user has ever used a mobile device on Discord
* <warn>This is only filled when using a user account.</warn>
* @type {?boolean}
* @deprecated
*/
this.mobile = typeof data.mobile === 'boolean' ? data.mobile : null;
@@ -73,6 +79,7 @@ class ClientUser extends User {
* Various settings for this user
* <warn>This is only filled when using a user account.</warn>
* @type {?ClientUserSettings}
* @deprecated
*/
this.settings = data.user_settings ? new ClientUserSettings(this, data.user_settings) : null;
@@ -80,6 +87,7 @@ class ClientUser extends User {
* All of the user's guild settings
* <warn>This is only filled when using a user account</warn>
* @type {Collection<Snowflake, ClientUserGuildSettings>}
* @deprecated
*/
this.guildSettings = new Collection();
if (data.user_guild_settings) {
@@ -116,6 +124,7 @@ class ClientUser extends User {
* @param {string} email New email to change to
* @param {string} password Current password
* @returns {Promise<ClientUser>}
* @deprecated
* @example
* // Set email
* client.user.setEmail('bob@gmail.com', 'some amazing password 123')
@@ -132,6 +141,7 @@ class ClientUser extends User {
* @param {string} newPassword New password to change to
* @param {string} oldPassword Current password
* @returns {Promise<ClientUser>}
* @deprecated
* @example
* // Set password
* client.user.setPassword('some new amazing password 456', 'some amazing password 123')
@@ -173,6 +183,11 @@ class ClientUser extends User {
* Sets the full presence of the client user.
* @param {PresenceData} data Data for the presence
* @returns {Promise<ClientUser>}
* @example
* // Set the client user's presence
* client.user.setPresence({ game: { name: 'with discord.js' }, status: 'idle' })
* .then(console.log)
* .catch(console.error);
*/
setPresence(data) {
// {"op":3,"d":{"status":"dnd","since":0,"game":null,"afk":false}}
@@ -240,6 +255,11 @@ class ClientUser extends User {
* Sets the status of the client user.
* @param {PresenceStatus} status Status to change to
* @returns {Promise<ClientUser>}
* @example
* // Set the client user's status
* client.user.setStatus('idle')
* .then(console.log)
* .catch(console.error);
*/
setStatus(status) {
return this.setPresence({ status });
@@ -269,12 +289,16 @@ class ClientUser extends User {
* @param {string} [options.url] Twitch stream URL
* @param {ActivityType|number} [options.type] Type of the activity
* @returns {Promise<Presence>}
* @example
* client.user.setActivity('YouTube', { type: 'WATCHING' })
* .then(presence => console.log(`Activity set to ${presence.game ? presence.game.name : 'none'}`))
* .catch(console.error);
*/
setActivity(name, { url, type } = {}) {
if (!name) return this.setPresence({ activity: null });
if (!name) return this.setPresence({ game: null });
return this.setPresence({
game: { name, type, url },
});
}).then(clientUser => clientUser.presence);
}
/**
@@ -288,12 +312,24 @@ class ClientUser extends User {
/**
* Fetches messages that mentioned the client's user.
* <warn>This is only available when using a user account.</warn>
* @param {Object} [options] Options for the fetch
* @param {number} [options.limit=25] Maximum number of mentions to retrieve
* @param {boolean} [options.roles=true] Whether to include role mentions
* @param {boolean} [options.everyone=true] Whether to include everyone/here mentions
* @param {Guild|Snowflake} [options.guild] Limit the search to a specific guild
* @param {GuildResolvable} [options.guild] Limit the search to a specific guild
* @returns {Promise<Message[]>}
* @deprecated
* @example
* // Fetch mentions
* client.user.fetchMentions()
* .then(console.log)
* .catch(console.error);
* @example
* // Fetch mentions from a guild
* client.user.fetchMentions({ guild: '222078108977594368' })
* .then(console.log)
* .catch(console.error);
*/
fetchMentions(options = {}) {
return this.client.rest.methods.fetchMentions(options);
@@ -304,6 +340,7 @@ class ClientUser extends User {
* <warn>This is only available when using a user account.</warn>
* @param {UserResolvable} user The user to send the friend request to
* @returns {Promise<User>} The user the friend request was sent to
* @deprecated
*/
addFriend(user) {
user = this.client.resolver.resolveUser(user);
@@ -315,6 +352,7 @@ class ClientUser extends User {
* <warn>This is only available when using a user account.</warn>
* @param {UserResolvable} user The user to remove from your friends
* @returns {Promise<User>} The user that was removed
* @deprecated
*/
removeFriend(user) {
user = this.client.resolver.resolveUser(user);
@@ -323,7 +361,7 @@ class ClientUser extends User {
/**
* Creates a guild.
* <warn>This is only available when using a user account.</warn>
* <warn>This is only available to bots in less than 10 guilds and user accounts.</warn>
* @param {string} name The name of the guild
* @param {string} [region] The region for the server
* @param {BufferResolvable|Base64Resolvable} [icon=null] The icon for the guild
@@ -353,12 +391,23 @@ class ClientUser extends User {
* Creates a Group DM.
* @param {GroupDMRecipientOptions[]} recipients The recipients
* @returns {Promise<GroupDMChannel>}
* @example
* // Create a Group DM with a token provided from OAuth
* client.user.createGroupDM([{
* user: '66564597481480192',
* accessToken: token
* }])
* .then(console.log)
* .catch(console.error);
*/
createGroupDM(recipients) {
return this.client.rest.methods.createGroupDM({
recipients: recipients.map(u => this.client.resolver.resolveUserID(u.user)),
accessTokens: recipients.map(u => u.accessToken),
nicks: recipients.map(u => u.nick),
nicks: recipients.reduce((o, r) => {
if (r.nick) o[r.user ? r.user.id : r.id] = r.nick;
return o;
}, {}),
});
}
@@ -367,13 +416,32 @@ class ClientUser extends User {
* <warn>This is only available when using a user account.</warn>
* @param {Invite|string} invite Invite or code to accept
* @returns {Promise<Guild>} Joined guild
* @deprecated
*/
acceptInvite(invite) {
return this.client.rest.methods.acceptInvite(invite);
}
}
ClientUser.prototype.acceptInvite =
util.deprecate(ClientUser.prototype.acceptInvite, 'ClientUser#acceptInvite: userbot methods will be removed');
ClientUser.prototype.setGame =
util.deprecate(ClientUser.prototype.setGame, 'ClientUser#setGame: use ClientUser#setActivity instead');
ClientUser.prototype.addFriend =
util.deprecate(ClientUser.prototype.addFriend, 'ClientUser#addFriend: userbot methods will be removed');
ClientUser.prototype.removeFriend =
util.deprecate(ClientUser.prototype.removeFriend, 'ClientUser#removeFriend: userbot methods will be removed');
ClientUser.prototype.setPassword =
util.deprecate(ClientUser.prototype.setPassword, 'ClientUser#setPassword: userbot methods will be removed');
ClientUser.prototype.setEmail =
util.deprecate(ClientUser.prototype.setEmail, 'ClientUser#setEmail: userbot methods will be removed');
ClientUser.prototype.fetchMentions =
util.deprecate(ClientUser.prototype.fetchMentions, 'ClientUser#fetchMentions: userbot methods will be removed');
module.exports = ClientUser;

View File

@@ -24,7 +24,17 @@ class DMChannel extends Channel {
*/
this.recipient = this.client.dataManager.newUser(data.recipients[0]);
/**
* The ID of the last message in the channel, if one was sent
* @type {?Snowflake}
*/
this.lastMessageID = data.last_message_id;
/**
* The timestamp when the last pinned message was pinned, if there was one
* @type {?number}
*/
this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
}
/**
@@ -38,6 +48,7 @@ class DMChannel extends Channel {
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
get lastPinAt() {}
send() {}
sendMessage() {}
sendEmbed() {}

View File

@@ -1,5 +1,6 @@
const Constants = require('../util/Constants');
const Collection = require('../util/Collection');
const Permissions = require('../util/Permissions');
const Snowflake = require('../util/Snowflake');
/**
@@ -21,6 +22,12 @@ class Emoji {
*/
this.guild = guild;
/**
* Whether this emoji has been deleted
* @type {boolean}
*/
this.deleted = false;
this.setup(data);
}
@@ -55,6 +62,13 @@ class Emoji {
*/
this.animated = data.animated;
/**
* Whether this emoji is available
* @type {boolean}
* @name Emoji#available
*/
if (typeof data.available !== 'undefined') this.available = data.available;
this._roles = data.roles;
}
@@ -76,6 +90,15 @@ class Emoji {
return new Date(this.createdTimestamp);
}
/**
* Whether the emoji is deletable by the client user
* @type {boolean}
* @readonly
*/
get deletable() {
return !this.managed && this.guild.me.hasPermission(Permissions.FLAGS.MANAGE_EMOJIS);
}
/**
* A collection of roles this emoji is active for (empty if all), mapped by role ID
* @type {Collection<Snowflake, Role>}
@@ -140,6 +163,21 @@ class Emoji {
return this.edit({ name }, reason);
}
/**
* Fetches the author for this emoji
* @returns {Promise<User>}
*/
fetchAuthor() {
if (this.managed) return Promise.reject(new Error('Emoji is managed and has no Author.'));
if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) {
return Promise.reject(
new Error(`Client must have Manage Emoji permission in guild ${this.guild} to see emoji authors.`)
);
}
return this.client.rest.makeRequest('get', Constants.Endpoints.Guild(this.guild).Emoji(this.id), true)
.then(emoji => this.client.dataManager.newUser(emoji.user));
}
/**
* Add a role to the list of roles that can use this emoji.
* @param {Role} role The role to add
@@ -184,6 +222,16 @@ class Emoji {
return this.edit({ roles: newRoles });
}
/**
* Deletes the emoji.
* @param {string} [reason] Reason for deleting the emoji
* @returns {Promise<Emoji>}
*/
delete(reason) {
return this.client.rest.methods.deleteEmoji(this, reason);
}
/**
* When concatenated with a string, this automatically returns the emoji mention rather than the object.
* @returns {string}

View File

@@ -94,7 +94,17 @@ class GroupDMChannel extends Channel {
}
}
/**
* The ID of the last message in the channel, if one was sent
* @type {?Snowflake}
*/
this.lastMessageID = data.last_message_id;
/**
* The timestamp when the last pinned message was pinned, if there was one
* @type {?number}
*/
this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null;
}
/**
@@ -183,7 +193,7 @@ class GroupDMChannel extends Channel {
}
/**
* Removes an user from this Group DM.
* Removes a user from this Group DM.
* @param {UserResolvable} user User to remove
* @returns {Promise<GroupDMChannel>}
*/
@@ -208,6 +218,7 @@ class GroupDMChannel extends Channel {
// These are here only for documentation purposes - they are implemented by TextBasedChannel
/* eslint-disable no-empty-function */
get lastPinAt() {}
send() {}
sendMessage() {}
sendEmbed() {}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
const Collection = require('../util/Collection');
const Snowflake = require('../util/Snowflake');
const Webhook = require('./Webhook');
const Integration = require('./Integration');
const Invite = require('./Invite');
/**
* The target type of an entry, e.g. `GUILD`. Here are the available types:
@@ -12,6 +14,7 @@ const Webhook = require('./Webhook');
* * WEBHOOK
* * EMOJI
* * MESSAGE
* * INTEGRATION
* @typedef {string} AuditLogTargetType
*/
@@ -30,6 +33,8 @@ const Targets = {
WEBHOOK: 'WEBHOOK',
EMOJI: 'EMOJI',
MESSAGE: 'MESSAGE',
INTEGRATION: 'INTEGRATION',
UNKNOWN: 'UNKNOWN',
};
/**
@@ -48,6 +53,9 @@ const Targets = {
* * MEMBER_BAN_REMOVE: 23
* * MEMBER_UPDATE: 24
* * MEMBER_ROLE_UPDATE: 25
* * MEMBER_MOVE: 26
* * MEMBER_DISCONNECT: 27
* * BOT_ADD: 28,
* * ROLE_CREATE: 30
* * ROLE_UPDATE: 31
* * ROLE_DELETE: 32
@@ -56,11 +64,17 @@ const Targets = {
* * INVITE_DELETE: 42
* * WEBHOOK_CREATE: 50
* * WEBHOOK_UPDATE: 51
* * WEBHOOK_DELETE: 50
* * WEBHOOK_DELETE: 52
* * EMOJI_CREATE: 60
* * EMOJI_UPDATE: 61
* * EMOJI_DELETE: 62
* * MESSAGE_DELETE: 72
* * MESSAGE_BULK_DELETE: 73
* * MESSAGE_PIN: 74
* * MESSAGE_UNPIN: 75
* * INTEGRATION_CREATE: 80
* * INTEGRATION_UPDATE: 81
* * INTEGRATION_DELETE: 82
* @typedef {?number|string} AuditLogAction
*/
@@ -84,6 +98,9 @@ const Actions = {
MEMBER_BAN_REMOVE: 23,
MEMBER_UPDATE: 24,
MEMBER_ROLE_UPDATE: 25,
MEMBER_MOVE: 26,
MEMBER_DISCONNECT: 27,
BOT_ADD: 28,
ROLE_CREATE: 30,
ROLE_UPDATE: 31,
ROLE_DELETE: 32,
@@ -97,6 +114,12 @@ const Actions = {
EMOJI_UPDATE: 61,
EMOJI_DELETE: 62,
MESSAGE_DELETE: 72,
MESSAGE_BULK_DELETE: 73,
MESSAGE_PIN: 74,
MESSAGE_UNPIN: 75,
INTEGRATION_CREATE: 80,
INTEGRATION_UPDATE: 81,
INTEGRATION_DELETE: 82,
};
@@ -119,6 +142,18 @@ class GuildAuditLogs {
}
}
/**
* Cached integrations
* @type {Collection<Snowflake, Integration>}
* @private
*/
this.integrations = new Collection();
if (data.integrations) {
for (const integration of data.integrations) {
this.integrations.set(integration.id, new Integration(guild.client, integration, guild));
}
}
/**
* The entries for this guild's audit logs
* @type {Collection<Snowflake, GuildAuditLogsEntry>}
@@ -147,8 +182,10 @@ class GuildAuditLogs {
* * An emoji
* * An invite
* * A webhook
* * An integration
* * An object with an id key if target was deleted
* * An object where the keys represent either the new value or the old value
* @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook} AuditLogEntryTarget
* @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook|Integration} AuditLogEntryTarget
*/
/**
@@ -165,6 +202,7 @@ class GuildAuditLogs {
if (target < 60) return Targets.WEBHOOK;
if (target < 70) return Targets.EMOJI;
if (target < 80) return Targets.MESSAGE;
if (target < 90) return Targets.INTEGRATION;
return null;
}
@@ -188,10 +226,13 @@ class GuildAuditLogs {
Actions.CHANNEL_CREATE,
Actions.CHANNEL_OVERWRITE_CREATE,
Actions.MEMBER_BAN_REMOVE,
Actions.BOT_ADD,
Actions.ROLE_CREATE,
Actions.INVITE_CREATE,
Actions.WEBHOOK_CREATE,
Actions.EMOJI_CREATE,
Actions.MESSAGE_PIN,
Actions.INTEGRATION_CREATE,
].includes(action)) return 'CREATE';
if ([
@@ -200,11 +241,15 @@ class GuildAuditLogs {
Actions.MEMBER_KICK,
Actions.MEMBER_PRUNE,
Actions.MEMBER_BAN_ADD,
Actions.MEMBER_DISCONNECT,
Actions.ROLE_DELETE,
Actions.INVITE_DELETE,
Actions.WEBHOOK_DELETE,
Actions.EMOJI_DELETE,
Actions.MESSAGE_DELETE,
Actions.MESSAGE_BULK_DELETE,
Actions.MESSAGE_UNPIN,
Actions.INTEGRATION_DELETE,
].includes(action)) return 'DELETE';
if ([
@@ -213,10 +258,12 @@ class GuildAuditLogs {
Actions.CHANNEL_OVERWRITE_UPDATE,
Actions.MEMBER_UPDATE,
Actions.MEMBER_ROLE_UPDATE,
Actions.MEMBER_MOVE,
Actions.ROLE_UPDATE,
Actions.INVITE_UPDATE,
Actions.WEBHOOK_UPDATE,
Actions.EMOJI_UPDATE,
Actions.INTEGRATION_UPDATE,
].includes(action)) return 'UPDATE';
return 'ALL';
@@ -227,6 +274,7 @@ class GuildAuditLogs {
* Audit logs entry.
*/
class GuildAuditLogsEntry {
// eslint-disable-next-line complexity
constructor(logs, guild, data) {
const targetType = GuildAuditLogs.targetType(data.action_type);
/**
@@ -284,39 +332,74 @@ class GuildAuditLogsEntry {
* @type {?Object|Role|GuildMember}
*/
this.extra = null;
if (data.options) {
if (data.action_type === Actions.MEMBER_PRUNE) {
switch (data.action_type) {
case Actions.MEMBER_PRUNE:
this.extra = {
removed: data.options.members_removed,
days: data.options.delete_member_days,
removed: Number(data.options.members_removed),
days: Number(data.options.delete_member_days),
};
} else if (data.action_type === Actions.MESSAGE_DELETE) {
break;
case Actions.MEMBER_MOVE:
case Actions.MESSAGE_DELETE:
case Actions.MESSAGE_BULK_DELETE:
this.extra = {
count: data.options.count,
channel: guild.channels.get(data.options.channel_id),
channel: guild.channels.get(data.options.channel_id) || { id: data.options.channel_id },
count: Number(data.options.count),
};
} else {
break;
case Actions.MESSAGE_PIN:
case Actions.MESSAGE_UNPIN:
this.extra = {
channel: guild.client.channels.get(data.options.channel_id) || { id: data.options.channel_id },
messageID: data.options.message_id,
};
break;
case Actions.MEMBER_DISCONNECT:
this.extra = {
count: Number(data.options.count),
};
break;
case Actions.CHANNEL_OVERWRITE_CREATE:
case Actions.CHANNEL_OVERWRITE_UPDATE:
case Actions.CHANNEL_OVERWRITE_DELETE:
switch (data.options.type) {
case 'member':
this.extra = guild.members.get(data.options.id);
if (!this.extra) this.extra = { id: data.options.id };
this.extra = guild.members.get(data.options.id) ||
{ id: data.options.id, type: 'member' };
break;
case 'role':
this.extra = guild.roles.get(data.options.id);
if (!this.extra) this.extra = { id: data.options.id, name: data.options.role_name };
this.extra = guild.roles.get(data.options.id) ||
{ id: data.options.id, name: data.options.role_name, type: 'role' };
break;
default:
break;
}
}
break;
default:
break;
}
if ([Targets.USER, Targets.GUILD].includes(targetType)) {
/**
* The target of this entry
* @type {AuditLogEntryTarget}
*/
this.target = guild.client[`${targetType.toLowerCase()}s`].get(data.target_id);
/**
* The target of this entry
* @type {AuditLogEntryTarget}
*/
this.target = null;
if (targetType === Targets.UNKNOWN) {
this.changes.reduce((o, c) => {
o[c.key] = c.new || c.old;
return o;
}, {});
this.target.id = data.target_id;
// MEMBER_DISCONNECT and similar types do not provide a target_id.
} else if (targetType === Targets.USER && data.target_id) {
this.target = guild.client.users.get(data.target_id);
} else if (targetType === Targets.GUILD) {
this.target = guild.client.guilds.get(data.target_id);
} else if (targetType === Targets.WEBHOOK) {
this.target = logs.webhooks.get(data.target_id) ||
new Webhook(guild.client,
@@ -328,16 +411,28 @@ class GuildAuditLogsEntry {
guild_id: guild.id,
}));
} else if (targetType === Targets.INVITE) {
const change = this.changes.find(c => c.key === 'code');
this.target = guild.fetchInvites()
.then(invites => {
this.target = invites.find(i => i.code === (change.new_value || change.old_value));
return this.target;
});
const changes = this.changes.reduce((o, c) => {
o[c.key] = c.new || c.old;
return o;
}, {
id: data.target_id,
guild,
});
changes.channel = { id: changes.channel_id };
this.target = new Invite(guild.client, changes);
} else if (targetType === Targets.MESSAGE) {
this.target = guild.client.users.get(data.target_id);
} else {
this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id);
// Discord sends a channel id for the MESSAGE_BULK_DELETE action type.
this.target = data.action_type === Actions.MESSAGE_BULK_DELETE ?
guild.channels.get(data.target_id) || { id: data.target_id } :
guild.client.users.get(data.target_id);
} else if (targetType === Targets.INTEGRATION) {
this.target = logs.integrations.get(data.target_id) ||
new Integration(guild.client, this.changes.reduce((o, c) => {
o[c.key] = c.new || c.old;
return o;
}, { id: data.target_id }), guild);
} else if (data.target_id) {
this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id) || { id: data.target_id };
}
}

View File

@@ -4,6 +4,8 @@ const PermissionOverwrites = require('./PermissionOverwrites');
const Permissions = require('../util/Permissions');
const Collection = require('../util/Collection');
const Constants = require('../util/Constants');
const Invite = require('./Invite');
const Util = require('../util/Util');
/**
* Represents a guild channel (i.e. text channels and voice channels).
@@ -73,44 +75,79 @@ class GuildChannel extends Channel {
}
/**
* Gets the overall set of permissions for a user in this channel, taking into account roles and permission
* overwrites.
* If the permissionOverwrites match the parent channel, null if no parent
* @type {?boolean}
* @readonly
*/
get permissionsLocked() {
if (!this.parent) return null;
if (this.permissionOverwrites.size !== this.parent.permissionOverwrites.size) return false;
return this.permissionOverwrites.every((value, key) => {
const testVal = this.parent.permissionOverwrites.get(key);
return testVal !== undefined &&
testVal.deny === value.deny &&
testVal.allow === value.allow;
});
}
/**
* Gets the overall set of permissions for a user in this channel, taking into account channel overwrites.
* @param {GuildMemberResolvable} member The user that you want to obtain the overall permissions for
* @returns {?Permissions}
*/
permissionsFor(member) {
memberPermissions(member) {
member = this.client.resolver.resolveGuildMember(this.guild, member);
if (!member) return null;
if (member.id === this.guild.ownerID) return new Permissions(member, Permissions.ALL);
let permissions = 0;
const roles = member.roles;
for (const role of roles.values()) permissions |= role.permissions;
const permissions = new Permissions(roles.map(role => role.permissions));
const admin = Boolean(permissions & Permissions.FLAGS.ADMINISTRATOR);
if (admin) return new Permissions(Permissions.ALL);
if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze();
const overwrites = this.overwritesFor(member, true, roles);
if (overwrites.everyone) {
permissions &= ~overwrites.everyone.deny;
permissions |= overwrites.everyone.allow;
}
return permissions
.remove(overwrites.everyone ? overwrites.everyone.deny : 0)
.add(overwrites.everyone ? overwrites.everyone.allow : 0)
.remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : 0)
.add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : 0)
.remove(overwrites.member ? overwrites.member.deny : 0)
.add(overwrites.member ? overwrites.member.allow : 0)
.freeze();
}
let allow = 0;
for (const overwrite of overwrites.roles) {
permissions &= ~overwrite.deny;
allow |= overwrite.allow;
}
permissions |= allow;
/**
* Gets the overall set of permissions for a role in this channel, taking into account channel overwrites.
* @param {RoleResolvable} role The role that you want to obtain the overall permissions for
* @returns {?Permissions}
*/
rolePermissions(role) {
if (role.permissions & Permissions.FLAGS.ADMINISTRATOR) return new Permissions(Permissions.ALL).freeze();
if (overwrites.member) {
permissions &= ~overwrites.member.deny;
permissions |= overwrites.member.allow;
}
const everyoneOverwrites = this.permissionOverwrites.get(this.guild.id);
const roleOverwrites = this.permissionOverwrites.get(role.id);
return new Permissions(member, permissions);
return new Permissions(role.permissions)
.remove(everyoneOverwrites ? everyoneOverwrites.deny : 0)
.add(everyoneOverwrites ? everyoneOverwrites.allow : 0)
.remove(roleOverwrites ? roleOverwrites.deny : 0)
.add(roleOverwrites ? roleOverwrites.allow : 0)
.freeze();
}
/**
* Get the overall set of permissions for a member or role in this channel, taking into account channel overwrites.
* @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for
* @returns {?Permissions}
*/
permissionsFor(memberOrRole) {
const member = this.guild.member(memberOrRole);
if (member) return this.memberPermissions(member);
const role = this.client.resolver.resolveRole(this.guild, memberOrRole);
if (role) return this.rolePermissions(role);
return null;
}
overwritesFor(member, verified = false, roles = null) {
@@ -140,10 +177,34 @@ class GuildChannel extends Channel {
}
/**
* An object mapping permission flags to `true` (enabled) or `false` (disabled).
* Replaces the permission overwrites for a channel
* @param {Object} [options] Options
* @param {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [options.overwrites]
* Permission overwrites
* @param {string} [options.reason] Reason for updating the channel overwrites
* @returns {Promise<GuildChannel>}
* @example
* channel.replacePermissionOverwrites({
* overwrites: [
* {
* id: message.author.id,
* denied: ['VIEW_CHANNEL'],
* },
* ],
* reason: 'Needed to change permissions'
* });
*/
replacePermissionOverwrites({ overwrites, reason } = {}) {
return this.edit({ permissionOverwrites: overwrites, reason })
.then(() => this);
}
/**
* An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled).
* ```js
* {
* 'SEND_MESSAGES': true,
* 'EMBED_LINKS': null,
* 'ATTACH_FILES': false,
* }
* ```
@@ -161,7 +222,15 @@ class GuildChannel extends Channel {
* message.channel.overwritePermissions(message.author, {
* SEND_MESSAGES: false
* })
* .then(() => console.log('Done!'))
* .then(updated => console.log(updated.permissionOverwrites.get(message.author.id)))
* .catch(console.error);
* @example
* // Overwite permissions for a message author and reset some
* message.channel.overwritePermissions(message.author, {
* VIEW_CHANNEL: false,
* SEND_MESSAGES: null
* })
* .then(updated => console.log(updated.permissionOverwrites.get(message.author.id)))
* .catch(console.error);
*/
overwritePermissions(userOrRole, options, reason) {
@@ -190,7 +259,7 @@ class GuildChannel extends Channel {
payload.deny = prevOverwrite.deny;
}
for (const perm in options) {
for (const perm of Object.keys(options)) {
if (options[perm] === true) {
payload.allow |= Permissions.FLAGS[perm] || 0;
payload.deny &= ~(Permissions.FLAGS[perm] || 0);
@@ -206,14 +275,36 @@ class GuildChannel extends Channel {
return this.client.rest.methods.setChannelOverwrite(this, payload, reason).then(() => this);
}
/**
* Locks in the permission overwrites from the parent channel.
* @returns {Promise<GuildChannel>}
*/
lockPermissions() {
if (!this.parent) return Promise.reject(new TypeError('Could not find a parent to this guild channel.'));
const permissionOverwrites = this.parent.permissionOverwrites.map(overwrite => ({
deny: overwrite.deny,
allow: overwrite.allow,
id: overwrite.id,
type: overwrite.type,
}));
return this.edit({ permissionOverwrites });
}
/**
* The data for a guild channel.
* @typedef {Object} ChannelData
* @property {string} [type] The type of the channel (Only when creating)
* @property {string} [name] The name of the channel
* @property {number} [position] The position of the channel
* @property {string} [topic] The topic of the text channel
* @property {boolean} [nsfw] Whether the channel is NSFW
* @property {number} [bitrate] The bitrate of the voice channel
* @property {number} [userLimit] The user limit of the channel
* @property {CategoryChannel|Snowflake} [parent] The parent or parent ID of the channel
* @property {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [permissionOverwrites]
* Overwrites of the channel
* @property {number} [rateLimitPerUser] The rate limit per user of the channel in seconds
* @property {string} [reason] Reason for creating the channel (Only when creating)
*/
/**
@@ -258,14 +349,19 @@ class GuildChannel extends Channel {
* .catch(console.error);
*/
setPosition(position, relative) {
return this.guild.setChannelPosition(this, position, relative);
return this.guild.setChannelPosition(this, position, relative).then(() => this);
}
/**
* Set a new parent for the guild channel.
* @param {GuildChannel|SnowFlake} parent The new parent for the guild channel
* @param {CategoryChannel|SnowFlake} parent The new parent for the guild channel
* @param {string} [reason] Reason for changing the guild channel's parent
* @returns {Promise<GuildChannel>}
* @example
* // Sets the parent of a channel
* channel.setParent('174674066072928256')
* .then(updated => console.log(`Set the category of ${updated.name} to ${updated.parent.name}`))
* .catch(console.error);
*/
setParent(parent, reason) {
parent = this.client.resolver.resolveChannelID(parent);
@@ -279,8 +375,8 @@ class GuildChannel extends Channel {
* @returns {Promise<GuildChannel>}
* @example
* // Set a new channel topic
* channel.setTopic('needs more rate limiting')
* .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
* channel.setTopic('Needs more rate limiting')
* .then(updated => console.log(`Channel's new topic is ${updated.topic}`))
* .catch(console.error);
*/
setTopic(topic, reason) {
@@ -308,17 +404,90 @@ class GuildChannel extends Channel {
return this.client.rest.methods.createChannelInvite(this, options, reason);
}
/* eslint-disable max-len */
/**
* Options to clone a guild channel.
* @typedef {Object} GuildChannelCloneOptions
* @property {string} [name=this.name] Name of the new channel
* @property {ChannelCreationOverwrites[]|Collection<Snowflake, PermissionOverwrites>} [permissionOverwrites=this.permissionOverwrites]
* Permission overwrites of the new channel
* @property {string} [type=this.type] Type of the new channel
* @property {string} [topic=this.topic] Topic of the new channel (only text)
* @property {boolean} [nsfw=this.nsfw] Whether the new channel is nsfw (only text)
* @property {number} [bitrate=this.bitrate] Bitrate of the new channel in bits (only voice)
* @property {number} [userLimit=this.userLimit] Maximum amount of users allowed in the new channel (only voice)
* @property {number} [rateLimitPerUser=ThisType.rateLimitPerUser] Ratelimit per user for the new channel (only text)
* @property {ChannelResolvable} [parent=this.parent] Parent of the new channel
* @property {string} [reason] Reason for cloning this channel
*/
/* eslint-enable max-len */
/**
* Clone this channel.
* @param {string} [name=this.name] Optional name for the new channel, otherwise it has the name of this channel
* @param {string|GuildChannelCloneOptions} [nameOrOptions={}] Name for the new channel.
* **(deprecated, use options)**
* Alternatively options for cloning the channel
* @param {boolean} [withPermissions=true] Whether to clone the channel with this channel's permission overwrites
* **(deprecated, use options)**
* @param {boolean} [withTopic=true] Whether to clone the channel with this channel's topic
* @param {string} [reason] Reason for cloning this channel
* **(deprecated, use options)**
* @param {string} [reason] Reason for cloning this channel **(deprecated, user options)**
* @returns {Promise<GuildChannel>}
* @example
* // Clone a channel
* channel.clone({ topic: null, reason: 'Needed a clone' })
* .then(clone => console.log(`Cloned ${channel.name} to make a channel called ${clone.name}`))
* .catch(console.error);
*/
clone(name = this.name, withPermissions = true, withTopic = true, reason) {
return this.guild.createChannel(name, this.type, withPermissions ? this.permissionOverwrites : [], reason)
.then(channel => withTopic ? channel.setTopic(this.topic) : channel);
clone(nameOrOptions = {}, withPermissions = true, withTopic = true, reason) {
// If more than one parameter was specified or the first is a string,
// convert them to a compatible options object and issue a warning
if (arguments.length > 1 || typeof nameOrOptions === 'string') {
process.emitWarning(
'GuildChannel#clone: Clone channels using an options object instead of separate parameters.',
'Deprecation Warning'
);
nameOrOptions = {
name: nameOrOptions,
permissionOverwrites: withPermissions ? this.permissionOverwrites : null,
topic: withTopic ? this.topic : null,
reason: reason || null,
};
}
Util.mergeDefault({
name: this.name,
permissionOverwrites: this.permissionOverwrites,
topic: this.topic,
type: this.type,
nsfw: this.nsfw,
parent: this.parent,
bitrate: this.bitrate,
userLimit: this.userLimit,
rateLimitPerUser: this.rateLimitPerUser,
reason: null,
}, nameOrOptions);
return this.guild.createChannel(nameOrOptions.name, nameOrOptions);
}
/**
* Fetches a collection of invites to this guild channel.
* Resolves with a collection mapping invites by their codes.
* @returns {Promise<Collection<string, Invite>>}
*/
fetchInvites() {
return this.client.rest.makeRequest('get', Constants.Endpoints.Channel(this.id).invites, true)
.then(data => {
const invites = new Collection();
for (let invite of data) {
invite = new Invite(this.client, invite);
invites.set(invite.code, invite);
}
return invites;
});
}
/**
@@ -327,9 +496,9 @@ class GuildChannel extends Channel {
* @returns {Promise<GuildChannel>}
* @example
* // Delete the channel
* channel.delete('making room for new channels')
* .then(channel => console.log(`Deleted ${channel.name} to make room for new channels`))
* .catch(console.error); // Log error
* channel.delete('Making room for new channels')
* .then(deleted => console.log(`Deleted ${deleted.name} to make room for new channels`))
* .catch(console.error);
*/
delete(reason) {
return this.client.rest.methods.deleteChannel(this, reason);
@@ -370,11 +539,24 @@ class GuildChannel extends Channel {
this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS);
}
/**
* Whether the channel is manageable by the client user
* @type {boolean}
* @readonly
*/
get manageable() {
if (this.client.user.id === this.guild.ownerID) return true;
const permissions = this.permissionsFor(this.client.user);
if (!permissions) return false;
return permissions.has([Permissions.FLAGS.MANAGE_CHANNELS, Permissions.FLAGS.VIEW_CHANNEL]);
}
/**
* Whether the channel is muted
* <warn>This is only available when using a user account.</warn>
* @type {?boolean}
* @readonly
* @deprecated
*/
get muted() {
if (this.client.user.bot) return null;
@@ -390,6 +572,7 @@ class GuildChannel extends Channel {
* <warn>This is only available when using a user account.</warn>
* @type {?MessageNotificationType}
* @readonly
* @deprecated
*/
get messageNotifications() {
if (this.client.user.bot) return null;

View File

@@ -2,7 +2,7 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel');
const Role = require('./Role');
const Permissions = require('../util/Permissions');
const Collection = require('../util/Collection');
const Presence = require('./Presence').Presence;
const { Presence } = require('./Presence');
const util = require('util');
/**
@@ -26,25 +26,43 @@ class GuildMember {
this.guild = guild;
/**
* The user that this guild member instance Represents
* The user that this member instance Represents
* @type {User}
*/
this.user = {};
/**
* The timestamp this member joined the guild at
* @type {number}
*/
this.joinedTimestamp = null;
/**
* The timestamp of when the member used their Nitro boost on the guild, if it was used
* @type {?number}
*/
this.premiumSinceTimestamp = null;
this._roles = [];
if (data) this.setup(data);
/**
* The ID of the last message sent by the member in their guild, if one was sent
* The ID of the last message sent by this member in their guild, if one was sent
* @type {?Snowflake}
*/
this.lastMessageID = null;
/**
* The Message object of the last message sent by the member in their guild, if one was sent
* The Message object of the last message sent by this member in their guild, if one was sent
* @type {?Message}
*/
this.lastMessage = null;
/**
* Whether the member has been removed from the guild
* @type {boolean}
*/
this.deleted = false;
}
setup(data) {
@@ -72,6 +90,12 @@ class GuildMember {
*/
this.selfDeaf = data.self_deaf;
/**
* Whether this member is streaming using "Go Live"
* @type {boolean}
*/
this.selfStream = data.self_stream || false;
/**
* The voice session ID of this member, if any
* @type {?Snowflake}
@@ -85,47 +109,53 @@ class GuildMember {
this.voiceChannelID = data.channel_id;
/**
* Whether this member is speaking
* Whether this member is speaking and the client is in the same channel
* @type {boolean}
*/
this.speaking = false;
/**
* The nickname of this guild member, if they have one
* The nickname of this member, if they have one
* @type {?string}
*/
this.nickname = data.nick || null;
/**
* The timestamp the member joined the guild at
* @type {number}
*/
this.joinedTimestamp = new Date(data.joined_at).getTime();
if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime();
if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime();
this.user = data.user;
this._roles = data.roles;
}
/**
* The time the member joined the guild
* @type {Date}
* The time this member joined the guild
* @type {?Date}
* @readonly
*/
get joinedAt() {
return new Date(this.joinedTimestamp);
return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null;
}
/**
* The presence of this guild member
* The time of when the member used their Nitro boost on the guild, if it was used
* @type {?Date}
* @readonly
*/
get premiumSince() {
return this.premiumSinceTimestamp ? new Date(this.premiumSinceTimestamp) : null;
}
/**
* The presence of this member
* @type {Presence}
* @readonly
*/
get presence() {
return this.frozenPresence || this.guild.presences.get(this.id) || new Presence();
return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(undefined, this.client);
}
/**
* A list of roles that are applied to this GuildMember, mapped by the role ID
* A list of roles that are applied to this member, mapped by the role ID
* @type {Collection<Snowflake, Role>}
* @readonly
*/
@@ -144,7 +174,7 @@ class GuildMember {
}
/**
* The role of the member with the highest position
* The role of this member with the highest position
* @type {Role}
* @readonly
*/
@@ -153,7 +183,7 @@ class GuildMember {
}
/**
* The role of the member used to set their color
* The role of this member used to set their color
* @type {?Role}
* @readonly
*/
@@ -164,7 +194,7 @@ class GuildMember {
}
/**
* The displayed color of the member in base 10
* The displayed color of this member in base 10
* @type {number}
* @readonly
*/
@@ -174,7 +204,7 @@ class GuildMember {
}
/**
* The displayed color of the member in hexadecimal
* The displayed color of this member in hexadecimal
* @type {string}
* @readonly
*/
@@ -184,7 +214,7 @@ class GuildMember {
}
/**
* The role of the member used to hoist them in a separate category in the users list
* The role of this member used to hoist them in a separate category in the users list
* @type {?Role}
* @readonly
*/
@@ -231,7 +261,7 @@ class GuildMember {
}
/**
* The nickname of the 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}
* @readonly
*/
@@ -240,7 +270,7 @@ class GuildMember {
}
/**
* The overall set of permissions for the guild member, taking only roles into account
* The overall set of permissions for this member, taking only roles into account
* @type {Permissions}
* @readonly
*/
@@ -255,33 +285,37 @@ class GuildMember {
}
/**
* Whether the member is kickable by the client user
* Whether this member is manageable in terms of role hierarchy by the client user
* @type {boolean}
* @readonly
*/
get manageable() {
if (this.user.id === this.guild.ownerID) return false;
if (this.user.id === this.client.user.id) return false;
if (this.client.user.id === this.guild.ownerID) return true;
return this.guild.me.highestRole.comparePositionTo(this.highestRole) > 0;
}
/**
* Whether this member is kickable by the client user
* @type {boolean}
* @readonly
*/
get kickable() {
if (this.user.id === this.guild.ownerID) return false;
if (this.user.id === this.client.user.id) return false;
const clientMember = this.guild.member(this.client.user);
if (!clientMember.permissions.has(Permissions.FLAGS.KICK_MEMBERS)) return false;
return clientMember.highestRole.comparePositionTo(this.highestRole) > 0;
return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.KICK_MEMBERS);
}
/**
* Whether the member is bannable by the client user
* Whether this member is bannable by the client user
* @type {boolean}
* @readonly
*/
get bannable() {
if (this.user.id === this.guild.ownerID) return false;
if (this.user.id === this.client.user.id) return false;
const clientMember = this.guild.member(this.client.user);
if (!clientMember.permissions.has(Permissions.FLAGS.BAN_MEMBERS)) return false;
return clientMember.highestRole.comparePositionTo(this.highestRole) > 0;
return this.manageable && this.guild.me.permissions.has(Permissions.FLAGS.BAN_MEMBERS);
}
/**
* Returns `channel.permissionsFor(guildMember)`. Returns permissions for a member in a guild channel,
* Returns `channel.permissionsFor(guildMember)`. Returns permissions for this member in a guild channel,
* taking into account roles and permission overwrites.
* @param {ChannelResolvable} channel The guild channel to use as context
* @returns {?Permissions}
@@ -293,8 +327,8 @@ class GuildMember {
}
/**
* Checks if any of the member's roles have a permission.
* @param {PermissionResolvable|PermissionResolvable[]} permission Permission(s) to check for
* Checks if any of this member's roles have a permission.
* @param {PermissionResolvable} permission Permission(s) to check for
* @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission
* **(deprecated)**
* @param {boolean} [checkAdmin] Whether to allow the administrator permission to override
@@ -311,8 +345,8 @@ class GuildMember {
}
/**
* Checks whether the roles of the member allows them to perform specific actions.
* @param {PermissionResolvable[]} permissions The permissions to check for
* Checks whether the roles of this member allows them to perform specific actions.
* @param {PermissionResolvable} permissions The permissions to check for
* @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions
* @returns {boolean}
* @deprecated
@@ -323,67 +357,93 @@ class GuildMember {
}
/**
* Checks whether the roles of the member allows them to perform specific actions, and lists any missing permissions.
* @param {PermissionResolvable[]} permissions The permissions to check for
* Checks whether the roles of this member allows them to perform specific actions, and lists any missing permissions.
* @param {PermissionResolvable} permissions The permissions to check for
* @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions
* @returns {PermissionResolvable[]}
* @returns {PermissionResolvable}
*/
missingPermissions(permissions, explicit = false) {
if (!(permissions instanceof Array)) permissions = [permissions];
return this.permissions.missing(permissions, explicit);
}
/**
* The data for editing a guild member.
* The data for editing this member.
* @typedef {Object} GuildMemberEditData
* @property {string} [nick] The nickname to set for the member
* @property {Collection<Snowflake, Role>|Role[]|Snowflake[]} [roles] The roles or role IDs to apply
* @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles or role IDs to apply
* @property {boolean} [mute] Whether or not the member should be muted
* @property {boolean} [deaf] Whether or not the member should be deafened
* @property {ChannelResolvable} [channel] Channel to move member to (if they are connected to voice)
* @property {ChannelResolvable|null} [channel] Channel to move member to (if they are connected to voice), or `null`
* if you want to kick them from voice
*/
/**
* Edit a guild member.
* Edits this member.
* @param {GuildMemberEditData} data The data to edit the member with
* @param {string} [reason] Reason for editing this user
* @returns {Promise<GuildMember>}
* @example
* // Set a member's nickname and clear their roles
* message.member.edit({
* nick: 'Cool Name',
* roles: []
* })
* .then(console.log)
* .catch(console.error);
*/
edit(data, reason) {
return this.client.rest.methods.updateGuildMember(this, data, reason);
}
/**
* Mute/unmute a user.
* Mute/unmute this member.
* @param {boolean} mute Whether or not the member should be muted
* @param {string} [reason] Reason for muting or unmuting
* @returns {Promise<GuildMember>}
* @example
* // Mute a member with a reason
* message.member.setMute(true, 'It needed to be done')
* .then(() => console.log(`Muted ${message.member.displayName}`)))
* .catch(console.error);
*/
setMute(mute, reason) {
return this.edit({ mute }, reason);
}
/**
* Deafen/undeafen a user.
* Deafen/undeafen this member.
* @param {boolean} deaf Whether or not the member should be deafened
* @param {string} [reason] Reason for deafening or undeafening
* @returns {Promise<GuildMember>}
* @example
* // Deafen a member
* message.member.setDeaf(true)
* .then(() => console.log(`Deafened ${message.member.displayName}`))
* .catch(console.error);
*/
setDeaf(deaf, reason) {
return this.edit({ deaf }, reason);
}
/**
* Moves the guild member to the given channel.
* @param {ChannelResolvable} channel The channel to move the member to
* Moves this member to the given channel.
* @param {ChannelResolvable|null} channel Channel to move the member to, or `null` if you want to kick them from
* voice
* @returns {Promise<GuildMember>}
* @example
* // Moves a member to a voice channel
* member.setVoiceChannel('174674066072928256')
* .then(() => console.log(`Moved ${member.displayName}`))
* .catch(console.error);
*/
setVoiceChannel(channel) {
return this.edit({ channel });
}
/**
* Sets the roles applied to the member.
* @param {Collection<Snowflake, Role>|Role[]|Snowflake[]} roles The roles or role IDs to apply
* Sets the roles applied to this member.
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to apply
* @param {string} [reason] Reason for applying the roles
* @returns {Promise<GuildMember>}
* @example
@@ -392,9 +452,9 @@ class GuildMember {
* .then(console.log)
* .catch(console.error);
* @example
* // Remove all the roles from a member
* // Remove all of the member's roles
* guildMember.setRoles([])
* .then(member => console.log(`Member roles is now of ${member.roles.size} size`))
* .then(member => console.log(`${member.displayName} now has ${member.roles.size} roles`))
* .catch(console.error);
*/
setRoles(roles, reason) {
@@ -402,10 +462,15 @@ class GuildMember {
}
/**
* Adds a single role to the member.
* @param {Role|Snowflake} role The role or ID of the role to add
* Adds a single role to this member.
* @param {RoleResolvable} role The role or ID of the role to add
* @param {string} [reason] Reason for adding the role
* @returns {Promise<GuildMember>}
* @example
* // Give a role to a member
* message.member.addRole('193654001089118208')
* .then(console.log)
* .catch(console.error);
*/
addRole(role, reason) {
if (!(role instanceof Role)) role = this.guild.roles.get(role);
@@ -414,10 +479,15 @@ class GuildMember {
}
/**
* Adds multiple roles to the member.
* @param {Collection<Snowflake, Role>|Role[]|Snowflake[]} roles The roles or role IDs to add
* Adds multiple roles to this member.
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to add
* @param {string} [reason] Reason for adding the roles
* @returns {Promise<GuildMember>}
* @example
* // Gives a member a few roles
* message.member.addRoles(['193654001089118208', '369308579892690945'])
* .then(console.log)
* .catch(console.error);
*/
addRoles(roles, reason) {
let allRoles;
@@ -431,10 +501,15 @@ class GuildMember {
}
/**
* Removes a single role from the member.
* @param {Role|Snowflake} role The role or ID of the role to remove
* Removes a single role from this member.
* @param {RoleResolvable} role The role or ID of the role to remove
* @param {string} [reason] Reason for removing the role
* @returns {Promise<GuildMember>}
* @example
* // Remove a role from a member
* message.member.removeRole('193654001089118208')
* .then(console.log)
* .catch(console.error);
*/
removeRole(role, reason) {
if (!(role instanceof Role)) role = this.guild.roles.get(role);
@@ -443,10 +518,15 @@ class GuildMember {
}
/**
* Removes multiple roles from the member.
* @param {Collection<Snowflake, Role>|Role[]|Snowflake[]} roles The roles or role IDs to remove
* Removes multiple roles from this member.
* @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role IDs to remove
* @param {string} [reason] Reason for removing the roles
* @returns {Promise<GuildMember>}
* @example
* // Removes a few roles from the member
* message.member.removeRoles(['193654001089118208', '369308579892690945'])
* .then(console.log)
* .catch(console.error);
*/
removeRoles(roles, reason) {
const allRoles = this._roles.slice();
@@ -465,17 +545,22 @@ class GuildMember {
}
/**
* Set the nickname for the guild member.
* Set the nickname for this member.
* @param {string} nick The nickname for the guild member
* @param {string} [reason] Reason for setting the nickname
* @returns {Promise<GuildMember>}
* @example
* // Update the member's nickname
* message.member.setNickname('Cool Name')
* .then(console.log)
* .catch(console.error);
*/
setNickname(nick, reason) {
return this.edit({ nick }, reason);
}
/**
* Creates a DM channel between the client and the member.
* Creates a DM channel between the client and this member.
* @returns {Promise<DMChannel>}
*/
createDM() {
@@ -494,13 +579,18 @@ class GuildMember {
* Kick this member from the guild.
* @param {string} [reason] Reason for kicking user
* @returns {Promise<GuildMember>}
* @example
* // Kick a member
* member.kick()
* .then(() => console.log(`Kicked ${member.displayName}`))
* .catch(console.error);
*/
kick(reason) {
return this.client.rest.methods.kickGuildMember(this.guild, this, reason);
}
/**
* Ban this guild member.
* Ban this member.
* @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a
* string, the ban reason. Supplying an object allows you to do both.
* @param {number} [options.days=0] Number of days of messages to delete
@@ -508,8 +598,8 @@ class GuildMember {
* @returns {Promise<GuildMember>}
* @example
* // Ban a guild member
* guildMember.ban(7)
* .then(console.log)
* member.ban(7)
* .then(() => console.log(`Banned ${member.displayName}`))
* .catch(console.error);
*/
ban(options) {
@@ -540,5 +630,7 @@ TextBasedChannel.applyToClass(GuildMember);
GuildMember.prototype.hasPermissions = util.deprecate(GuildMember.prototype.hasPermissions,
'GuildMember#hasPermissions is deprecated - use GuildMember#hasPermission, it now takes an array');
GuildMember.prototype.missingPermissions = util.deprecate(GuildMember.prototype.missingPermissions,
'GuildMember#missingPermissions is deprecated - use GuildMember#permissions.missing, it now takes an array');
module.exports = GuildMember;

View File

@@ -0,0 +1,151 @@
/**
* The information account for an integration
* @typedef {Object} IntegrationAccount
* @property {string} id The id of the account
* @property {string} name The name of the account
*/
/**
* Represents a guild integration.
*/
class Integration {
constructor(client, data, guild) {
/**
* The client that created this integration
* @name Integration#client
* @type {Client}
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
/**
* The guild this integration belongs to
* @type {Guild}
*/
this.guild = guild;
/**
* The integration id
* @type {Snowflake}
*/
this.id = data.id;
/**
* The integration name
* @type {string}
*/
this.name = data.name;
/**
* The integration type (twitch, youtube, etc)
* @type {string}
*/
this.type = data.type;
/**
* Whether this integration is enabled
* @type {boolean}
*/
this.enabled = data.enabled;
/**
* Whether this integration is syncing
* @type {boolean}
*/
this.syncing = data.syncing;
/**
* The role that this integration uses for subscribers
* @type {Role}
*/
this.role = this.guild.roles.get(data.role_id);
/**
* The user for this integration
* @type {User}
*/
this.user = this.client.dataManager.newUser(data.user);
/**
* The account integration information
* @type {IntegrationAccount}
*/
this.account = data.account;
/**
* The last time this integration was last synced
* @type {number}
*/
this.syncedAt = data.synced_at;
this._patch(data);
}
_patch(data) {
/**
* The behavior of expiring subscribers
* @type {number}
*/
this.expireBehavior = data.expire_behavior;
/**
* The grace period before expiring subscribers
* @type {number}
*/
this.expireGracePeriod = data.expire_grace_period;
}
/**
* Syncs this integration
* @returns {Promise<Integration>}
*/
sync() {
this.syncing = true;
return this.client.rest.methods.syncIntegration(this)
.then(() => {
this.syncing = false;
this.syncedAt = Date.now();
return this;
});
}
/**
* The data for editing an integration.
* @typedef {Object} IntegrationEditData
* @property {number} [expireBehavior] The new behaviour of expiring subscribers
* @property {number} [expireGracePeriod] The new grace period before expiring subscribers
*/
/**
* Edits this integration.
* @param {IntegrationEditData} data The data to edit this integration with
* @param {string} reason Reason for editing this integration
* @returns {Promise<Integration>}
*/
edit(data, reason) {
if ('expireBehavior' in data) {
data.expire_behavior = data.expireBehavior;
data.expireBehavior = undefined;
}
if ('expireGracePeriod' in data) {
data.expire_grace_period = data.expireGracePeriod;
data.expireGracePeriod = undefined;
}
// The option enable_emoticons is only available for Twitch at this moment
return this.client.rest.methods.editIntegration(this, data, reason)
.then(() => {
this._patch(data);
return this;
});
}
/**
* Deletes this integration.
* @returns {Promise<Integration>}
* @param {string} [reason] Reason for deleting this integration
*/
delete(reason) {
return this.client.rest.methods.deleteIntegration(this, reason)
.then(() => this);
}
}
module.exports = Integration;

View File

@@ -4,7 +4,8 @@ const Constants = require('../util/Constants');
/**
* Represents an invitation to a guild channel.
* <warn>The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing.</warn>
* <warn>The only guaranteed properties are `code`, `url`, `guild`, and `channel`.
* Other properties can be missing.</warn>
*/
class Invite {
constructor(client, data) {
@@ -84,7 +85,7 @@ class Invite {
if (data.inviter) {
/**
* The user who created this invite
* @type {User}
* @type {?User}
*/
this.inviter = this.client.dataManager.newUser(data.inviter);
}

View File

@@ -8,6 +8,7 @@ const Util = require('../util/Util');
const Collection = require('../util/Collection');
const Constants = require('../util/Constants');
const Permissions = require('../util/Permissions');
const MessageFlags = require('../util/MessageFlags');
let GuildMember;
/**
@@ -29,6 +30,12 @@ class Message {
*/
this.channel = channel;
/**
* Whether this message has been deleted
* @type {boolean}
*/
this.deleted = false;
if (data) this.setup(data);
}
@@ -41,7 +48,7 @@ class Message {
/**
* The type of the message
* @type {string}
* @type {MessageType}
*/
this.type = Constants.MessageTypes[data.type];
@@ -55,14 +62,7 @@ class Message {
* The author of the message
* @type {User}
*/
this.author = this.client.dataManager.newUser(data.author);
/**
* Represents the author of the message as a guild member
* Only available if the message comes from a guild where the author is still a member
* @type {?GuildMember}
*/
this.member = this.guild ? this.guild.member(this.author) || null : null;
this.author = this.client.dataManager.newUser(data.author, !data.webhook_id);
/**
* Whether or not this message is pinned
@@ -78,7 +78,9 @@ class Message {
/**
* A random number or string used for checking message delivery
* @type {string}
* <warn>This is only received after the message was sent successfully, and
* lost if re-fetched</warn>
* @type {?string}
*/
this.nonce = data.nonce;
@@ -86,7 +88,7 @@ class Message {
* Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
* @type {boolean}
*/
this.system = data.type === 6;
this.system = data.type !== 0;
/**
* A list of embeds in the message - e.g. YouTube Player
@@ -129,7 +131,7 @@ class Message {
* All valid mentions that the message contains
* @type {MessageMentions}
*/
this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone);
this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone, data.mention_channels);
/**
* ID of the webhook that sent the message, if applicable
@@ -143,12 +145,47 @@ class Message {
*/
this.hit = typeof data.hit === 'boolean' ? data.hit : null;
/**
* Flags that are applied to the message
* @type {Readonly<MessageFlags>}
*/
this.flags = new MessageFlags(data.flags).freeze();
/**
* Reference data sent in a crossposted message.
* @typedef {Object} MessageReference
* @property {string} channelID ID of the channel the message was crossposted from
* @property {?string} guildID ID of the guild the message was crossposted from
* @property {?string} messageID ID of the message that was crossposted
*/
/**
* Message reference data
* @type {?MessageReference}
*/
this.reference = data.message_reference ? {
channelID: data.message_reference.channel_id,
guildID: data.message_reference.guild_id,
messageID: data.message_reference.message_id,
} : null;
/**
* The previous versions of the message, sorted with the most recent first
* @type {Message[]}
* @private
*/
this._edits = [];
if (data.member && this.guild && this.author && !this.guild.members.has(this.author.id)) {
this.guild._addMember(Object.assign(data.member, { user: this.author }), false);
}
/**
* Represents the author of the message as a guild member
* Only available if the message comes from a guild where the author is still a member
* @type {?GuildMember}
*/
this.member = this.guild ? this.guild.member(this.author) || null : null;
}
/**
@@ -160,7 +197,7 @@ class Message {
const clone = Util.cloneObject(this);
this._edits.unshift(clone);
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 ('pinned' in data) this.pinned = data.pinned;
if ('tts' in data) this.tts = data.tts;
@@ -178,8 +215,11 @@ class Message {
this,
'mentions' in data ? data.mentions : this.mentions.users,
'mentions_roles' in data ? data.mentions_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
);
this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze();
}
/**
@@ -209,6 +249,15 @@ class Message {
return this.channel.guild || null;
}
/**
* The url to jump to the message
* @type {string}
* @readonly
*/
get url() {
return `https://discordapp.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`;
}
/**
* The message contents with all mentions replaced by the equivalent text.
* If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted.
@@ -319,9 +368,9 @@ class Message {
* @readonly
*/
get deletable() {
return this.author.id === this.client.user.id || (this.guild &&
return !this.deleted && (this.author.id === this.client.user.id || (this.guild &&
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES)
);
));
}
/**
@@ -330,8 +379,8 @@ class Message {
* @readonly
*/
get pinnable() {
return !this.guild ||
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES);
return this.type === 'DEFAULT' && (!this.guild ||
this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES));
}
/**
@@ -365,6 +414,7 @@ class Message {
* @typedef {Object} MessageEditOptions
* @property {Object} [embed] An embed to be added/edited
* @property {string|boolean} [code] Language for optional codeblock formatting to apply
* @property {MessageFlagsResolvable} [flags] Message flags to apply
*/
/**
@@ -375,7 +425,7 @@ class Message {
* @example
* // Update the content of a message
* message.edit('This is my new content!')
* .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
* .then(msg => console.log(`New message content: ${msg}`))
* .catch(console.error);
*/
edit(content, options) {
@@ -421,6 +471,16 @@ class Message {
* Add a reaction to the message.
* @param {string|Emoji|ReactionEmoji} emoji The emoji to react with
* @returns {Promise<MessageReaction>}
* @example
* // React to a message with a unicode emoji
* message.react('🤔')
* .then(console.log)
* .catch(console.error);
* @example
* // React to a message with a custom emoji
* message.react(message.guild.emojis.get('123456789012345678'))
* .then(console.log)
* .catch(console.error);
*/
react(emoji) {
emoji = this.client.resolver.resolveEmojiIdentifier(emoji);
@@ -444,7 +504,7 @@ class Message {
* @example
* // Delete a message
* message.delete()
* .then(msg => console.log(`Deleted message from ${msg.author}`))
* .then(msg => console.log(`Deleted message from ${msg.author.username}`))
* .catch(console.error);
*/
delete(timeout = 0) {
@@ -467,7 +527,7 @@ class Message {
* @example
* // Reply to a message
* message.reply('Hey, I\'m a reply!')
* .then(msg => console.log(`Sent a reply to ${msg.author}`))
* .then(sent => console.log(`Sent a reply to ${sent.author.username}`))
* .catch(console.error);
*/
reply(content, options) {
@@ -484,6 +544,7 @@ class Message {
* Marks the message as read.
* <warn>This is only available when using a user account.</warn>
* @returns {Promise<Message>}
* @deprecated
*/
acknowledge() {
return this.client.rest.methods.ackMessage(this);
@@ -498,6 +559,23 @@ class Message {
return this.client.fetchWebhook(this.webhookID);
}
/**
* Suppresses or unsuppresses embeds on a message
* @param {boolean} [suppress=true] If the embeds should be suppressed or not
* @returns {Promise<Message>}
*/
suppressEmbeds(suppress = true) {
const flags = new MessageFlags(this.flags.bitfield);
if (suppress) {
flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
} else {
flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
}
return this.edit(undefined, { flags });
}
/**
* Used mainly internally. Whether two messages are identical in properties. If you want to compare messages
* without checking all the properties, use `message.id === message2.id`, which is much more efficient. This
@@ -512,12 +590,12 @@ class Message {
if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length;
let equal = this.id === message.id &&
this.author.id === message.author.id &&
this.content === message.content &&
this.tts === message.tts &&
this.nonce === message.nonce &&
this.embeds.length === message.embeds.length &&
this.attachments.length === message.attachments.length;
this.author.id === message.author.id &&
this.content === message.content &&
this.tts === message.tts &&
this.nonce === message.nonce &&
this.embeds.length === message.embeds.length &&
this.attachments.length === message.attachments.length;
if (equal && rawData) {
equal = this.mentions.everyone === message.mentions.everyone &&
@@ -560,6 +638,10 @@ class Message {
const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name;
if (this.reactions.has(emojiID)) {
const reaction = this.reactions.get(emojiID);
if (!user) {
this.reactions.delete(emojiID);
return reaction;
}
if (reaction.users.has(user.id)) {
reaction.users.delete(user.id);
reaction.count--;

View File

@@ -1,3 +1,5 @@
const { basename } = require('path');
/**
* Represents an attachment in a message.
*/
@@ -63,6 +65,15 @@ class MessageAttachment {
*/
this.width = data.width;
}
/**
* Whether or not this attachment has been marked as a spoiler
* @type {boolean}
* @readonly
*/
get spoiler() {
return basename(this.url).startsWith('SPOILER_');
}
}
module.exports = MessageAttachment;

View File

@@ -33,11 +33,9 @@ class MessageCollector extends Collector {
*/
this.received = 0;
if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1);
this.client.on('message', this.listener);
// For backwards compatibility (remove in v12)
if (this.options.max) this.options.maxProcessed = this.options.max;
if (this.options.maxMatches) this.options.max = this.options.maxMatches;
this._reEmitter = message => {
/**
* Emitted when the collector receives a message.
@@ -80,8 +78,8 @@ class MessageCollector extends Collector {
*/
postCheck() {
// Consider changing the end reasons for v12
if (this.options.maxMatches && this.collected.size >= this.options.max) return 'matchesLimit';
if (this.options.max && this.received >= this.options.maxProcessed) return 'limit';
if (this.options.maxMatches && this.collected.size >= this.options.maxMatches) return 'matchesLimit';
if (this.options.max && this.received >= this.options.max) return 'limit';
return null;
}
@@ -92,6 +90,7 @@ class MessageCollector extends Collector {
cleanup() {
this.removeListener('collect', this._reEmitter);
this.client.removeListener('message', this.listener);
if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1);
}
}

View File

@@ -1,6 +1,6 @@
/**
* Represents an embed in a message (image/video preview, rich embed, etc.)
* <info>This class is only used for *recieved* embeds. If you wish to send one, use the {@link RichEmbed} class.</info>
* <info>This class is only used for *received* embeds. If you wish to send one, use the {@link RichEmbed} class.</info>
*/
class MessageEmbed {
constructor(message, data) {
@@ -63,7 +63,7 @@ class MessageEmbed {
* The timestamp of this embed
* @type {number}
*/
this.createdTimestamp = data.timestamp;
this.timestamp = data.timestamp;
/**
* The thumbnail of this embed
@@ -113,10 +113,11 @@ class MessageEmbed {
/**
* The hexadecimal version of the embed color, with a leading hash
* @type {string}
* @type {?string}
* @readonly
*/
get hexColor() {
if (!this.color) return null;
let col = this.color.toString(16);
while (col.length < 6) col = `0${col}`;
return `#${col}`;

View File

@@ -1,10 +1,11 @@
const Collection = require('../util/Collection');
const { ChannelTypes } = require('../util/Constants');
/**
* Keeps track of mentions in a {@link Message}.
*/
class MessageMentions {
constructor(message, users, roles, everyone) {
constructor(message, users, roles, everyone, crosspostedChannels) {
/**
* Whether `@everyone` or `@here` were mentioned
* @type {boolean}
@@ -15,6 +16,7 @@ class MessageMentions {
if (users instanceof Collection) {
/**
* Any users that were mentioned
* <info>Order as received from the API, not as they appear in the message content</info>
* @type {Collection<Snowflake, User>}
*/
this.users = new Collection(users);
@@ -24,6 +26,9 @@ class MessageMentions {
let user = message.client.users.get(mention.id);
if (!user) user = message.client.dataManager.newUser(mention);
this.users.set(user.id, user);
if (mention.member && message.guild && !message.guild.members.has(mention.id)) {
message.guild._addMember(Object.assign(mention.member, { user }), false);
}
}
}
} else {
@@ -34,6 +39,7 @@ class MessageMentions {
if (roles instanceof Collection) {
/**
* Any roles that were mentioned
* <info>Order as received from the API, not as they appear in the message content</
* @type {Collection<Snowflake, Role>}
*/
this.roles = new Collection(roles);
@@ -82,10 +88,44 @@ class MessageMentions {
* @private
*/
this._channels = null;
/**
* Crossposted channel data.
* @typedef {Object} CrosspostedChannel
* @property {Snowflake} channelID ID of the mentioned channel
* @property {Snowflake} guildID ID of the guild that has the channel
* @property {string} type Type of the channel
* @property {string} name Name of the channel
*/
if (crosspostedChannels) {
if (crosspostedChannels instanceof Collection) {
/**
* A collection of crossposted channels
* @type {Collection<Snowflake, CrosspostedChannel>}
*/
this.crosspostedChannels = new Collection(crosspostedChannels);
} else {
this.crosspostedChannels = new Collection();
const channelTypes = Object.keys(ChannelTypes);
for (const d of crosspostedChannels) {
const type = channelTypes[d.type];
this.crosspostedChannels.set(d.id, {
channelID: d.id,
guildID: d.guild_id,
type: type ? type.toLowerCase() : 'unknown',
name: d.name,
});
}
}
} else {
this.crosspostedChannels = new Collection();
}
}
/**
* Any members that were mentioned (only in {@link TextChannel}s)
* <info>Order as received from the API, not as they appear in the message content</
* @type {?Collection<Snowflake, GuildMember>}
* @readonly
*/
@@ -102,6 +142,7 @@ class MessageMentions {
/**
* Any channels that were mentioned
* <info>Order as they appear first in the message content</info>
* @type {Collection<Snowflake, GuildChannel>}
* @readonly
*/

View File

@@ -31,7 +31,7 @@ class MessageReaction {
*/
this.users = new Collection();
this._emoji = new ReactionEmoji(this, emoji.name, emoji.id);
this._emoji = new ReactionEmoji(this, emoji);
}
/**
@@ -69,6 +69,17 @@ class MessageReaction {
);
}
/**
* Removes this reaction from the message
* @returns {Promise<MessageReaction>}
*/
removeAll() {
const message = this.message;
return message.client.rest.methods.removeMessageReactionEmoji(
message, this.emoji.identifier
);
}
/**
* Fetch all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs.
* @param {number} [limit=100] The maximum amount of users to fetch, defaults to 100
@@ -81,12 +92,14 @@ class MessageReaction {
const message = this.message;
return message.client.rest.methods.getMessageReactionUsers(
message, this.emoji.identifier, { after, before, limit }
).then(users => {
for (const rawUser of users) {
).then(data => {
const users = new Collection();
for (const rawUser of data) {
const user = this.message.client.dataManager.newUser(rawUser);
this.users.set(user.id, user);
users.set(user.id, user);
}
return this.users;
return users;
});
}
}

View File

@@ -0,0 +1,24 @@
const TextChannel = require('./TextChannel');
/**
* Represents a guild news channel on Discord.
* @extends {TextChannel}
*/
class NewsChannel extends TextChannel {
constructor(guild, data) {
super(guild, data);
this.type = 'news';
}
setup(data) {
super.setup(data);
/**
* The ratelimit per user for this channel (always 0)
* @type {number}
*/
this.rateLimitPerUser = 0;
}
}
module.exports = NewsChannel;

View File

@@ -1,4 +1,6 @@
const Snowflake = require('../util/Snowflake');
const Team = require('./Team');
const util = require('util');
/**
* Represents an OAuth2 Application.
@@ -102,6 +104,14 @@ class OAuth2Application {
*/
this.owner = this.client.dataManager.newUser(data.owner);
}
/**
* The owning team of this OAuth application
* <info>In v12.0.0 this property moves to `Team#owner`.</info>
* @type {?Team}
* @deprecated
*/
this.team = data.team ? new Team(this.client, data.team) : null;
}
/**
@@ -126,6 +136,7 @@ class OAuth2Application {
* Reset the app's secret and bot token.
* <warn>This is only available when using a user account.</warn>
* @returns {OAuth2Application}
* @deprecated
*/
reset() {
return this.client.rest.methods.resetApplication(this.id);
@@ -140,4 +151,7 @@ class OAuth2Application {
}
}
OAuth2Application.prototype.reset =
util.deprecate(OAuth2Application.prototype.reset, 'OAuth2Application#reset: userbot methods will be removed');
module.exports = OAuth2Application;

View File

@@ -1,3 +1,5 @@
const Permissions = require('../util/Permissions');
/**
* Represents a permission overwrite for a role or member in a guild channel.
*/
@@ -27,8 +29,31 @@ class PermissionOverwrites {
*/
this.type = data.type;
/**
* The permissions that are denied for the user or role as a bitfield.
* @type {number}
*/
this.deny = data.deny;
/**
* The permissions that are allowed for the user or role as a bitfield.
* @type {number}
*/
this.allow = data.allow;
/**
* The permissions that are denied for the user or role.
* @type {Permissions}
* @deprecated
*/
this.denied = new Permissions(data.deny).freeze();
/**
* The permissions that are allowed for the user or role.
* @type {Permissions}
* @deprecated
*/
this.allowed = new Permissions(data.allow).freeze();
}
/**

View File

@@ -1,29 +1,73 @@
const { ActivityFlags, Endpoints } = require('../util/Constants');
const ReactionEmoji = require('./ReactionEmoji');
/**
* The status of this presence:
* * **`online`** - user is online
* * **`idle`** - user is AFK
* * **`offline`** - user is offline or invisible
* * **`dnd`** - user is in Do Not Disturb
* @typedef {string} PresenceStatus
*/
/**
* The status of this presence:
* * **`online`** - user is online
* * **`idle`** - user is AFK
* * **`dnd`** - user is in Do Not Disturb
* @typedef {string} ClientPresenceStatus
*/
/**
* Represents a user's presence.
*/
class Presence {
constructor(data = {}) {
constructor(data = {}, client) {
/**
* The status of the presence:
*
* * **`online`** - user is online
* * **`offline`** - user is offline or invisible
* * **`idle`** - user is AFK
* * **`dnd`** - user is in Do not Disturb
* @type {string}
* The client that instantiated this
* @name Presence#client
* @type {Client}
* @readonly
*/
this.status = data.status || 'offline';
Object.defineProperty(this, 'client', { value: client });
this.update(data);
}
update(data) {
/**
* The status of this presence:
* @type {PresenceStatus}
*/
this.status = data.status || this.status || 'offline';
/**
* The game that the user is playing
* @type {?Game}
* @deprecated
*/
this.game = data.game ? new Game(data.game) : null;
}
this.game = data.game ? new Game(data.game, this) : null;
update(data) {
this.status = data.status || this.status;
this.game = data.game ? new Game(data.game) : null;
if (data.activities) {
/**
* The activities of this presence
* @type {Game[]}
*/
this.activities = data.activities.map(activity => new Game(activity, this));
} else if (data.activity || data.game) {
this.activities = [new Game(data.activity || data.game, this)];
} else {
this.activities = [];
}
/**
* The devices this presence is on
* @type {?Object}
* @property {?ClientPresenceStatus} web The current presence in the web application
* @property {?ClientPresenceStatus} mobile The current presence in the mobile application
* @property {?ClientPresenceStatus} desktop The current presence in the desktop application
*/
this.clientStatus = data.client_status || null;
}
/**
@@ -35,7 +79,11 @@ class Presence {
return this === presence || (
presence &&
this.status === presence.status &&
this.game ? this.game.equals(presence.game) : !presence.game
this.activities.length === presence.activities.length &&
this.activities.every((activity, index) => activity.equals(presence.activities[index])) &&
this.clientStatus.web === presence.clientStatus.web &&
this.clientStatus.mobile === presence.clientStatus.mobile &&
this.clientStatus.desktop === presence.clientStatus.desktop
);
}
}
@@ -44,7 +92,9 @@ class Presence {
* Represents a game that is part of a user's presence.
*/
class Game {
constructor(data) {
constructor(data, presence) {
Object.defineProperty(this, 'presence', { value: presence });
/**
* The name of the game being played
* @type {string}
@@ -52,7 +102,11 @@ class Game {
this.name = data.name;
/**
* The type of the game status
* The type of the game status, its possible values:
* - 0: Playing
* - 1: Streaming
* - 2: Listening
* - 3: Watching
* @type {number}
*/
this.type = data.type;
@@ -62,6 +116,92 @@ class Game {
* @type {?string}
*/
this.url = data.url || null;
/**
* Details about the activity
* @type {?string}
*/
this.details = data.details || null;
/**
* State of the activity
* @type {?string}
*/
this.state = data.state || null;
/**
* Application ID associated with this activity
* @type {?Snowflake}
*/
this.applicationID = data.application_id || null;
/**
* Timestamps for the activity
* @type {?Object}
* @prop {?Date} start When the activity started
* @prop {?Date} end When the activity will end
*/
this.timestamps = data.timestamps ? {
start: data.timestamps.start ? new Date(Number(data.timestamps.start)) : null,
end: data.timestamps.end ? new Date(Number(data.timestamps.end)) : null,
} : null;
/**
* Party of the activity
* @type {?Object}
* @prop {?string} id ID of the party
* @prop {number[]} size Size of the party as `[current, max]`
*/
this.party = data.party || null;
/**
* Assets for rich presence
* @type {?RichPresenceAssets}
*/
this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null;
if (data.emoji) {
/**
* Emoji for a custom activity
* <warn>There is no `reaction` property for this emoji.</warn>
* @type {?ReactionEmoji}
*/
this.emoji = new ReactionEmoji({ message: { client: this.presence.client } }, data.emoji);
this.emoji.reaction = null;
} else {
this.emoji = null;
}
/**
* Creation date of the activity
* @type {number}
*/
this.createdTimestamp = new Date(data.created_at).getTime();
this.syncID = data.sync_id;
this._flags = data.flags;
}
/**
* The time the activity was created at
* @type {Date}
* @readonly
*/
get createdAt() {
return new Date(this.createdTimestamp);
}
/**
* Flags that describe the activity
* @type {ActivityFlags[]}
*/
get flags() {
const flags = [];
for (const [name, flag] of Object.entries(ActivityFlags)) {
if ((this._flags & flag) === flag) flags.push(name);
}
return flags;
}
/**
@@ -73,6 +213,14 @@ class Game {
return this.type === 1;
}
/**
* When concatenated with a string, this automatically returns the game's name instead of the Game object.
* @returns {string}
*/
toString() {
return this.name;
}
/**
* Whether this game is equal to another game
* @param {Game} game The game to compare with
@@ -88,5 +236,66 @@ class Game {
}
}
/**
* Assets for a rich presence
*/
class RichPresenceAssets {
constructor(game, assets) {
Object.defineProperty(this, 'game', { value: game });
/**
* Hover text for the large image
* @type {?string}
*/
this.largeText = assets.large_text || null;
/**
* Hover text for the small image
* @type {?string}
*/
this.smallText = assets.small_text || null;
/**
* ID of the large image asset
* @type {?Snowflake}
*/
this.largeImage = assets.large_image || null;
/**
* ID of the small image asset
* @type {?Snowflake}
*/
this.smallImage = assets.small_image || null;
}
/**
* The URL of the small image asset
* @type {?string}
* @readonly
*/
get smallImageURL() {
if (!this.smallImage) return null;
return Endpoints.CDN(this.game.presence.client.options.http.cdn)
.AppAsset(this.game.applicationID, this.smallImage);
}
/**
* The URL of the large image asset
* @type {?string}
* @readonly
*/
get largeImageURL() {
if (!this.largeImage) return null;
if (/^spotify:/.test(this.largeImage)) {
return `https://i.scdn.co/image/${this.largeImage.slice(8)}`;
} else if (/^twitch:/.test(this.largeImage)) {
return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${this.largeImage.slice(7)}.png`;
}
return Endpoints.CDN(this.game.presence.client.options.http.cdn)
.AppAsset(this.game.applicationID, this.largeImage);
}
}
exports.Presence = Presence;
exports.Game = Game;
exports.RichPresenceAssets = RichPresenceAssets;

View File

@@ -39,7 +39,13 @@ class ReactionCollector extends Collector {
*/
this.total = 0;
if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1);
this.client.on('messageReactionAdd', this.listener);
this.on('fullCollect', (reaction, user) => {
this.users.set(user.id, user);
this.total++;
});
}
/**
@@ -63,9 +69,8 @@ class ReactionCollector extends Collector {
* @returns {?string} Reason to end the collector, if any
* @private
*/
postCheck(reaction, user) {
this.users.set(user.id, user);
if (this.options.max && ++this.total >= this.options.max) return 'limit';
postCheck() {
if (this.options.max && this.total >= this.options.max) return 'limit';
if (this.options.maxEmojis && this.collected.size >= this.options.maxEmojis) return 'emojiLimit';
if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit';
return null;
@@ -77,6 +82,7 @@ class ReactionCollector extends Collector {
*/
cleanup() {
this.client.removeListener('messageReactionAdd', this.listener);
if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1);
}
}

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