mirror of
https://github.com/discordjs/discord.js.git
synced 2026-05-27 22:10:08 +00:00
Compare commits
317 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da39e858a1 | ||
|
|
747d76de10 | ||
|
|
de0cacdf32 | ||
|
|
bb4cb3e7fe | ||
|
|
08865a98cd | ||
|
|
20075e306b | ||
|
|
d72172744e | ||
|
|
34d352dcbe | ||
|
|
b3931eaebb | ||
|
|
08e7328b86 | ||
|
|
97457e1de2 | ||
|
|
6eaf63fb7c | ||
|
|
cf646b5394 | ||
|
|
b0aed050e3 | ||
|
|
b0d0b81c61 | ||
|
|
7e9c995566 | ||
|
|
330c410796 | ||
|
|
ab866d6b2e | ||
|
|
544b14a5ed | ||
|
|
46e8bc44fc | ||
|
|
b7ccf9a53e | ||
|
|
dbdb49ee1c | ||
|
|
83bc6e0779 | ||
|
|
364914fd35 | ||
|
|
c955fd00c7 | ||
|
|
a12e1e87ee | ||
|
|
2589db6633 | ||
|
|
17b8b23b80 | ||
|
|
ccd60438df | ||
|
|
fbcd363ec9 | ||
|
|
6d7e1e4953 | ||
|
|
ab7f9e80b4 | ||
|
|
6b297b8776 | ||
|
|
099a1a47e8 | ||
|
|
30adb378fc | ||
|
|
88b675d38a | ||
|
|
4ca18647ba | ||
|
|
a505a55e03 | ||
|
|
903f6ca75f | ||
|
|
40afbc1d7e | ||
|
|
17237c70c8 | ||
|
|
464ef25898 | ||
|
|
d8419ac2c7 | ||
|
|
c5d2b96524 | ||
|
|
01826aeefe | ||
|
|
0f49d67e2e | ||
|
|
6ab46491c8 | ||
|
|
36c0496ea5 | ||
|
|
07996d12a2 | ||
|
|
684bb1bf36 | ||
|
|
f6d1db6a24 | ||
|
|
5556b05241 | ||
|
|
fbe9bc499b | ||
|
|
d1d0d75d4a | ||
|
|
367c80070f | ||
|
|
cbabc1663c | ||
|
|
1d6606293a | ||
|
|
96037e107f | ||
|
|
f91ad7023b | ||
|
|
18613526bd | ||
|
|
91600a6946 | ||
|
|
7011c512fb | ||
|
|
2610bf57ae | ||
|
|
8ddd0616a9 | ||
|
|
a8e365743c | ||
|
|
94ce19dd1a | ||
|
|
2a78b00454 | ||
|
|
505df2ebb3 | ||
|
|
748555036d | ||
|
|
43c0a794e1 | ||
|
|
dcee09c308 | ||
|
|
1121b2f7bf | ||
|
|
0cd7556934 | ||
|
|
c355236f7f | ||
|
|
b8924369ea | ||
|
|
e6a378b361 | ||
|
|
6f49aadf4f | ||
|
|
0c6101901d | ||
|
|
5dd9181497 | ||
|
|
db492e66e2 | ||
|
|
8c213e9088 | ||
|
|
06b72ee19f | ||
|
|
e3d03adcf8 | ||
|
|
363cec31bc | ||
|
|
c844a7b4e2 | ||
|
|
e82633fb00 | ||
|
|
b2f89e4594 | ||
|
|
8fba786765 | ||
|
|
43f2485c9c | ||
|
|
8a086e04ab | ||
|
|
f30019dd4f | ||
|
|
5e4654ee07 | ||
|
|
ee42bdfd76 | ||
|
|
67da457c0a | ||
|
|
60013b7a10 | ||
|
|
12e041bc2b | ||
|
|
3cd224dc76 | ||
|
|
923c945b4b | ||
|
|
831f988fe2 | ||
|
|
5cd6d8d380 | ||
|
|
745e18b823 | ||
|
|
be2f78851f | ||
|
|
6e761eb030 | ||
|
|
32ad56a562 | ||
|
|
f61c044b26 | ||
|
|
45a17e7ebd | ||
|
|
49e8bd9edd | ||
|
|
1618829cc6 | ||
|
|
a0ff72b556 | ||
|
|
7bc2e231cf | ||
|
|
890b1be714 | ||
|
|
a2a0c05102 | ||
|
|
5272cec6c8 | ||
|
|
359ddaf1df | ||
|
|
8b602ebed4 | ||
|
|
73aaab5106 | ||
|
|
3b7b282b69 | ||
|
|
5ed2a95856 | ||
|
|
46fd7b093c | ||
|
|
17ca83663f | ||
|
|
9d83516918 | ||
|
|
89a9b93cdc | ||
|
|
7186c91063 | ||
|
|
2aa8e1d9c1 | ||
|
|
351f0a32bf | ||
|
|
691aaef07e | ||
|
|
6aa7792097 | ||
|
|
980d71f307 | ||
|
|
b3f459091f | ||
|
|
950abd4ac3 | ||
|
|
7ea88adeca | ||
|
|
ea3e575546 | ||
|
|
fcf4745a43 | ||
|
|
b92f8d9c06 | ||
|
|
c6201ee41b | ||
|
|
1e85887229 | ||
|
|
e0f522a745 | ||
|
|
9de3e098da | ||
|
|
641ee86105 | ||
|
|
da2d4d7230 | ||
|
|
cd58599caf | ||
|
|
b83fdbfefe | ||
|
|
89986ae293 | ||
|
|
091b4fc214 | ||
|
|
3345c77ce2 | ||
|
|
1fc84a95d0 | ||
|
|
58ba2c7b14 | ||
|
|
2e2c9c4b9a | ||
|
|
bd14d5d2fa | ||
|
|
453098117f | ||
|
|
93bf430fc7 | ||
|
|
469fbe2889 | ||
|
|
911e289b55 | ||
|
|
7684ad3ca6 | ||
|
|
552323363b | ||
|
|
d3e7dbc738 | ||
|
|
4ee3cf0b55 | ||
|
|
dc8cf08de9 | ||
|
|
f5b3dff7f5 | ||
|
|
5d889be6db | ||
|
|
616e0dd398 | ||
|
|
efc8445260 | ||
|
|
ebfbf20f07 | ||
|
|
3021e5ce7f | ||
|
|
b5df8603e8 | ||
|
|
8b1bb95b1a | ||
|
|
6775684ff0 | ||
|
|
d772bff632 | ||
|
|
8adfc97409 | ||
|
|
72bb9cb532 | ||
|
|
0e370d7a4c | ||
|
|
7126cade45 | ||
|
|
bef07152c8 | ||
|
|
16331d9e85 | ||
|
|
bafbee9677 | ||
|
|
6da423fc07 | ||
|
|
1e5b5b83c8 | ||
|
|
c76f3048af | ||
|
|
af6d649510 | ||
|
|
c7f379f51d | ||
|
|
4a48a7d621 | ||
|
|
c33ab1ea61 | ||
|
|
87b4b23596 | ||
|
|
b63948e14e | ||
|
|
4a9c2f8884 | ||
|
|
41f6eaa635 | ||
|
|
95a2d25b7d | ||
|
|
d685e39af4 | ||
|
|
96011cf68f | ||
|
|
488b1eb4ee | ||
|
|
6d70da5b1e | ||
|
|
e4da97e058 | ||
|
|
fbbd8f43b3 | ||
|
|
886c21223e | ||
|
|
a97b5040e6 | ||
|
|
524a15df0b | ||
|
|
0702a0fcda | ||
|
|
1d9edec567 | ||
|
|
d81441f627 | ||
|
|
695ff1e70f | ||
|
|
a667e44c4d | ||
|
|
3d82ca901b | ||
|
|
448f38429d | ||
|
|
3fa9ed1f42 | ||
|
|
72346fb47e | ||
|
|
7ce1d1642c | ||
|
|
8a3ae875bb | ||
|
|
bd3d8d48c0 | ||
|
|
493ba73037 | ||
|
|
507cce3ff4 | ||
|
|
6de5acbac3 | ||
|
|
ecf6e2b62d | ||
|
|
caa4104b95 | ||
|
|
f238883046 | ||
|
|
eef4a4ad17 | ||
|
|
c699888780 | ||
|
|
0387d34ab4 | ||
|
|
13880fe7de | ||
|
|
f23b61794c | ||
|
|
f456f4c3c0 | ||
|
|
f921261f3f | ||
|
|
6070b22382 | ||
|
|
b2d1cb6a3d | ||
|
|
09ddbcb88a | ||
|
|
6f02be2b2e | ||
|
|
0d90798c6c | ||
|
|
379061987c | ||
|
|
de7d90ada3 | ||
|
|
2b6592ed54 | ||
|
|
9bb8831619 | ||
|
|
99041671f0 | ||
|
|
dd7eedbd48 | ||
|
|
c0ca73a40c | ||
|
|
15a8e17710 | ||
|
|
54913d9edb | ||
|
|
9169958264 | ||
|
|
96b115ef7b | ||
|
|
2d831269ab | ||
|
|
e5e59cce32 | ||
|
|
ae28f52e0d | ||
|
|
9264511092 | ||
|
|
2f2e28183b | ||
|
|
54fa5f644f | ||
|
|
b757f9ef2d | ||
|
|
bd9c9ce4e0 | ||
|
|
60288d0704 | ||
|
|
ed8ab91782 | ||
|
|
21326f67a0 | ||
|
|
8700e965c5 | ||
|
|
a89de09855 | ||
|
|
a85d801a12 | ||
|
|
5f50d9e627 | ||
|
|
33a4232652 | ||
|
|
d9a091f674 | ||
|
|
44fefdfa49 | ||
|
|
b05622766b | ||
|
|
49ad8cc2cc | ||
|
|
7b9e84dff5 | ||
|
|
384e96d51e | ||
|
|
d7e7803178 | ||
|
|
feb0991c46 | ||
|
|
ff671b2f3c | ||
|
|
b60ee25038 | ||
|
|
3ba26ad972 | ||
|
|
de78a8d0b4 | ||
|
|
7c37a0d386 | ||
|
|
c387e96078 | ||
|
|
7c0b6173dd | ||
|
|
92b421607e | ||
|
|
f3ae7fd638 | ||
|
|
6e5b674338 | ||
|
|
e5bd6ec150 | ||
|
|
c8f78b2bf0 | ||
|
|
419a2eea3f | ||
|
|
a3cec3bc1f | ||
|
|
ad93b90cd9 | ||
|
|
8f9e911b5f | ||
|
|
363ead922a | ||
|
|
96e88f3cef | ||
|
|
fcdffcf623 | ||
|
|
acdf43a872 | ||
|
|
38f5288be8 | ||
|
|
f64e924f0d | ||
|
|
f2c5714751 | ||
|
|
ced93fe826 | ||
|
|
7f5c1038db | ||
|
|
af75e43900 | ||
|
|
b79722a77b | ||
|
|
2b24b10246 | ||
|
|
af3517594f | ||
|
|
c6f92c1bc5 | ||
|
|
b7851bad37 | ||
|
|
0ec53c9d6f | ||
|
|
dc92582eb4 | ||
|
|
be40858e32 | ||
|
|
5454a074ee | ||
|
|
2254a3ee11 | ||
|
|
da14e33ff7 | ||
|
|
a7b46be923 | ||
|
|
40a2f093aa | ||
|
|
8ee2788baf | ||
|
|
3df3741922 | ||
|
|
96904b1826 | ||
|
|
1f14758e0c | ||
|
|
52c402faea | ||
|
|
e978253896 | ||
|
|
8df1ac9920 | ||
|
|
00e2f39ea1 | ||
|
|
b5ff309bf9 | ||
|
|
6e10d258b6 | ||
|
|
aafb291ce2 | ||
|
|
51ddd6595c | ||
|
|
bb8ed98a85 | ||
|
|
c49c5576d0 | ||
|
|
8cbefcc081 | ||
|
|
932980e91f | ||
|
|
2c8eb8a1ec |
@@ -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",
|
||||
|
||||
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@@ -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
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "typings"]
|
||||
path = typings
|
||||
url = https://github.com/zajrik/discord.js-typings
|
||||
@@ -24,3 +24,5 @@ webpack/
|
||||
webpack.config.js
|
||||
.github/
|
||||
test/
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
- "7"
|
||||
- "8"
|
||||
- "10"
|
||||
- "12"
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
51
README.md
51
README.md
@@ -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
|
||||
|
||||
163
docs/examples/attachments.md
Normal file
163
docs/examples/attachments.md
Normal 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:
|
||||
|
||||

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

|
||||
|
||||
## 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:
|
||||
|
||||

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

|
||||
@@ -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
38
docs/examples/embed.js
Normal 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');
|
||||
@@ -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
145
docs/examples/moderation.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
70
package.json
70
package.json
@@ -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,
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ class GuildEmojiDeleteAction extends Action {
|
||||
handle(emoji) {
|
||||
const client = this.client;
|
||||
client.dataManager.killEmoji(emoji);
|
||||
emoji.deleted = true;
|
||||
return { emoji };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
29
src/client/actions/InviteCreate.js
Normal file
29
src/client/actions/InviteCreate.js
Normal 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;
|
||||
25
src/client/actions/InviteDelete.js
Normal file
25
src/client/actions/InviteDelete.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
27
src/client/actions/MessageReactionRemoveEmoji.js
Normal file
27
src/client/actions/MessageReactionRemoveEmoji.js
Normal 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;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
34
src/client/voice/opus/DiscordJsOpusEngine.js
Normal file
34
src/client/voice/opus/DiscordJsOpusEngine.js
Normal 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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
16
src/client/voice/util/Silence.js
Normal file
16
src/client/voice/util/Silence.js
Normal 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;
|
||||
17
src/client/voice/util/SingleSilence.js
Normal file
17
src/client/voice/util/SingleSilence.js
Normal 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;
|
||||
@@ -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))));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
11
src/client/websocket/packets/handlers/InviteCreate.js
Normal file
11
src/client/websocket/packets/handlers/InviteCreate.js
Normal 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;
|
||||
11
src/client/websocket/packets/handlers/InviteDelete.js
Normal file
11
src/client/websocket/packets/handlers/InviteDelete.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
19
src/client/websocket/packets/handlers/WebhooksUpdate.js
Normal file
19
src/client/websocket/packets/handlers/WebhooksUpdate.js
Normal 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;
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
151
src/structures/Integration.js
Normal file
151
src/structures/Integration.js
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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--;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
24
src/structures/NewsChannel.js
Normal file
24
src/structures/NewsChannel.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user