diff --git a/.eslintrc.json b/.eslintrc.json
index 90f35dc06..e477d36c6 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -14,17 +14,20 @@
"valid-jsdoc": ["error", {
"requireReturn": false,
"requireReturnDescription": false,
+ "prefer": {
+ "return": "returns",
+ "arg": "param"
+ },
"preferType": {
"String": "string",
"Number": "number",
"Boolean": "boolean",
- "Function": "function",
"object": "Object",
+ "function": "Function",
+ "array": "Array",
"date": "Date",
- "error": "Error"
- },
- "prefer": {
- "return": "returns"
+ "error": "Error",
+ "null": "void"
}
}],
@@ -80,6 +83,7 @@
"consistent-this": ["error", "$this"],
"eol-last": "error",
"func-names": "error",
+ "func-name-matching": "error",
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"indent": ["error", 2, { "SwitchCase": 1 }],
"key-spacing": "error",
@@ -122,6 +126,7 @@
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"prefer-arrow-callback": "error",
+ "prefer-numeric-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
diff --git a/.gitignore b/.gitignore
index dee8a5889..c6e423898 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,10 +8,14 @@ logs/
# Authentication
test/auth.json
+test/auth.js
docs/deploy/deploy_key
docs/deploy/deploy_key.pub
+deploy/deploy_key
+deploy/deploy_key.pub
# Miscellaneous
.tmp/
.vscode/
docs/docs.json
+webpack/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..d5aa0ecce
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "typings"]
+ path = typings
+ url = https://github.com/zajrik/discord.js-typings
diff --git a/.tern-project b/.tern-project
new file mode 100644
index 000000000..cc31d86e5
--- /dev/null
+++ b/.tern-project
@@ -0,0 +1,11 @@
+{
+ "ecmaVersion": 6,
+ "libs": [],
+ "plugins": {
+ "node": {
+ "dontLoad": "node_modules/**",
+ "load": "",
+ "modules": ""
+ }
+ }
+}
diff --git a/.travis.yml b/.travis.yml
index 310a7c4b1..06b247f7f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,9 +5,12 @@ cache:
directories:
- node_modules
install: npm install
-script: bash ./docs/deploy/deploy.sh
+script:
+ - npm run test
+ - bash ./deploy/deploy.sh
env:
global:
- ENCRYPTION_LABEL: "af862fa96d3e"
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
-
+dist: trusty
+sudo: false
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4d1c83166..8b56f1c1a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,7 +6,7 @@ is a great boon to your coding process.
## Setup
To get ready to work on the codebase, please do the following:
-1. Fork & clone the repository
+1. Fork & clone the repository, and make sure you're on the **indev** branch
2. Run `npm install`
3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript`
4. Code your heart out!
diff --git a/README.md b/README.md
index a0909b7d6..eae457c31 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
+
-
+
+
@@ -15,21 +17,31 @@
## About
-discord.js is a powerful node.js module that allows you to interact with the [Discord API](https://discordapp.com/developers/docs/intro) very easily.
-It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.
-Usability and performance are key focuses of discord.js. It also has nearly 100% coverage of the Discord API.
+discord.js is a powerful node.js module that allows you to interact with the
+[Discord API](https://discordapp.com/developers/docs/intro) very easily.
+
+- Object-oriented
+- Predictable abstractions
+- Performant
+- Nearly 100% coverage of the Discord API
## Installation
-**Node.js 6.0.0 or newer is required.**
+**Node.js 6.0.0 or newer is required.**
+Ignore any warnings about unmet peer dependencies - all peer dependencies are 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`
+### 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 on Windows, since getting node-opus to build there can be a bit of a challenge.
+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.
+### Optional packages
+- [uws](https://www.npmjs.com/package/uws) for much a much faster WebSocket connection (`npm install uws --save`)
+- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack --save`)
+
## Example Usage
```js
const Discord = require('discord.js');
@@ -50,21 +62,27 @@ client.login('your token');
A bot template using discord.js can be generated using [generator-discordbot](https://www.npmjs.com/package/generator-discordbot).
+## Web distributions
+Web builds of discord.js that are fully capable of running in browsers are available [here](https://github.com/hydrabolt/discord.js/tree/webpack).
+These are built using [Webpack 2](https://webpack.js.org/). The API is identical, but rather than using `require('discord.js')`,
+the entire `Discord` object is available as a global (on the `window` object).
+The ShardingManager and any voice-related functionality is unavailable in these builds.
+
## Links
-* [Website](http://discord.js.org/)
+* [Website](https://discord.js.org/)
* [Discord.js server](https://discord.gg/bRCvFy9)
* [Discord API server](https://discord.gg/rV4BwdK)
-* [Documentation](http://discord.js.org/#!/docs)
+* [Documentation](https://discord.js.org/#/docs)
* [Legacy (v8) documentation](http://discordjs.readthedocs.io/en/8.2.0/docs_client.html)
-* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
+* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/examples)
* [GitHub](https://github.com/hydrabolt/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js)
-* [Related libraries](https://discordapi.com/unofficial/libs.html)
+* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc))
## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
-[documentation](http://discord.js.org/#!/docs).
-See [the contributing guide](CONTRIBUTING.md) if you'd like to submit a PR.
+[documentation](https://discord.js.org/#/docs).
+See [the contribution guide](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
diff --git a/docs/deploy/deploy.sh b/deploy/deploy.sh
similarity index 60%
rename from docs/deploy/deploy.sh
rename to deploy/deploy.sh
index fa0cdfa70..cbd644ed9 100644
--- a/docs/deploy/deploy.sh
+++ b/deploy/deploy.sh
@@ -4,7 +4,11 @@
set -e
function build {
- node docs/generator/generator.js
+ # Build docs
+ npm run docs
+
+ # Build the webpack
+ VERSIONED=false npm run web-dist
}
# Ignore Travis checking PRs
@@ -15,7 +19,9 @@ if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
fi
# Ignore travis checking other branches irrelevant to users
-if [ "$TRAVIS_BRANCH" != "master" -a "$TRAVIS_BRANCH" != "indev" ]; then
+# Apparently Travis considers tag builds as separate branches so we need to
+# check for that separately
+if [ "$TRAVIS_BRANCH" != "master" -a "$TRAVIS_BRANCH" != "indev" -a "$TRAVIS_BRANCH" != "$TRAVIS_TAG" ]; then
echo "deploy.sh: Ignoring push to another branch than master/indev"
build
exit 0
@@ -29,19 +35,27 @@ if [ -n "$TRAVIS_TAG" ]; then
SOURCE=$TRAVIS_TAG
fi
+# Initialise some useful variables
REPO=`git config remote.origin.url`
SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:}
SHA=`git rev-parse --verify HEAD`
-TARGET_BRANCH="docs"
+# Decrypt and add the ssh key
+ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key"
+ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv"
+ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR}
+ENCRYPTED_IV=${!ENCRYPTED_IV_VAR}
+openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in deploy/deploy_key.enc -out deploy_key -d
+chmod 600 deploy_key
+eval `ssh-agent -s`
+ssh-add deploy_key
+
+# Build everything
+build
# Checkout the repo in the target branch so we can build docs and push to it
+TARGET_BRANCH="docs"
git clone $REPO out -b $TARGET_BRANCH
-cd out
-cd ..
-
-# Build the docs
-build
# Move the generated JSON file to the newly-checked-out repo, to be committed
# and pushed
@@ -49,20 +63,28 @@ mv docs/docs.json out/$SOURCE.json
# Commit and push
cd out
+git add .
git config user.name "Travis CI"
git config user.email "$COMMIT_AUTHOR_EMAIL"
-
-git add .
-git commit -m "Docs build: ${SHA}"
-
-ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key"
-ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv"
-ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR}
-ENCRYPTED_IV=${!ENCRYPTED_IV_VAR}
-openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in ../docs/deploy/deploy_key.enc -out deploy_key -d
-chmod 600 deploy_key
-eval `ssh-agent -s`
-ssh-add deploy_key
-
-# Now that we're all set up, we can push.
+git commit -m "Docs build: ${SHA}" || true
+git push $SSH_REPO $TARGET_BRANCH
+
+# Clean up...
+cd ..
+rm -rf out
+
+# ...then do the same once more for the webpack
+TARGET_BRANCH="webpack"
+git clone $REPO out -b $TARGET_BRANCH
+
+# Move the generated webpack over
+mv webpack/discord.js out/discord.$SOURCE.js
+mv webpack/discord.min.js out/discord.$SOURCE.min.js
+
+# Commit and push
+cd out
+git add .
+git config user.name "Travis CI"
+git config user.email "$COMMIT_AUTHOR_EMAIL"
+git commit -m "Webpack build: ${SHA}" || true
git push $SSH_REPO $TARGET_BRANCH
diff --git a/docs/deploy/deploy_key.enc b/deploy/deploy_key.enc
similarity index 100%
rename from docs/deploy/deploy_key.enc
rename to deploy/deploy_key.enc
diff --git a/docs/README.md b/docs/README.md
index 6369fd9e6..d41af8eaf 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,2 +1 @@
-# discord.js docs
-[View documentation here](http://discord.js.org/#!/docs)
+## [View the documentation here.](https://discord.js.org/#/docs)
diff --git a/docs/custom/avatar.js b/docs/custom/avatar.js
deleted file mode 100644
index b3a77ced7..000000000
--- a/docs/custom/avatar.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const fs = require('fs');
-
-module.exports = {
- category: 'Examples',
- name: 'Avatars',
- data:
-`\`\`\`js
-${fs.readFileSync('./docs/custom/examples/avatar.js').toString('utf-8')}
-\`\`\``,
-};
diff --git a/docs/custom/faq.js b/docs/custom/faq.js
deleted file mode 100644
index abb06946a..000000000
--- a/docs/custom/faq.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const fs = require('fs');
-
-module.exports = {
- category: 'General',
- name: 'FAQ',
- data: fs.readFileSync('./docs/custom/documents/faq.md').toString('utf-8'),
-};
diff --git a/docs/custom/index.js b/docs/custom/index.js
deleted file mode 100644
index a076c4619..000000000
--- a/docs/custom/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const files = [
- require('./welcome'),
- require('./updating'),
- require('./faq'),
- require('./ping_pong'),
- require('./avatar'),
-];
-
-const categories = {};
-for (const file of files) {
- file.category = file.category.toLowerCase();
- if (!categories[file.category]) {
- categories[file.category] = [];
- }
- categories[file.category].push(file);
-}
-
-module.exports = categories;
diff --git a/docs/custom/ping_pong.js b/docs/custom/ping_pong.js
deleted file mode 100644
index c6952ad6c..000000000
--- a/docs/custom/ping_pong.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const fs = require('fs');
-
-module.exports = {
- category: 'Examples',
- name: 'Ping Pong',
- data:
-`\`\`\`js
-${fs.readFileSync('./docs/custom/examples/ping_pong.js').toString('utf-8')}
-\`\`\``,
-};
diff --git a/docs/custom/updating.js b/docs/custom/updating.js
deleted file mode 100644
index 1615d95cd..000000000
--- a/docs/custom/updating.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const fs = require('fs');
-
-module.exports = {
- category: 'General',
- name: 'Updating your code',
- data: fs.readFileSync('./docs/custom/documents/updating.md').toString('utf-8'),
-};
diff --git a/docs/custom/webhook.js b/docs/custom/webhook.js
deleted file mode 100644
index 10f57f361..000000000
--- a/docs/custom/webhook.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const fs = require('fs');
-
-module.exports = {
- category: 'Examples',
- name: 'Webhooks',
- data:
-`\`\`\`js
-${fs.readFileSync('./docs/custom/examples/webhook.js').toString('utf-8')}
-\`\`\``,
-};
diff --git a/docs/custom/welcome.js b/docs/custom/welcome.js
deleted file mode 100644
index 809093e7e..000000000
--- a/docs/custom/welcome.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const fs = require('fs');
-
-module.exports = {
- category: 'General',
- name: 'Welcome',
- data: fs.readFileSync('./docs/custom/documents/welcome.md').toString('utf-8'),
-};
diff --git a/docs/custom/examples/avatar.js b/docs/examples/avatars.js
similarity index 100%
rename from docs/custom/examples/avatar.js
rename to docs/examples/avatars.js
diff --git a/docs/custom/examples/ping_pong.js b/docs/examples/ping.js
similarity index 100%
rename from docs/custom/examples/ping_pong.js
rename to docs/examples/ping.js
diff --git a/docs/custom/examples/webhook.js b/docs/examples/webhook.js
similarity index 100%
rename from docs/custom/examples/webhook.js
rename to docs/examples/webhook.js
diff --git a/docs/custom/documents/faq.md b/docs/general/faq.md
similarity index 69%
rename from docs/custom/documents/faq.md
rename to docs/general/faq.md
index 82a94f656..d7e4188b8 100644
--- a/docs/custom/documents/faq.md
+++ b/docs/general/faq.md
@@ -9,13 +9,15 @@ 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, but is tougher to get working on Windows.
+ node-opus is greatly preferred, due to it having significantly better performance.
## How do I install FFMPEG?
-- **Ubuntu 16.04:** `sudo apt install ffpmeg`
+- **npm:** `npm install --save 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).
## How do I set up node-opus?
- **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats!
-- **Windows:** See [AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md). Good luck.
+- **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!
diff --git a/docs/custom/documents/updating.md b/docs/general/updating.md
similarity index 97%
rename from docs/custom/documents/updating.md
rename to docs/general/updating.md
index 9efe1c8f8..2926691ab 100644
--- a/docs/custom/documents/updating.md
+++ b/docs/general/updating.md
@@ -1,128 +1,128 @@
-# Version 10
-Version 10's non-BC changes focus on cleaning up some inconsistencies that exist in previous versions.
-Upgrading from v9 should be quick and painless.
-
-## Client options
-All client options have been converted to camelCase rather than snake_case, and `max_message_cache` was renamed to `messageCacheMaxSize`.
-
-v9 code example:
-```js
-const client = new Discord.Client({
- disable_everyone: true,
- max_message_cache: 500,
- message_cache_lifetime: 120,
- message_sweep_interval: 60
-});
-```
-
-v10 code example:
-```js
-const client = new Discord.Client({
- disableEveryone: true,
- messageCacheMaxSize: 500,
- messageCacheLifetime: 120,
- messageSweepInterval: 60
-});
-```
-
-## Presences
-Presences have been completely restructured.
-Previous versions of discord.js assumed that users had the same presence amongst all guilds - with the introduction of sharding, however, this is no longer the case.
-
-v9 discord.js code may look something like this:
-```js
-User.status; // the status of the user
-User.game; // the game that the user is playing
-ClientUser.setStatus(status, game, url); // set the new status for the user
-```
-
-v10 moves presences to GuildMember instances. For the sake of simplicity, though, User classes also expose presences.
-When accessing a presence on a User object, it simply finds the first GuildMember for the user, and uses its presence.
-Additionally, the introduction of the Presence class keeps all of the presence data organised.
-
-**It is strongly recommended that you use a GuildMember's presence where available, rather than a User.
-A user may have an entirely different presence between two different guilds.**
-
-v10 code:
-```js
-MemberOrUser.presence.status; // the status of the member or user
-MemberOrUser.presence.game; // the game that the member or user is playing
-ClientUser.setStatus(status); // online, idle, dnd, offline
-ClientUser.setGame(game, streamingURL); // a game
-ClientUser.setPresence(fullPresence); // status and game combined
-```
-
-## Voice
-Voice has been rewritten internally, but in a backwards-compatible manner.
-There is only one breaking change here; the `disconnected` event was renamed to `disconnect`.
-Several more events have been made available to a VoiceConnection, so see the documentation.
-
-## Events
-Many events have been renamed or had their arguments change.
-
-### Client events
-| Version 9 | Version 10 |
-|------------------------------------------------------|-----------------------------------------------|
-| guildMemberAdd(guild, member) | guildMemberAdd(member) |
-| guildMemberAvailable(guild, member) | guildMemberAvailable(member) |
-| guildMemberRemove(guild, member) | guildMemberRemove(member) |
-| guildMembersChunk(guild, members) | guildMembersChunk(members) |
-| guildMemberUpdate(guild, oldMember, newMember) | guildMemberUpdate(oldMember, newMember) |
-| guildRoleCreate(guild, role) | roleCreate(role) |
-| guildRoleDelete(guild, role) | roleDelete(role) |
-| guildRoleUpdate(guild, oldRole, newRole) | roleUpdate(oldRole, newRole) |
-
-The guild parameter that has been dropped from the guild-related events can still be derived using `member.guild` or `role.guild`.
-
-### VoiceConnection events
-| Version 9 | Version 10 |
-|--------------|------------|
-| disconnected | disconnect |
-
-## Dates and timestamps
-All dates/timestamps on the structures have been refactored to have a consistent naming scheme and availability.
-All of them are named similarly to this:
-**Date:** `Message.createdAt`
-**Timestamp:** `Message.createdTimestamp`
-See the docs for each structure to see which date/timestamps are available on them.
-
-
-# Version 9
-The version 9 (v9) rewrite takes a much more object-oriented approach than previous versions,
-which allows your code to be much more readable and manageable.
-It's been rebuilt from the ground up and should be much more stable, fixing caching issues that affected
-older versions. It also has support for newer Discord Features, such as emojis.
-
-Version 9, while containing a sizable number of breaking changes, does not require much change in your code's logic -
-most of the concepts are still the same, but loads of functions have been moved around.
-The vast majority of methods you're used to using have been moved out of the Client class,
-into other more relevant classes where they belong.
-Because of this, you will need to convert most of your calls over to the new methods.
-
-Here are a few examples of methods that have changed:
-* `Client.sendMessage(channel, message)` ==> `TextChannel.sendMessage(message)`
- * `Client.sendMessage(user, message)` ==> `User.sendMessage(message)`
-* `Client.updateMessage(message, "New content")` ==> `Message.edit("New Content")`
-* `Client.getChannelLogs(channel, limit)` ==> `TextChannel.fetchMessages({options})`
-* `Server.detailsOfUser(User)` ==> `Server.members.get(User).properties` (retrieving a member gives a GuildMember object)
-* `Client.joinVoiceChannel(voicechannel)` => `VoiceChannel.join()`
-
-A couple more important details:
-* `Client.loginWithToken("token")` ==> `client.login("token")`
-* `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`)
-
-## No more callbacks!
-Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
-For example, the following code:
-
-```js
-client.getChannelLogs(channel, 100, function(messages) {
- console.log(`${messages.length} messages found`);
-});
-```
-
-```js
-channel.fetchMessages({limit: 100}).then(messages => {
- console.log(`${messages.size} messages found`);
-});
-```
+# Version 10
+Version 10's non-BC changes focus on cleaning up some inconsistencies that exist in previous versions.
+Upgrading from v9 should be quick and painless.
+
+## Client options
+All client options have been converted to camelCase rather than snake_case, and `max_message_cache` was renamed to `messageCacheMaxSize`.
+
+v9 code example:
+```js
+const client = new Discord.Client({
+ disable_everyone: true,
+ max_message_cache: 500,
+ message_cache_lifetime: 120,
+ message_sweep_interval: 60
+});
+```
+
+v10 code example:
+```js
+const client = new Discord.Client({
+ disableEveryone: true,
+ messageCacheMaxSize: 500,
+ messageCacheLifetime: 120,
+ messageSweepInterval: 60
+});
+```
+
+## Presences
+Presences have been completely restructured.
+Previous versions of discord.js assumed that users had the same presence amongst all guilds - with the introduction of sharding, however, this is no longer the case.
+
+v9 discord.js code may look something like this:
+```js
+User.status; // the status of the user
+User.game; // the game that the user is playing
+ClientUser.setStatus(status, game, url); // set the new status for the user
+```
+
+v10 moves presences to GuildMember instances. For the sake of simplicity, though, User classes also expose presences.
+When accessing a presence on a User object, it simply finds the first GuildMember for the user, and uses its presence.
+Additionally, the introduction of the Presence class keeps all of the presence data organised.
+
+**It is strongly recommended that you use a GuildMember's presence where available, rather than a User.
+A user may have an entirely different presence between two different guilds.**
+
+v10 code:
+```js
+MemberOrUser.presence.status; // the status of the member or user
+MemberOrUser.presence.game; // the game that the member or user is playing
+ClientUser.setStatus(status); // online, idle, dnd, offline
+ClientUser.setGame(game, streamingURL); // a game
+ClientUser.setPresence(fullPresence); // status and game combined
+```
+
+## Voice
+Voice has been rewritten internally, but in a backwards-compatible manner.
+There is only one breaking change here; the `disconnected` event was renamed to `disconnect`.
+Several more events have been made available to a VoiceConnection, so see the documentation.
+
+## Events
+Many events have been renamed or had their arguments change.
+
+### Client events
+| Version 9 | Version 10 |
+|------------------------------------------------------|-----------------------------------------------|
+| guildMemberAdd(guild, member) | guildMemberAdd(member) |
+| guildMemberAvailable(guild, member) | guildMemberAvailable(member) |
+| guildMemberRemove(guild, member) | guildMemberRemove(member) |
+| guildMembersChunk(guild, members) | guildMembersChunk(members) |
+| guildMemberUpdate(guild, oldMember, newMember) | guildMemberUpdate(oldMember, newMember) |
+| guildRoleCreate(guild, role) | roleCreate(role) |
+| guildRoleDelete(guild, role) | roleDelete(role) |
+| guildRoleUpdate(guild, oldRole, newRole) | roleUpdate(oldRole, newRole) |
+
+The guild parameter that has been dropped from the guild-related events can still be derived using `member.guild` or `role.guild`.
+
+### VoiceConnection events
+| Version 9 | Version 10 |
+|--------------|------------|
+| disconnected | disconnect |
+
+## Dates and timestamps
+All dates/timestamps on the structures have been refactored to have a consistent naming scheme and availability.
+All of them are named similarly to this:
+**Date:** `Message.createdAt`
+**Timestamp:** `Message.createdTimestamp`
+See the docs for each structure to see which date/timestamps are available on them.
+
+
+# Version 9
+The version 9 (v9) rewrite takes a much more object-oriented approach than previous versions,
+which allows your code to be much more readable and manageable.
+It's been rebuilt from the ground up and should be much more stable, fixing caching issues that affected
+older versions. It also has support for newer Discord Features, such as emojis.
+
+Version 9, while containing a sizable number of breaking changes, does not require much change in your code's logic -
+most of the concepts are still the same, but loads of functions have been moved around.
+The vast majority of methods you're used to using have been moved out of the Client class,
+into other more relevant classes where they belong.
+Because of this, you will need to convert most of your calls over to the new methods.
+
+Here are a few examples of methods that have changed:
+* `Client.sendMessage(channel, message)` ==> `TextChannel.sendMessage(message)`
+ * `Client.sendMessage(user, message)` ==> `User.sendMessage(message)`
+* `Client.updateMessage(message, "New content")` ==> `Message.edit("New Content")`
+* `Client.getChannelLogs(channel, limit)` ==> `TextChannel.fetchMessages({options})`
+* `Server.detailsOfUser(User)` ==> `Server.members.get(User).properties` (retrieving a member gives a GuildMember object)
+* `Client.joinVoiceChannel(voicechannel)` => `VoiceChannel.join()`
+
+A couple more important details:
+* `Client.loginWithToken("token")` ==> `client.login("token")`
+* `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`)
+
+## No more callbacks!
+Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
+For example, the following code:
+
+```js
+client.getChannelLogs(channel, 100, function(messages) {
+ console.log(`${messages.length} messages found`);
+});
+```
+
+```js
+channel.fetchMessages({limit: 100}).then(messages => {
+ console.log(`${messages.size} messages found`);
+});
+```
diff --git a/docs/custom/documents/welcome.md b/docs/general/welcome.md
similarity index 65%
rename from docs/custom/documents/welcome.md
rename to docs/general/welcome.md
index b5215178e..cb72c85a9 100644
--- a/docs/custom/documents/welcome.md
+++ b/docs/general/welcome.md
@@ -1,54 +1,72 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-# Welcome!
-Welcome to the discord.js v10 documentation.
-v10 is just a more consistent and stable iteration over v9, and contains loads of new and improved features, optimisations, and bug fixes.
-
-## About
-discord.js is a powerful node.js module that allows you to interact with the [Discord API](https://discordapp.com/developers/docs/intro) very easily.
-It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.
-Usability and performance are key focuses of discord.js. It also has nearly 100% coverage of the Discord API.
-
-## Installation
-**Node.js 6.0.0 or newer is required.**
-
-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`
-
-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 on Windows, since getting node-opus to build there can be a bit of a challenge.
-For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
-
-## Guides
-* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/)
-* [York's v9 upgrade guide](https://yorkaargh.wordpress.com/2016/09/03/updating-discord-js-bots/)
-
-## Links
-* [Website](http://discord.js.org/)
-* [Discord.js server](https://discord.gg/bRCvFy9)
-* [Discord API server](https://discord.gg/rV4BwdK)
-* [Documentation](http://discord.js.org/#!/docs)
-* [Legacy (v8) documentation](http://discordjs.readthedocs.io/en/8.2.0/docs_client.html)
-* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
-* [GitHub](https://github.com/hydrabolt/discord.js)
-* [NPM](https://www.npmjs.com/package/discord.js)
-* [Related libraries](https://discordapi.com/unofficial/libs.html)
-
-## Help
-If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
-nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# Welcome!
+Welcome to the discord.js v10 documentation.
+v10 is just a more consistent and stable iteration over v9, and contains loads of new and improved features, optimisations, and bug fixes.
+
+## About
+discord.js is a powerful node.js module that allows you to interact with the
+[Discord API](https://discordapp.com/developers/docs/intro) very easily.
+
+- Object-oriented
+- Predictable abstractions
+- Performant
+- Nearly 100% coverage of the Discord API
+
+## Installation
+**Node.js 6.0.0 or newer is required.**
+Ignore any warnings about unmet peer dependencies - all of them are 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`
+
+### 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.
+
+### Optional packages
+- [uws](https://www.npmjs.com/package/uws) for much a much faster WebSocket connection (`npm install uws --save`)
+- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack --save`)
+
+## Web distributions
+Web builds of discord.js that are fully capable of running in browsers are available [here](https://github.com/hydrabolt/discord.js/tree/webpack).
+These are built by [Webpack 2](https://webpack.js.org/). The API is identical, but rather than using `require('discord.js')`,
+the entire `Discord` object is available as a global (on the `window` object).
+The ShardingManager and any voice-related functionality is unavailable in these builds.
+
+## Guides
+* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/)
+* [York's v9 upgrade guide](https://yorkaargh.wordpress.com/2016/09/03/updating-discord-js-bots/)
+
+## Links
+* [Website](https://discord.js.org/)
+* [Discord.js server](https://discord.gg/bRCvFy9)
+* [Discord API server](https://discord.gg/rV4BwdK)
+* [Documentation](https://discord.js.org/#/docs)
+* [Legacy (v8) documentation](http://discordjs.readthedocs.io/en/8.2.0/docs_client.html)
+* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/examples)
+* [GitHub](https://github.com/hydrabolt/discord.js)
+* [NPM](https://www.npmjs.com/package/discord.js)
+* [Related libraries](https://discordapi.com/unofficial/libs.html)
+
+## Help
+If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
+nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
diff --git a/docs/generator/config.json b/docs/generator/config.json
deleted file mode 100644
index 72bda7152..000000000
--- a/docs/generator/config.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "GEN_VERSION": 13,
- "COMPRESS": false
-}
\ No newline at end of file
diff --git a/docs/generator/documentation.js b/docs/generator/documentation.js
deleted file mode 100644
index f5dba5b1c..000000000
--- a/docs/generator/documentation.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/* eslint-disable no-console */
-const DocumentedClass = require('./types/DocumentedClass');
-const DocumentedInterface = require('./types/DocumentedInterface');
-const DocumentedTypeDef = require('./types/DocumentedTypeDef');
-const DocumentedConstructor = require('./types/DocumentedConstructor');
-const DocumentedMember = require('./types/DocumentedMember');
-const DocumentedFunction = require('./types/DocumentedFunction');
-const DocumentedEvent = require('./types/DocumentedEvent');
-const GEN_VERSION = require('./config').GEN_VERSION;
-
-class Documentation {
- constructor(items, custom) {
- this.classes = new Map();
- this.interfaces = new Map();
- this.typedefs = new Map();
- this.custom = custom;
- this.parse(items);
- }
-
- registerRoots(data) {
- for (const item of data) {
- switch (item.kind) {
- case 'class':
- this.classes.set(item.name, new DocumentedClass(this, item));
- break;
- case 'interface':
- this.interfaces.set(item.name, new DocumentedInterface(this, item));
- break;
- case 'typedef':
- this.typedefs.set(item.name, new DocumentedTypeDef(this, item));
- break;
- default:
- break;
- }
- }
- }
-
- findParent(item) {
- if (['constructor', 'member', 'function', 'event'].includes(item.kind)) {
- let val = this.classes.get(item.memberof);
- if (val) return val;
- val = this.interfaces.get(item.memberof);
- if (val) return val;
- }
- return null;
- }
-
- parse(items) {
- this.registerRoots(items.filter(item => ['class', 'interface', 'typedef'].includes(item.kind)));
- const members = items.filter(item => !['class', 'interface', 'typedef'].includes(item.kind));
- const unknowns = new Map();
-
- for (const member of members) {
- let item;
- switch (member.kind) {
- case 'constructor':
- item = new DocumentedConstructor(this, member);
- break;
- case 'member':
- item = new DocumentedMember(this, member);
- break;
- case 'function':
- item = new DocumentedFunction(this, member);
- break;
- case 'event':
- item = new DocumentedEvent(this, member);
- break;
- default:
- unknowns.set(member.kind, member);
- continue;
- }
-
- const parent = this.findParent(member);
- if (!parent) {
- console.warn(`- "${member.name || member.directData.name}" has no accessible parent.`);
- continue;
- }
- parent.add(item);
- }
- for (const [key, val] of unknowns) {
- console.warn(`- Unknown documentation kind "${key}" - \n${JSON.stringify(val)}\n`);
- }
- }
-
- serialize() {
- const meta = {
- version: GEN_VERSION,
- date: Date.now(),
- };
- const serialized = {
- meta,
- classes: Array.from(this.classes.values()).map(c => c.serialize()),
- interfaces: Array.from(this.interfaces.values()).map(i => i.serialize()),
- typedefs: Array.from(this.typedefs.values()).map(t => t.serialize()),
- custom: this.custom,
- };
- return serialized;
- }
-}
-
-module.exports = Documentation;
diff --git a/docs/generator/generator.js b/docs/generator/generator.js
deleted file mode 100644
index f9c314ec6..000000000
--- a/docs/generator/generator.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/* eslint-disable no-console */
-const fs = require('fs-extra');
-const zlib = require('zlib');
-const jsdoc2md = require('jsdoc-to-markdown');
-const Documentation = require('./documentation');
-const custom = require('../custom/index');
-const config = require('./config');
-
-process.on('unhandledRejection', console.error);
-
-console.log(`Using format version ${config.GEN_VERSION}.`);
-console.log('Parsing JSDocs in source files...');
-
-jsdoc2md.getTemplateData({ files: [`./src/*.js`, `./src/**/*.js`] }).then(data => {
- console.log(`${data.length} items found.`);
- const documentation = new Documentation(data, custom);
- console.log('Serializing...');
- let output = JSON.stringify(documentation.serialize(), null, 0);
- if (config.compress) {
- console.log('Compressing...');
- output = zlib.deflateSync(output).toString('utf8');
- }
- if (!process.argv.slice(2).includes('silent')) {
- console.log('Writing to docs.json...');
- fs.writeFileSync('./docs/docs.json', output);
- }
- console.log('Done!');
- process.exit(0);
-}).catch(console.error);
diff --git a/docs/generator/types/DocumentedClass.js b/docs/generator/types/DocumentedClass.js
deleted file mode 100644
index 323afafba..000000000
--- a/docs/generator/types/DocumentedClass.js
+++ /dev/null
@@ -1,83 +0,0 @@
-const DocumentedItem = require('./DocumentedItem');
-const DocumentedItemMeta = require('./DocumentedItemMeta');
-const DocumentedConstructor = require('./DocumentedConstructor');
-const DocumentedFunction = require('./DocumentedFunction');
-const DocumentedMember = require('./DocumentedMember');
-const DocumentedEvent = require('./DocumentedEvent');
-
-/*
-{ id: 'VoiceChannel',
- longname: 'VoiceChannel',
- name: 'VoiceChannel',
- scope: 'global',
- kind: 'class',
- augments: [ 'GuildChannel' ],
- description: 'Represents a Server Voice Channel on Discord.',
- meta:
- { lineno: 7,
- filename: 'VoiceChannel.js',
- path: 'src/structures' },
- order: 232 }
- */
-
-class DocumentedClass extends DocumentedItem {
-
- constructor(docParent, data) {
- super(docParent, data);
- this.props = new Map();
- this.methods = new Map();
- this.events = new Map();
- }
-
- add(item) {
- if (item instanceof DocumentedConstructor) {
- if (this.classConstructor) {
- throw new Error(`Doc ${this.directData.name} already has constructor - ${this.directData.classConstructor}`);
- }
- this.classConstructor = item;
- } else if (item instanceof DocumentedFunction) {
- if (this.methods.get(item.directData.name)) {
- throw new Error(`Doc ${this.directData.name} already has method ${item.directData.name}`);
- }
- this.methods.set(item.directData.name, item);
- } else if (item instanceof DocumentedMember) {
- if (this.props.get(item.directData.name)) {
- throw new Error(`Doc ${this.directData.name} already has prop ${item.directData.name}`);
- }
- this.props.set(item.directData.name, item);
- } else if (item instanceof DocumentedEvent) {
- if (this.events.get(item.directData.name)) {
- throw new Error(`Doc ${this.directData.name} already has event ${item.directData.name}`);
- }
- this.events.set(item.directData.name, item);
- }
- }
-
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.directData = data;
- this.directData.meta = new DocumentedItemMeta(this, data.meta);
- }
-
- serialize() {
- super.serialize();
- const { id, name, description, meta, augments, access } = this.directData;
- const serialized = {
- id,
- name,
- description,
- meta: meta.serialize(),
- extends: augments,
- access,
- };
- if (this.classConstructor) {
- serialized.classConstructor = this.classConstructor.serialize();
- }
- serialized.methods = Array.from(this.methods.values()).map(m => m.serialize());
- serialized.properties = Array.from(this.props.values()).map(p => p.serialize());
- serialized.events = Array.from(this.events.values()).map(e => e.serialize());
- return serialized;
- }
-}
-
-module.exports = DocumentedClass;
diff --git a/docs/generator/types/DocumentedConstructor.js b/docs/generator/types/DocumentedConstructor.js
deleted file mode 100644
index e539bf642..000000000
--- a/docs/generator/types/DocumentedConstructor.js
+++ /dev/null
@@ -1,46 +0,0 @@
-const DocumentedItem = require('./DocumentedItem');
-const DocumentedParam = require('./DocumentedParam');
-
-/*
-{ id: 'Client()',
- longname: 'Client',
- name: 'Client',
- kind: 'constructor',
- description: 'Creates an instance of Client.',
- memberof: 'Client',
- params:
- [ { type: [Object],
- optional: true,
- description: 'options to pass to the client',
- name: 'options' } ],
- order: 10 }
-*/
-
-class DocumentedConstructor extends DocumentedItem {
-
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.directData = data;
- const newParams = [];
- for (const param of data.params) {
- newParams.push(new DocumentedParam(this, param));
- }
- this.directData.params = newParams;
- }
-
- serialize() {
- super.serialize();
- const { id, name, description, memberof, access, params } = this.directData;
- return {
- id,
- name,
- description,
- memberof,
- access,
- params: params.map(p => p.serialize()),
- };
- }
-
-}
-
-module.exports = DocumentedConstructor;
diff --git a/docs/generator/types/DocumentedEvent.js b/docs/generator/types/DocumentedEvent.js
deleted file mode 100644
index 0641b23b8..000000000
--- a/docs/generator/types/DocumentedEvent.js
+++ /dev/null
@@ -1,80 +0,0 @@
-const DocumentedItem = require('./DocumentedItem');
-const DocumentedItemMeta = require('./DocumentedItemMeta');
-const DocumentedParam = require('./DocumentedParam');
-
-/*
-{
- "id":"Client#event:guildMemberRolesUpdate",
- "longname":"Client#event:guildMemberRolesUpdate",
- "name":"guildMemberRolesUpdate",
- "scope":"instance",
- "kind":"event",
- "description":"Emitted whenever a Guild Member's Roles change - i.e. new role or removed role",
- "memberof":"Client",
- "params":[
- {
- "type":{
- "names":[
- "Guild"
- ]
- },
- "description":"the guild that the update affects",
- "name":"guild"
- },
- {
- "type":{
- "names":[
- "Array."
- ]
- },
- "description":"the roles before the update",
- "name":"oldRoles"
- },
- {
- "type":{
- "names":[
- "Guild"
- ]
- },
- "description":"the roles after the update",
- "name":"newRoles"
- }
- ],
- "meta":{
- "lineno":91,
- "filename":"Guild.js",
- "path":"src/structures"
- },
- "order":110
-}
-*/
-
-class DocumentedEvent extends DocumentedItem {
-
- registerMetaInfo(data) {
- this.directData = data;
- this.directData.meta = new DocumentedItemMeta(this, data.meta);
- const newParams = [];
- data.params = data.params || [];
- for (const param of data.params) {
- newParams.push(new DocumentedParam(this, param));
- }
- this.directData.params = newParams;
- }
-
- serialize() {
- super.serialize();
- const { id, name, description, memberof, meta, params } = this.directData;
- return {
- id,
- name,
- description,
- memberof,
- meta: meta.serialize(),
- params: params.map(p => p.serialize()),
- };
- }
-
-}
-
-module.exports = DocumentedEvent;
diff --git a/docs/generator/types/DocumentedFunction.js b/docs/generator/types/DocumentedFunction.js
deleted file mode 100644
index 698fb16b5..000000000
--- a/docs/generator/types/DocumentedFunction.js
+++ /dev/null
@@ -1,91 +0,0 @@
-const DocumentedItem = require('./DocumentedItem');
-const DocumentedItemMeta = require('./DocumentedItemMeta');
-const DocumentedVarType = require('./DocumentedVarType');
-const DocumentedParam = require('./DocumentedParam');
-
-/*
-{
- "id":"ClientUser#sendTTSMessage",
- "longname":"ClientUser#sendTTSMessage",
- "name":"sendTTSMessage",
- "scope":"instance",
- "kind":"function",
- "inherits":"User#sendTTSMessage",
- "inherited":true,
- "implements":[
- "TextBasedChannel#sendTTSMessage"
- ],
- "description":"Send a text-to-speech message to this channel",
- "memberof":"ClientUser",
- "params":[
- {
- "type":{
- "names":[
- "String"
- ]
- },
- "description":"the content to send",
- "name":"content"
- }
- ],
- "examples":[
- "// send a TTS message..."
- ],
- "returns":[
- {
- "type":{
- "names":[
- "Promise."
- ]
- }
- }
- ],
- "meta":{
- "lineno":38,
- "filename":"TextBasedChannel.js",
- "path":src/structures/interface"
- },
- "order":293
-}
- */
-
-class DocumentedFunction extends DocumentedItem {
-
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.directData = data;
- this.directData.meta = new DocumentedItemMeta(this, data.meta);
- this.directData.returns = new DocumentedVarType(this, data.returns ? data.returns[0].type : {
- names: ['null'],
- });
- const newParams = [];
- for (const param of data.params) {
- newParams.push(new DocumentedParam(this, param));
- }
- this.directData.params = newParams;
- }
-
- serialize() {
- super.serialize();
- const {
- id, name, description, memberof, examples, inherits, inherited, meta, returns, params, access,
- } = this.directData;
- const serialized = {
- id,
- access,
- name,
- description,
- memberof,
- examples,
- inherits,
- inherited,
- meta: meta.serialize(),
- returns: returns.serialize(),
- params: params.map(p => p.serialize()),
- };
- serialized.implements = this.directData.implements;
- return serialized;
- }
-}
-
-module.exports = DocumentedFunction;
diff --git a/docs/generator/types/DocumentedInterface.js b/docs/generator/types/DocumentedInterface.js
deleted file mode 100644
index ce2905785..000000000
--- a/docs/generator/types/DocumentedInterface.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const DocumentedClass = require('./DocumentedClass');
-
-/*
-{ id: 'TextBasedChannel',
- longname: 'TextBasedChannel',
- name: 'TextBasedChannel',
- scope: 'global',
- kind: 'interface',
- classdesc: 'Interface for classes that have text-channel-like features',
- params: [],
- meta:
- { lineno: 5,
- filename: 'TextBasedChannel.js',
- path: 'src/structures/interface' },
- order: 175 }
- */
-
-class DocumentedInterface extends DocumentedClass {
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.directData = data;
- // this.directData.meta = new DocumentedItemMeta(this, data.meta);
- }
-
- serialize() {
- const serialized = super.serialize();
- serialized.description = this.directData.classdesc;
- return serialized;
- }
-}
-
-module.exports = DocumentedInterface;
diff --git a/docs/generator/types/DocumentedItem.js b/docs/generator/types/DocumentedItem.js
deleted file mode 100644
index 562c706d3..000000000
--- a/docs/generator/types/DocumentedItem.js
+++ /dev/null
@@ -1,17 +0,0 @@
-class DocumentedItem {
- constructor(parent, info) {
- this.parent = parent;
- this.directData = {};
- this.registerMetaInfo(info);
- }
-
- registerMetaInfo() {
- return;
- }
-
- serialize() {
- return;
- }
-}
-
-module.exports = DocumentedItem;
diff --git a/docs/generator/types/DocumentedItemMeta.js b/docs/generator/types/DocumentedItemMeta.js
deleted file mode 100644
index 7b44941b6..000000000
--- a/docs/generator/types/DocumentedItemMeta.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const cwd = (`${process.cwd()}\\`).replace(/\\/g, '/');
-const backToForward = /\\/g;
-
-const DocumentedItem = require('./DocumentedItem');
-
-/*
- { lineno: 7,
- filename: 'VoiceChannel.js',
- path: 'src/structures' },
-*/
-
-class DocumentedItemMeta extends DocumentedItem {
-
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.directData.line = data.lineno;
- this.directData.file = data.filename;
- this.directData.path = data.path.replace(backToForward, '/').replace(cwd, '');
- }
-
- serialize() {
- super.serialize();
- const { line, file, path } = this.directData;
- return { line, file, path };
- }
-
-}
-
-module.exports = DocumentedItemMeta;
diff --git a/docs/generator/types/DocumentedMember.js b/docs/generator/types/DocumentedMember.js
deleted file mode 100644
index 3eaddc24e..000000000
--- a/docs/generator/types/DocumentedMember.js
+++ /dev/null
@@ -1,58 +0,0 @@
-const DocumentedItem = require('./DocumentedItem');
-const DocumentedItemMeta = require('./DocumentedItemMeta');
-const DocumentedVarType = require('./DocumentedVarType');
-const DocumentedParam = require('./DocumentedParam');
-
-/*
-{ id: 'Client#rest',
- longname: 'Client#rest',
- name: 'rest',
- scope: 'instance',
- kind: 'member',
- description: 'The REST manager of the client',
- memberof: 'Client',
- type: { names: [ 'RESTManager' ] },
- access: 'private',
- meta:
- { lineno: 32,
- filename: 'Client.js',
- path: 'src/client' },
- order: 11 }
-*/
-
-class DocumentedMember extends DocumentedItem {
-
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.directData = data;
- this.directData.meta = new DocumentedItemMeta(this, data.meta);
- this.directData.type = new DocumentedVarType(this, data.type);
- if (data.properties) {
- const newProps = [];
- for (const param of data.properties) {
- newProps.push(new DocumentedParam(this, param));
- }
- this.directData.properties = newProps;
- } else {
- data.properties = [];
- }
- }
-
- serialize() {
- super.serialize();
- const { id, name, description, memberof, type, access, meta, properties } = this.directData;
- return {
- id,
- name,
- description,
- memberof,
- type: type.serialize(),
- access,
- meta: meta.serialize(),
- props: properties.map(p => p.serialize()),
- };
- }
-
-}
-
-module.exports = DocumentedMember;
diff --git a/docs/generator/types/DocumentedParam.js b/docs/generator/types/DocumentedParam.js
deleted file mode 100644
index bb94d7e37..000000000
--- a/docs/generator/types/DocumentedParam.js
+++ /dev/null
@@ -1,36 +0,0 @@
-const DocumentedItem = require('./DocumentedItem');
-const DocumentedVarType = require('./DocumentedVarType');
-
-/*
-{
- "type":{
- "names":[
- "Guild"
- ]
- },
- "description":"the roles after the update",
- "name":"newRoles"
- }
-*/
-
-class DocumentedParam extends DocumentedItem {
-
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.directData = data;
- this.directData.type = new DocumentedVarType(this, data.type);
- }
-
- serialize() {
- super.serialize();
- const { name, description, type, optional } = this.directData;
- return {
- name,
- description,
- optional,
- type: type.serialize(),
- };
- }
-}
-
-module.exports = DocumentedParam;
diff --git a/docs/generator/types/DocumentedTypeDef.js b/docs/generator/types/DocumentedTypeDef.js
deleted file mode 100644
index dfabc0dfa..000000000
--- a/docs/generator/types/DocumentedTypeDef.js
+++ /dev/null
@@ -1,56 +0,0 @@
-const DocumentedItem = require('./DocumentedItem');
-const DocumentedItemMeta = require('./DocumentedItemMeta');
-const DocumentedVarType = require('./DocumentedVarType');
-const DocumentedParam = require('./DocumentedParam');
-
-/*
-{ id: 'StringResolvable',
- longname: 'StringResolvable',
- name: 'StringResolvable',
- scope: 'global',
- kind: 'typedef',
- description: 'Data that can be resolved to give a String...',
- type: { names: [ 'String', 'Array', 'Object' ] },
- meta:
- { lineno: 142,
- filename: 'ClientDataResolver.js',
- path: 'src/client' },
- order: 37 }
-*/
-
-class DocumentedTypeDef extends DocumentedItem {
-
- constructor(...args) {
- super(...args);
- }
-
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.props = new Map();
- this.directData = data;
- this.directData.meta = new DocumentedItemMeta(this, data.meta);
- this.directData.type = new DocumentedVarType(this, data.type);
- data.properties = data.properties || [];
- for (const prop of data.properties) {
- this.props.set(prop.name, new DocumentedParam(this, prop));
- }
- }
-
- serialize() {
- super.serialize();
- const { id, name, description, type, access, meta } = this.directData;
- const serialized = {
- id,
- name,
- description,
- type: type.serialize(),
- access,
- meta: meta.serialize(),
- };
- serialized.properties = Array.from(this.props.values()).map(p => p.serialize());
- return serialized;
- }
-
-}
-
-module.exports = DocumentedTypeDef;
diff --git a/docs/generator/types/DocumentedVarType.js b/docs/generator/types/DocumentedVarType.js
deleted file mode 100644
index 209431daf..000000000
--- a/docs/generator/types/DocumentedVarType.js
+++ /dev/null
@@ -1,50 +0,0 @@
-const DocumentedItem = require('./DocumentedItem');
-
-/*
-{
- "names":[
- "String"
- ]
-}
-*/
-
-const regex = /([\w]+)([^\w]+)/;
-const regexG = /([\w]+)([^\w]+)/g;
-
-function splitVarName(str) {
- if (str === '*') {
- return ['*', ''];
- }
- const matches = str.match(regexG);
- const output = [];
- if (matches) {
- for (const match of matches) {
- const groups = match.match(regex);
- output.push([groups[1], groups[2]]);
- }
- } else {
- output.push([str.match(/(\w+)/g)[0], '']);
- }
- return output;
-}
-
-class DocumentedVarType extends DocumentedItem {
-
- registerMetaInfo(data) {
- super.registerMetaInfo(data);
- this.directData = data;
- }
-
- serialize() {
- super.serialize();
- const names = [];
- for (const name of this.directData.names) {
- names.push(splitVarName(name));
- }
- return {
- types: names,
- };
- }
-}
-
-module.exports = DocumentedVarType;
diff --git a/docs/index.yml b/docs/index.yml
new file mode 100644
index 000000000..4bf13c7e3
--- /dev/null
+++ b/docs/index.yml
@@ -0,0 +1,16 @@
+- name: General
+ files:
+ - name: Welcome
+ path: welcome.md
+ - name: Updating your code
+ path: updating.md
+ - name: FAQ
+ path: faq.md
+- name: Examples
+ files:
+ - name: Ping
+ path: ping.js
+ - name: Avatars
+ path: avatars.js
+ - name: Webhook
+ path: webhook.js
diff --git a/docs/logo.svg b/docs/logo.svg
new file mode 100644
index 000000000..81feb17ba
--- /dev/null
+++ b/docs/logo.svg
@@ -0,0 +1,19 @@
+
+
diff --git a/package.json b/package.json
index 6e92756f8..f9a466282 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,13 @@
"version": "10.0.1",
"description": "A powerful library for interacting with the Discord API",
"main": "./src/index",
+ "types": "./typings/index.d.ts",
"scripts": {
- "test": "eslint src/ && node docs/generator/generator.js silent",
- "docs": "node docs/generator/generator.js"
+ "test": "eslint src && docgen --source src --custom docs/index.yml",
+ "docs": "docgen --source src --custom docs/index.yml --output docs/docs.json",
+ "test-docs": "docgen --source src --custom docs",
+ "lint": "eslint src",
+ "web-dist": "node ./node_modules/parallel-webpack/bin/run.js"
},
"repository": {
"type": "git",
@@ -25,21 +29,57 @@
"url": "https://github.com/hydrabolt/discord.js/issues"
},
"homepage": "https://github.com/hydrabolt/discord.js#readme",
+ "runkitExampleFilename": "./docs/examples/ping.js",
"dependencies": {
- "superagent": "^2.3.0",
- "tweetnacl": "^0.14.3",
- "ws": "^1.1.1"
+ "@types/node": "^6.0.0",
+ "pako": "^1.0.0",
+ "superagent": "^3.3.0",
+ "tweetnacl": "^0.14.0",
+ "ws": "^1.1.0"
},
"peerDependencies": {
- "node-opus": "^0.2.1",
- "opusscript": "^0.0.1"
+ "erlpack": "hammerandchisel/erlpack#master",
+ "node-opus": "^0.2.0",
+ "opusscript": "^0.0.1",
+ "uws": "^0.12.0"
},
"devDependencies": {
- "eslint": "^3.8.0",
- "fs-extra": "^0.30.0",
- "jsdoc-to-markdown": "^2.0.0"
+ "discord.js-docgen": "hydrabolt/discord.js-docgen#master",
+ "eslint": "^3.12.0",
+ "parallel-webpack": "^1.6.0",
+ "uglify-js": "mishoo/UglifyJS2#harmony",
+ "webpack": "2.2.0-rc.3"
},
"engines": {
"node": ">=6.0.0"
+ },
+ "browser": {
+ "ws": false,
+ "uws": false,
+ "erlpack": false,
+ "opusscript": false,
+ "node-opus": false,
+ "tweet-nacl": false,
+ "src/sharding/Shard.js": false,
+ "src/sharding/ShardClientUtil.js": false,
+ "src/sharding/ShardingManager.js": false,
+ "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/OpusEngineList.js": false,
+ "src/client/voice/opus/OpusScriptEngine.js": false,
+ "src/client/voice/pcm/ConverterEngine.js": false,
+ "src/client/voice/pcm/ConverterEngineList.js": false,
+ "src/client/voice/pcm/FfmpegConverterEngine.js": false,
+ "src/client/voice/player/AudioPlayer.js": false,
+ "src/client/voice/player/BasePlayer.js": false,
+ "src/client/voice/player/DefaultPlayer.js": false,
+ "src/client/voice/receiver/VoiceReadable.js": false,
+ "src/client/voice/receiver/VoiceReceiver.js": false,
+ "src/client/voice/util/SecretKey.js": false,
+ "src/client/voice/ClientVoiceManager.js": false,
+ "src/client/voice/VoiceConnection.js": false,
+ "src/client/voice/VoiceUDPClient.js": false,
+ "src/client/voice/VoiceWebSocket.js": false
}
}
diff --git a/src/client/Client.js b/src/client/Client.js
index eb95c45c3..11cd5f403 100644
--- a/src/client/Client.js
+++ b/src/client/Client.js
@@ -77,39 +77,39 @@ class Client extends EventEmitter {
this.actions = new ActionsManager(this);
/**
- * The Voice Manager of the Client
- * @type {ClientVoiceManager}
+ * The Voice Manager of the Client (`null` in browsers)
+ * @type {?ClientVoiceManager}
* @private
*/
- this.voice = new ClientVoiceManager(this);
+ this.voice = !this.browser ? new ClientVoiceManager(this) : null;
/**
* The shard helpers for the client (only if the process was spawned as a child, such as from a ShardingManager)
- * @type {?ShardUtil}
+ * @type {?ShardClientUtil}
*/
this.shard = process.send ? ShardClientUtil.singleton(this) : null;
/**
- * A Collection of the Client's stored users
+ * A collection of the Client's stored users
* @type {Collection}
*/
this.users = new Collection();
/**
- * A Collection of the Client's stored guilds
+ * A collection of the Client's stored guilds
* @type {Collection}
*/
this.guilds = new Collection();
/**
- * A Collection of the Client's stored channels
+ * A collection of the Client's stored channels
* @type {Collection}
*/
this.channels = new Collection();
/**
- * A Collection of presences for friends of the logged in user.
- * This is only present for user accounts, not bot accounts!
+ * A collection of presences for friends of the logged in user.
+ * This is only filled when using a user account.
* @type {Collection}
*/
this.presences = new Collection();
@@ -124,18 +124,6 @@ class Client extends EventEmitter {
this.token = null;
}
- /**
- * The email, if there is one, for the logged in Client
- * @type {?string}
- */
- this.email = null;
-
- /**
- * The password, if there is one, for the logged in Client
- * @type {?string}
- */
- this.password = null;
-
/**
* The ClientUser representing the logged in Client
* @type {?ClientUser}
@@ -148,6 +136,13 @@ class Client extends EventEmitter {
*/
this.readyAt = null;
+ /**
+ * The previous heartbeat pings of the websocket (most recent first, limited to three elements)
+ * @type {number[]}
+ */
+ this.pings = [];
+
+ this._pingTimestamp = 0;
this._timeouts = new Set();
this._intervals = new Set();
@@ -175,11 +170,21 @@ class Client extends EventEmitter {
}
/**
- * Returns a Collection, mapping Guild ID to Voice Connections.
+ * The average heartbeat ping of the websocket
+ * @type {number}
+ * @readonly
+ */
+ get ping() {
+ return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length;
+ }
+
+ /**
+ * Returns a collection, mapping guild ID to voice connections.
* @type {Collection}
* @readonly
*/
get voiceConnections() {
+ if (this.browser) return new Collection();
return this.voice.connections;
}
@@ -205,14 +210,21 @@ class Client extends EventEmitter {
return this.readyAt ? this.readyAt.getTime() : null;
}
+ /**
+ * Whether the client is in a browser environment
+ * @type {boolean}
+ * @readonly
+ */
+ get browser() {
+ return typeof window !== 'undefined';
+ }
+
/**
* Logs the client in. If successful, resolves with the account's token. If you're making a bot, it's
* much better to use a bot account rather than a user account.
* Bot accounts have higher rate limits and have access to some features user accounts don't have. User bots
* that are making a lot of API requests can even be banned.
- * @param {string} tokenOrEmail The token or email used for the account. If it is an email, a password _must_ be
- * provided.
- * @param {string} [password] The password for the account, only needed if an email was provided.
+ * @param {string} token The token used for the account.
* @returns {Promise}
* @example
* // log the client in using a token
@@ -224,9 +236,8 @@ class Client extends EventEmitter {
* const password = 'supersecret123';
* client.login(email, password);
*/
- login(tokenOrEmail, password = null) {
- if (password) return this.rest.methods.loginEmailPassword(tokenOrEmail, password);
- return this.rest.methods.loginToken(tokenOrEmail);
+ login(token) {
+ return this.rest.methods.login(token);
}
/**
@@ -238,29 +249,26 @@ class Client extends EventEmitter {
for (const i of this._intervals) clearInterval(i);
this._timeouts.clear();
this._intervals.clear();
- this.token = null;
- this.email = null;
- this.password = null;
return this.manager.destroy();
}
/**
* This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however
- * if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.
+ * if you wish to force a sync of guild data, you can use this.
+ * This is only available when using a user account.
* @param {Guild[]|Collection} [guilds=this.guilds] An array or collection of guilds to sync
*/
syncGuilds(guilds = this.guilds) {
- if (!this.user.bot) {
- this.ws.send({
- op: 12,
- d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
- });
- }
+ if (this.user.bot) return;
+ this.ws.send({
+ op: 12,
+ d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
+ });
}
/**
* Caches a user, or obtains it from the cache if it's already cached.
- * If the user isn't already cached, it will only be obtainable by OAuth bot accounts.
+ * This is only available when using a bot account.
* @param {string} id The ID of the user to obtain
* @returns {Promise}
*/
@@ -282,10 +290,11 @@ class Client extends EventEmitter {
/**
* Fetch a webhook by ID.
* @param {string} id ID of the webhook
+ * @param {string} [token] Token for the webhook
* @returns {Promise}
*/
- fetchWebhook(id) {
- return this.rest.methods.getWebhook(id);
+ fetchWebhook(id, token) {
+ return this.rest.methods.getWebhook(id, token);
}
/**
@@ -324,31 +333,90 @@ class Client extends EventEmitter {
return messages;
}
- setTimeout(fn, ...params) {
+ /**
+ * Gets the bot's OAuth2 application.
+ * This is only available when using a bot account.
+ * @returns {Promise}
+ */
+ fetchApplication() {
+ if (!this.user.bot) throw new Error(Constants.Errors.NO_BOT_ACCOUNT);
+ return this.rest.methods.getMyApplication();
+ }
+
+ /**
+ * Generate an invite link for your bot
+ * @param {PermissionResolvable[]|number} [permissions] An array of permissions to request
+ * @returns {Promise} The invite link
+ * @example
+ * client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE'])
+ * .then(link => {
+ * console.log(`Generated bot invite link: ${link}`);
+ * });
+ */
+ generateInvite(permissions) {
+ if (permissions) {
+ if (permissions instanceof Array) permissions = this.resolver.resolvePermissions(permissions);
+ } else {
+ permissions = 0;
+ }
+ return this.fetchApplication().then(application =>
+ `https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot`
+ );
+ }
+
+ /**
+ * Sets a timeout that will be automatically cancelled if the client is destroyed.
+ * @param {Function} fn Function to execute
+ * @param {number} delay Time to wait before executing (in milliseconds)
+ * @param {...*} args Arguments for the function
+ * @returns {Timeout}
+ */
+ setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn();
this._timeouts.delete(timeout);
- }, ...params);
+ }, delay, ...args);
this._timeouts.add(timeout);
return timeout;
}
+ /**
+ * Clears a timeout
+ * @param {Timeout} timeout Timeout to cancel
+ */
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
- setInterval(...params) {
- const interval = setInterval(...params);
+ /**
+ * Sets an interval that will be automatically cancelled if the client is destroyed.
+ * @param {Function} fn Function to execute
+ * @param {number} delay Time to wait before executing (in milliseconds)
+ * @param {...*} args Arguments for the function
+ * @returns {Timeout}
+ */
+ setInterval(fn, delay, ...args) {
+ const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
+ /**
+ * Clears an interval
+ * @param {Timeout} interval Interval to cancel
+ */
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
+ _pong(startTime) {
+ this.pings.unshift(Date.now() - startTime);
+ if (this.pings.length > 3) this.pings.length = 3;
+ this.ws.lastHeartbeatAck = true;
+ }
+
_setPresence(id, presence) {
if (this.presences.get(id)) {
this.presences.get(id).update(presence);
@@ -400,11 +468,11 @@ module.exports = Client;
/**
* Emitted for general warnings
* @event Client#warn
- * @param {string} The warning
+ * @param {string} info The warning
*/
/**
* Emitted for general debugging information
* @event Client#debug
- * @param {string} The debug information
+ * @param {string} info The debug information
*/
diff --git a/src/client/ClientDataManager.js b/src/client/ClientDataManager.js
index 7d837d970..32b442b73 100644
--- a/src/client/ClientDataManager.js
+++ b/src/client/ClientDataManager.js
@@ -24,7 +24,7 @@ class ClientDataManager {
this.client.guilds.set(guild.id, guild);
if (this.pastReady && !already) {
/**
- * Emitted whenever the client joins a Guild.
+ * Emitted whenever the client joins a guild.
* @event Client#guildCreate
* @param {Guild} guild The created guild
*/
@@ -78,7 +78,7 @@ class ClientDataManager {
const already = guild.emojis.has(data.id);
if (data && !already) {
let emoji = new Emoji(guild, data);
- this.client.emit(Constants.Events.EMOJI_CREATE, emoji);
+ this.client.emit(Constants.Events.GUILD_EMOJI_CREATE, emoji);
guild.emojis.set(emoji.id, emoji);
return emoji;
} else if (already) {
@@ -90,7 +90,7 @@ class ClientDataManager {
killEmoji(emoji) {
if (!(emoji instanceof Emoji && emoji.guild)) return;
- this.client.emit(Constants.Events.EMOJI_DELETE, emoji);
+ this.client.emit(Constants.Events.GUILD_EMOJI_DELETE, emoji);
emoji.guild.emojis.delete(emoji.id);
}
diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js
index 8fee276f0..d38fb7c95 100644
--- a/src/client/ClientDataResolver.js
+++ b/src/client/ClientDataResolver.js
@@ -3,11 +3,14 @@ const fs = require('fs');
const request = require('superagent');
const Constants = require('../util/Constants');
-const User = require(`../structures/User`);
-const Message = require(`../structures/Message`);
-const Guild = require(`../structures/Guild`);
-const Channel = require(`../structures/Channel`);
-const GuildMember = require(`../structures/GuildMember`);
+const convertArrayBuffer = require('../util/ConvertArrayBuffer');
+const User = require('../structures/User');
+const Message = require('../structures/Message');
+const Guild = require('../structures/Guild');
+const Channel = require('../structures/Channel');
+const GuildMember = require('../structures/GuildMember');
+const Emoji = require('../structures/Emoji');
+const ReactionEmoji = require('../structures/ReactionEmoji');
/**
* The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g.
@@ -25,10 +28,10 @@ class ClientDataResolver {
/**
* Data that resolves to give a User object. This can be:
* * A User object
- * * A User ID
- * * A Message (resolves to the message author)
- * * A Guild (owner of the guild)
- * * A Guild Member
+ * * A user ID
+ * * A Message object (resolves to the message author)
+ * * A Guild object (owner of the guild)
+ * * A GuildMember object
* @typedef {User|string|Message|Guild|GuildMember} UserResolvable
*/
@@ -62,7 +65,8 @@ class ClientDataResolver {
/**
* Data that resolves to give a Guild object. This can be:
* * A Guild object
- * @typedef {Guild} GuildResolvable
+ * * A Guild ID
+ * @typedef {Guild|string} GuildResolvable
*/
/**
@@ -91,20 +95,18 @@ class ClientDataResolver {
*/
resolveGuildMember(guild, user) {
if (user instanceof GuildMember) return user;
-
guild = this.resolveGuild(guild);
user = this.resolveUser(user);
if (!guild || !user) return null;
-
return guild.members.get(user.id) || null;
}
/**
* Data that can be resolved to give a Channel. This can be:
- * * An instance of a Channel
- * * An instance of a Message (the channel the message was sent in)
- * * An instance of a Guild (the #general channel)
- * * An ID of a Channel
+ * * A Channel object
+ * * A Message object (the channel the message was sent in)
+ * * A Guild object (the #general channel)
+ * * A channel ID
* @typedef {Channel|Guild|Message|string} ChannelResolvable
*/
@@ -136,7 +138,6 @@ class ClientDataResolver {
resolveInviteCode(data) {
const inviteRegex = /discord(?:app)?\.(?:gg|com\/invite)\/([a-z0-9]{5})/i;
const match = inviteRegex.exec(data);
-
if (match && match[1]) return match[1];
return data;
}
@@ -155,6 +156,7 @@ class ClientDataResolver {
* "ADMINISTRATOR",
* "MANAGE_CHANNELS",
* "MANAGE_GUILD",
+ * "ADD_REACTIONS", // add reactions to messages
* "READ_MESSAGES",
* "SEND_MESSAGES",
* "SEND_TTS_MESSAGES",
@@ -172,7 +174,9 @@ class ClientDataResolver {
* "USE_VAD", // use voice activity detection
* "CHANGE_NICKNAME",
* "MANAGE_NICKNAMES", // change nicknames of others
- * "MANAGE_ROLES_OR_PERMISSIONS"
+ * "MANAGE_ROLES_OR_PERMISSIONS",
+ * "MANAGE_WEBHOOKS",
+ * "MANAGE_EMOJIS"
* ]
* ```
* @typedef {string|number} PermissionResolvable
@@ -189,10 +193,21 @@ class ClientDataResolver {
return permission;
}
+ /**
+ * Turn an array of permissions into a valid Discord permission bitfield
+ * @param {PermissionResolvable[]} permissions Permissions to resolve together
+ * @returns {number}
+ */
+ resolvePermissions(permissions) {
+ let bitfield = 0;
+ for (const permission of permissions) bitfield |= this.resolvePermission(permission);
+ return bitfield;
+ }
+
/**
* Data that can be resolved to give a string. This can be:
* * A string
- * * An Array (joined with a new line delimiter to give a string)
+ * * An array (joined with a new line delimiter to give a string)
* * Any value
* @typedef {string|Array|*} StringResolvable
*/
@@ -211,7 +226,7 @@ class ClientDataResolver {
/**
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
* * A Buffer
- * * A Base64 string
+ * * A base64 string
* @typedef {Buffer|string} Base64Resolvable
*/
@@ -230,21 +245,29 @@ class ClientDataResolver {
* * A Buffer
* * The path to a local file
* * A URL
- * @typedef {string|Buffer} FileResolvable
+ * @typedef {string|Buffer} BufferResolvable
*/
/**
- * Resolves a FileResolvable to a Buffer
- * @param {FileResolvable} resource The file resolvable to resolve
+ * Resolves a BufferResolvable to a Buffer
+ * @param {BufferResolvable} resource The buffer resolvable to resolve
* @returns {Promise}
*/
- resolveFile(resource) {
+ resolveBuffer(resource) {
+ if (resource instanceof Buffer) return Promise.resolve(resource);
+ if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertArrayBuffer(resource));
+
if (typeof resource === 'string') {
return new Promise((resolve, reject) => {
if (/^https?:\/\//.test(resource)) {
- request.get(resource)
- .set('Content-Type', 'blob')
- .end((err, res) => err ? reject(err) : resolve(res.body));
+ const req = request.get(resource).set('Content-Type', 'blob');
+ if (this.client.browser) req.responseType('arraybuffer');
+ req.end((err, res) => {
+ if (err) return reject(err);
+ if (this.client.browser) return resolve(convertArrayBuffer(res.xhr.response));
+ 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) => {
@@ -258,8 +281,28 @@ class ClientDataResolver {
});
}
- if (resource instanceof Buffer) return Promise.resolve(resource);
- return Promise.reject(new TypeError('Resource must be a string or Buffer.'));
+ return Promise.reject(new TypeError('The resource must be a string or Buffer.'));
+ }
+
+ /**
+ * Data that can be resolved to give an emoji identifier. This can be:
+ * * A string
+ * * An Emoji
+ * * A ReactionEmoji
+ * @typedef {string|Emoji|ReactionEmoji} EmojiIdentifierResolvable
+ */
+
+ /**
+ * Resolves an EmojiResolvable to an emoji identifier
+ * @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve
+ * @returns {string}
+ */
+ resolveEmojiIdentifier(emoji) {
+ if (emoji instanceof Emoji || emoji instanceof ReactionEmoji) return emoji.identifier;
+ if (typeof emoji === 'string') {
+ if (!emoji.includes('%')) return encodeURIComponent(emoji);
+ }
+ return null;
}
}
diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js
index ef943b339..0cfbbfdf4 100644
--- a/src/client/ClientManager.js
+++ b/src/client/ClientManager.js
@@ -22,8 +22,8 @@ class ClientManager {
/**
* Connects the Client to the WebSocket
* @param {string} token The authorization token
- * @param {function} resolve Function to run when connection is successful
- * @param {function} reject Function to run when connection fails
+ * @param {Function} resolve Function to run when connection is successful
+ * @param {Function} reject Function to run when connection fails
*/
connectToWebSocket(token, resolve, reject) {
this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`);
@@ -40,7 +40,7 @@ class ClientManager {
resolve(token);
this.client.clearTimeout(timeout);
});
- }).catch(reject);
+ }, reject);
}
/**
@@ -48,24 +48,19 @@ class ClientManager {
* @param {number} time The interval in milliseconds at which heartbeat packets should be sent
*/
setupKeepAlive(time) {
- this.heartbeatInterval = this.client.setInterval(() => {
- this.client.emit('debug', 'Sending heartbeat');
- this.client.ws.send({
- op: Constants.OPCodes.HEARTBEAT,
- d: this.client.ws.sequence,
- }, true);
- }, time);
+ this.heartbeatInterval = this.client.setInterval(() => this.client.ws.heartbeat(true), time);
}
destroy() {
- return new Promise((resolve, reject) => {
- this.client.ws.destroy();
- if (!this.client.user.bot) {
- this.client.rest.methods.logout().then(resolve, reject);
- } else {
- resolve();
- }
- });
+ this.client.ws.destroy();
+ if (this.client.user.bot) {
+ this.client.token = null;
+ return Promise.resolve();
+ } else {
+ return this.client.rest.methods.logout().then(() => {
+ this.client.token = null;
+ });
+ }
}
}
diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js
index 024db857c..ac95aa7fb 100644
--- a/src/client/actions/ActionsManager.js
+++ b/src/client/actions/ActionsManager.js
@@ -2,33 +2,36 @@ class ActionsManager {
constructor(client) {
this.client = client;
- this.register('MessageCreate');
- this.register('MessageDelete');
- this.register('MessageDeleteBulk');
- this.register('MessageUpdate');
- this.register('ChannelCreate');
- this.register('ChannelDelete');
- this.register('ChannelUpdate');
- this.register('GuildDelete');
- this.register('GuildUpdate');
- this.register('GuildMemberGet');
- this.register('GuildMemberRemove');
- this.register('GuildBanRemove');
- this.register('GuildRoleCreate');
- this.register('GuildRoleDelete');
- this.register('GuildRoleUpdate');
- this.register('UserGet');
- this.register('UserUpdate');
- this.register('GuildSync');
- this.register('GuildEmojiCreate');
- this.register('GuildEmojiDelete');
- this.register('GuildEmojiUpdate');
- this.register('GuildRolesPositionUpdate');
+ this.register(require('./MessageCreate'));
+ this.register(require('./MessageDelete'));
+ this.register(require('./MessageDeleteBulk'));
+ this.register(require('./MessageUpdate'));
+ this.register(require('./MessageReactionAdd'));
+ this.register(require('./MessageReactionRemove'));
+ this.register(require('./MessageReactionRemoveAll'));
+ this.register(require('./ChannelCreate'));
+ this.register(require('./ChannelDelete'));
+ this.register(require('./ChannelUpdate'));
+ this.register(require('./GuildDelete'));
+ this.register(require('./GuildUpdate'));
+ this.register(require('./GuildMemberGet'));
+ this.register(require('./GuildMemberRemove'));
+ this.register(require('./GuildBanRemove'));
+ this.register(require('./GuildRoleCreate'));
+ this.register(require('./GuildRoleDelete'));
+ this.register(require('./GuildRoleUpdate'));
+ this.register(require('./UserGet'));
+ this.register(require('./UserUpdate'));
+ this.register(require('./UserNoteUpdate'));
+ this.register(require('./GuildSync'));
+ this.register(require('./GuildEmojiCreate'));
+ this.register(require('./GuildEmojiDelete'));
+ this.register(require('./GuildEmojiUpdate'));
+ this.register(require('./GuildRolesPositionUpdate'));
}
- register(name) {
- const Action = require(`./${name}`);
- this[name] = new Action(this.client);
+ register(Action) {
+ this[Action.name.replace(/Action$/, '')] = new Action(this.client);
}
}
diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js
index a3f238fe5..5df1ced3c 100644
--- a/src/client/actions/GuildEmojiCreate.js
+++ b/src/client/actions/GuildEmojiCreate.js
@@ -1,9 +1,9 @@
const Action = require('./Action');
-class EmojiCreateAction extends Action {
- handle(data, guild) {
+class GuildEmojiCreateAction extends Action {
+ handle(guild, createdEmoji) {
const client = this.client;
- const emoji = client.dataManager.newEmoji(data, guild);
+ const emoji = client.dataManager.newEmoji(createdEmoji, guild);
return {
emoji,
};
@@ -11,8 +11,8 @@ class EmojiCreateAction extends Action {
}
/**
- * Emitted whenever an emoji is created
- * @event Client#guildEmojiCreate
+ * Emitted whenever a custom emoji is created in a guild
+ * @event Client#emojiCreate
* @param {Emoji} emoji The emoji that was created.
*/
-module.exports = EmojiCreateAction;
+module.exports = GuildEmojiCreateAction;
diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js
index 7fdd1ca32..8cfa20591 100644
--- a/src/client/actions/GuildEmojiDelete.js
+++ b/src/client/actions/GuildEmojiDelete.js
@@ -1,18 +1,18 @@
const Action = require('./Action');
-class EmojiDeleteAction extends Action {
- handle(data) {
+class GuildEmojiDeleteAction extends Action {
+ handle(emoji) {
const client = this.client;
- client.dataManager.killEmoji(data);
+ client.dataManager.killEmoji(emoji);
return {
- data,
+ emoji,
};
}
}
/**
- * Emitted whenever an emoji is deleted
- * @event Client#guildEmojiDelete
+ * Emitted whenever a custom guild emoji is deleted
+ * @event Client#emojiDelete
* @param {Emoji} emoji The emoji that was deleted.
*/
-module.exports = EmojiDeleteAction;
+module.exports = GuildEmojiDeleteAction;
diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js
index 88e0c395c..94bfa24d9 100644
--- a/src/client/actions/GuildEmojiUpdate.js
+++ b/src/client/actions/GuildEmojiUpdate.js
@@ -1,28 +1,14 @@
const Action = require('./Action');
class GuildEmojiUpdateAction extends Action {
- handle(data, guild) {
- const client = this.client;
- for (let emoji of data.emojis) {
- const already = guild.emojis.has(emoji.id);
- if (already) {
- client.dataManager.updateEmoji(guild.emojis.get(emoji.id), emoji);
- } else {
- emoji = client.dataManager.newEmoji(emoji, guild);
- }
- }
- for (let emoji of guild.emojis) {
- if (!data.emoijs.has(emoji.id)) client.dataManager.killEmoji(emoji);
- }
- return {
- emojis: data.emojis,
- };
+ handle(oldEmoji, newEmoji) {
+ this.client.dataManager.updateEmoji(oldEmoji, newEmoji);
}
}
/**
- * Emitted whenever an emoji is updated
- * @event Client#guildEmojiUpdate
+ * Emitted whenever a custom guild emoji is updated
+ * @event Client#emojiUpdate
* @param {Emoji} oldEmoji The old emoji
* @param {Emoji} newEmoji The new emoji
*/
diff --git a/src/client/actions/GuildSync.js b/src/client/actions/GuildSync.js
index d9b8dab02..7b94ec83c 100644
--- a/src/client/actions/GuildSync.js
+++ b/src/client/actions/GuildSync.js
@@ -17,7 +17,7 @@ class GuildSync extends Action {
if (member) {
guild._updateMember(member, syncMember);
} else {
- guild._addMember(syncMember);
+ guild._addMember(syncMember, false);
}
}
}
diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js
index 1280d991a..00fc1e93d 100644
--- a/src/client/actions/MessageCreate.js
+++ b/src/client/actions/MessageCreate.js
@@ -6,17 +6,25 @@ class MessageCreateAction extends Action {
const client = this.client;
const channel = client.channels.get((data instanceof Array ? data[0] : data).channel_id);
+ const user = client.users.get((data instanceof Array ? data[0] : data).author.id);
if (channel) {
+ const member = channel.guild ? channel.guild.member(user) : null;
if (data instanceof Array) {
const messages = new Array(data.length);
for (let i = 0; i < data.length; i++) {
messages[i] = channel._cacheMessage(new Message(channel, data[i], client));
}
+ channel.lastMessageID = messages[messages.length - 1].id;
+ if (user) user.lastMessageID = messages[messages.length - 1].id;
+ if (member) member.lastMessageID = messages[messages.length - 1].id;
return {
messages,
};
} else {
const message = channel._cacheMessage(new Message(channel, data, client));
+ channel.lastMessageID = data.id;
+ if (user) user.lastMessageID = data.id;
+ if (member) member.lastMessageID = data.id;
return {
message,
};
diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js
new file mode 100644
index 000000000..f57ec2e27
--- /dev/null
+++ b/src/client/actions/MessageReactionAdd.js
@@ -0,0 +1,43 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+
+/*
+{ user_id: 'id',
+ message_id: 'id',
+ emoji: { name: '�', id: null },
+ channel_id: 'id' } }
+*/
+
+class MessageReactionAdd extends Action {
+ handle(data) {
+ const user = this.client.users.get(data.user_id);
+ if (!user) return false;
+
+ const channel = this.client.channels.get(data.channel_id);
+ if (!channel || channel.type === 'voice') return false;
+
+ const message = channel.messages.get(data.message_id);
+ if (!message) return false;
+
+ if (!data.emoji) return false;
+
+ const reaction = message._addReaction(data.emoji, user);
+
+ if (reaction) {
+ this.client.emit(Constants.Events.MESSAGE_REACTION_ADD, reaction, user);
+ }
+
+ return {
+ message,
+ reaction,
+ user,
+ };
+ }
+}
+/**
+ * Emitted whenever a reaction is added to a message.
+ * @event Client#messageReactionAdd
+ * @param {MessageReaction} messageReaction The reaction object.
+ * @param {User} user The user that applied the emoji or reaction emoji.
+ */
+module.exports = MessageReactionAdd;
diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js
new file mode 100644
index 000000000..98a958d15
--- /dev/null
+++ b/src/client/actions/MessageReactionRemove.js
@@ -0,0 +1,43 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+
+/*
+{ user_id: 'id',
+ message_id: 'id',
+ emoji: { name: '�', id: null },
+ channel_id: 'id' } }
+*/
+
+class MessageReactionRemove extends Action {
+ handle(data) {
+ const user = this.client.users.get(data.user_id);
+ if (!user) return false;
+
+ const channel = this.client.channels.get(data.channel_id);
+ if (!channel || channel.type === 'voice') return false;
+
+ const message = channel.messages.get(data.message_id);
+ if (!message) return false;
+
+ if (!data.emoji) return false;
+
+ const reaction = message._removeReaction(data.emoji, user);
+
+ if (reaction) {
+ this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE, reaction, user);
+ }
+
+ return {
+ message,
+ reaction,
+ user,
+ };
+ }
+}
+/**
+ * Emitted whenever a reaction is removed from a message.
+ * @event Client#messageReactionRemove
+ * @param {MessageReaction} messageReaction The reaction object.
+ * @param {User} user The user that removed the emoji or reaction emoji.
+ */
+module.exports = MessageReactionRemove;
diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js
new file mode 100644
index 000000000..f35b78503
--- /dev/null
+++ b/src/client/actions/MessageReactionRemoveAll.js
@@ -0,0 +1,25 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+
+class MessageReactionRemoveAll extends Action {
+ handle(data) {
+ const channel = this.client.channels.get(data.channel_id);
+ if (!channel || channel.type === 'voice') return false;
+
+ const message = channel.messages.get(data.message_id);
+ if (!message) return false;
+
+ message._clearReactions();
+ this.client.emit(Constants.Events.MESSAGE_REACTION_REMOVE_ALL, message);
+
+ return {
+ message,
+ };
+ }
+}
+/**
+ * Emitted whenever all reactions are removed from a message.
+ * @event Client#messageReactionRemoveAll
+ * @param {MessageReaction} messageReaction The reaction object.
+ */
+module.exports = MessageReactionRemoveAll;
diff --git a/src/client/actions/UserNoteUpdate.js b/src/client/actions/UserNoteUpdate.js
new file mode 100644
index 000000000..4c2cc2187
--- /dev/null
+++ b/src/client/actions/UserNoteUpdate.js
@@ -0,0 +1,30 @@
+const Action = require('./Action');
+const Constants = require('../../util/Constants');
+
+class UserNoteUpdateAction extends Action {
+ handle(data) {
+ const client = this.client;
+
+ const oldNote = client.user.notes.get(data.id);
+ const note = data.note.length ? data.note : null;
+
+ client.user.notes.set(data.id, note);
+
+ client.emit(Constants.Events.USER_NOTE_UPDATE, data.id, oldNote, note);
+
+ return {
+ old: oldNote,
+ updated: note,
+ };
+ }
+}
+
+/**
+ * Emitted whenever a note is updated.
+ * @event Client#userNoteUpdate
+ * @param {User} user The user the note belongs to
+ * @param {string} oldNote The note content before the update
+ * @param {string} newNote The note content after the update
+ */
+
+module.exports = UserNoteUpdateAction;
diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js
index 0e6e987cb..36c2d8fed 100644
--- a/src/client/rest/APIRequest.js
+++ b/src/client/rest/APIRequest.js
@@ -4,7 +4,7 @@ const Constants = require('../../util/Constants');
function getRoute(url) {
let route = url.split('?')[0];
if (route.includes('/channels/') || route.includes('/guilds/')) {
- const startInd = ~route.indexOf('/channels/') ? route.indexOf('/channels/') : route.indexOf('/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);
}
@@ -37,15 +37,11 @@ class APIRequest {
if (this.file && this.file.file) {
apiRequest.attach('file', this.file.file, this.file.name);
this.data = this.data || {};
- for (const key in this.data) {
- if (this.data[key]) {
- apiRequest.field(key, this.data[key]);
- }
- }
+ apiRequest.field('payload_json', JSON.stringify(this.data));
} else if (this.data) {
apiRequest.send(this.data);
}
- apiRequest.set('User-Agent', this.rest.userAgentManager.userAgent);
+ if (!this.rest.client.browser) apiRequest.set('User-Agent', this.rest.userAgentManager.userAgent);
return apiRequest;
}
}
diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js
index 1555d4e95..70062482e 100644
--- a/src/client/rest/RESTMethods.js
+++ b/src/client/rest/RESTMethods.js
@@ -1,37 +1,29 @@
const Constants = require('../../util/Constants');
const Collection = require('../../util/Collection');
const splitMessage = require('../../util/SplitMessage');
+const parseEmoji = require('../../util/ParseEmoji');
+const escapeMarkdown = require('../../util/EscapeMarkdown');
-const requireStructure = name => require(`../../structures/${name}`);
-const User = requireStructure('User');
-const GuildMember = requireStructure('GuildMember');
-const Role = requireStructure('Role');
-const Invite = requireStructure('Invite');
-const Webhook = requireStructure('Webhook');
-const UserProfile = requireStructure('UserProfile');
+const User = require('../../structures/User');
+const GuildMember = require('../../structures/GuildMember');
+const Message = require('../../structures/Message');
+const Role = require('../../structures/Role');
+const Invite = require('../../structures/Invite');
+const Webhook = require('../../structures/Webhook');
+const UserProfile = require('../../structures/UserProfile');
+const ClientOAuth2Application = require('../../structures/ClientOAuth2Application');
class RESTMethods {
constructor(restManager) {
this.rest = restManager;
+ this.client = restManager.client;
}
- loginToken(token = this.rest.client.token) {
- token = token.replace(/^Bot\s*/i, '');
+ login(token = this.client.token) {
return new Promise((resolve, reject) => {
- this.rest.client.manager.connectToWebSocket(token, resolve, reject);
- });
- }
-
- loginEmailPassword(email, password) {
- return new Promise((resolve, reject) => {
- this.rest.client.emit('warn', 'Client launched using email and password - should use token instead');
- this.rest.client.email = email;
- this.rest.client.password = password;
- this.rest.makeRequest('post', Constants.Endpoints.login, false, { email, password })
- .then(data => {
- resolve(this.loginToken(data.token));
- })
- .catch(reject);
+ if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN);
+ token = token.replace(/^Bot\s*/i, '');
+ this.client.manager.connectToWebSocket(token, resolve, reject);
});
}
@@ -40,13 +32,9 @@ class RESTMethods {
}
getGateway() {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.gateway, true)
- .then(res => {
- this.rest.client.ws.gateway = `${res.url}/?encoding=json&v=${Constants.PROTOCOL_VERSION}`;
- resolve(this.rest.client.ws.gateway);
- })
- .catch(reject);
+ return this.rest.makeRequest('get', Constants.Endpoints.gateway, true).then(res => {
+ this.client.ws.gateway = `${res.url}/?v=${Constants.PROTOCOL_VERSION}`;
+ return this.client.ws.gateway;
});
}
@@ -54,444 +42,403 @@ class RESTMethods {
return this.rest.makeRequest('get', Constants.Endpoints.botGateway, true);
}
- sendMessage(channel, content, { tts, nonce, disableEveryone, split } = {}, file = null) {
+ sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code } = {}, file = null) {
return new Promise((resolve, reject) => {
- if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
+ if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content);
if (content) {
- if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
+ if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
+ content = escapeMarkdown(this.client.resolver.resolveString(content), true);
+ content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
+ }
+
+ if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) {
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
}
if (split) content = splitMessage(content, typeof split === 'object' ? split : {});
}
+ const send = chan => {
+ if (content instanceof Array) {
+ const messages = [];
+ (function sendChunk(list, index) {
+ const options = index === list.length ? { tts, embed } : { tts };
+ chan.send(list[index], options, index === list.length ? file : null).then((message) => {
+ messages.push(message);
+ if (index >= list.length) return resolve(messages);
+ return sendChunk(list, ++index);
+ });
+ }(content, 0));
+ } else {
+ this.rest.makeRequest('post', Constants.Endpoints.channelMessages(chan.id), true, {
+ content, tts, nonce, embed,
+ }, file).then(data => resolve(this.client.actions.MessageCreate.handle(data).message), reject);
+ }
+ };
+
if (channel instanceof User || channel instanceof GuildMember) {
- this.createDM(channel).then(chan => {
- this._sendMessageRequest(chan, content, file, tts, nonce, resolve, reject);
- }).catch(reject);
+ this.createDM(channel).then(send, reject);
} else {
- this._sendMessageRequest(channel, content, file, tts, nonce, resolve, reject);
+ send(channel);
}
});
}
- _sendMessageRequest(channel, content, file, tts, nonce, resolve, reject) {
- if (content instanceof Array) {
- const datas = [];
- let promise = this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
- content: content[0], tts, nonce,
- }, file).catch(reject);
-
- for (let i = 1; i <= content.length; i++) {
- if (i < content.length) {
- const i2 = i;
- promise = promise.then(data => {
- datas.push(data);
- return this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
- content: content[i2], tts, nonce,
- }, file);
- }).catch(reject);
- } else {
- promise.then(data => {
- datas.push(data);
- resolve(this.rest.client.actions.MessageCreate.handle(datas).messages);
- }).catch(reject);
- }
- }
- } else {
- this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
- content, tts, nonce,
- }, file)
- .then(data => resolve(this.rest.client.actions.MessageCreate.handle(data).message))
- .catch(reject);
+ updateMessage(message, content, { embed, code } = {}) {
+ content = this.client.resolver.resolveString(content);
+ if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
+ content = escapeMarkdown(this.client.resolver.resolveString(content), true);
+ content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
}
+ return this.rest.makeRequest('patch', Constants.Endpoints.channelMessage(message.channel.id, message.id), true, {
+ content, embed,
+ }).then(data => this.client.actions.MessageUpdate.handle(data).updated);
}
deleteMessage(message) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('del', Constants.Endpoints.channelMessage(message.channel.id, message.id), true)
- .then(() => {
- resolve(this.rest.client.actions.MessageDelete.handle({
- id: message.id,
- channel_id: message.channel.id,
- }).message);
- })
- .catch(reject);
- });
+ return this.rest.makeRequest('del', Constants.Endpoints.channelMessage(message.channel.id, message.id), true)
+ .then(() =>
+ this.client.actions.MessageDelete.handle({
+ id: message.id,
+ channel_id: message.channel.id,
+ }).message
+ );
}
bulkDeleteMessages(channel, messages) {
- return new Promise((resolve, reject) => {
- const options = { messages };
- this.rest.makeRequest('post', `${Constants.Endpoints.channelMessages(channel.id)}/bulk_delete`, true, options)
- .then(() => {
- resolve(this.rest.client.actions.MessageDeleteBulk.handle({
- channel_id: channel.id,
- ids: messages,
- }).messages);
- })
- .catch(reject);
- });
+ return this.rest.makeRequest('post', `${Constants.Endpoints.channelMessages(channel.id)}/bulk_delete`, true, {
+ messages,
+ }).then(() =>
+ this.client.actions.MessageDeleteBulk.handle({
+ channel_id: channel.id,
+ ids: messages,
+ }).messages
+ );
}
- updateMessage(message, content) {
- return new Promise((resolve, reject) => {
- content = this.rest.client.resolver.resolveString(content);
-
- this.rest.makeRequest('patch', Constants.Endpoints.channelMessage(message.channel.id, message.id), true, {
- content,
- }).then(data => {
- resolve(this.rest.client.actions.MessageUpdate.handle(data).updated);
- }).catch(reject);
- });
- }
-
- createChannel(guild, channelName, channelType) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('post', Constants.Endpoints.guildChannels(guild.id), true, {
- name: channelName,
- type: channelType,
- }).then(data => {
- resolve(this.rest.client.actions.ChannelCreate.handle(data).channel);
- }).catch(reject);
- });
- }
-
- getExistingDM(recipient) {
- return this.rest.client.channels.filter(channel =>
- channel.recipient && channel.recipient.id === recipient.id
- ).first();
+ createChannel(guild, channelName, channelType, overwrites) {
+ if (overwrites instanceof Collection) overwrites = overwrites.array();
+ return this.rest.makeRequest('post', Constants.Endpoints.guildChannels(guild.id), true, {
+ name: channelName,
+ type: channelType,
+ permission_overwrites: overwrites,
+ }).then(data => this.client.actions.ChannelCreate.handle(data).channel);
}
createDM(recipient) {
- return new Promise((resolve, reject) => {
- const dmChannel = this.getExistingDM(recipient);
- if (dmChannel) return resolve(dmChannel);
- return this.rest.makeRequest('post', Constants.Endpoints.userChannels(this.rest.client.user.id), true, {
- recipient_id: recipient.id,
- }).then(data => resolve(this.rest.client.actions.ChannelCreate.handle(data).channel)).catch(reject);
- });
+ const dmChannel = this.getExistingDM(recipient);
+ if (dmChannel) return Promise.resolve(dmChannel);
+ return this.rest.makeRequest('post', Constants.Endpoints.userChannels(this.client.user.id), true, {
+ recipient_id: recipient.id,
+ }).then(data => this.client.actions.ChannelCreate.handle(data).channel);
+ }
+
+ getExistingDM(recipient) {
+ return this.client.channels.find(channel =>
+ channel.recipient && channel.recipient.id === recipient.id
+ );
}
deleteChannel(channel) {
- return new Promise((resolve, reject) => {
- if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel);
- this.rest.makeRequest('del', Constants.Endpoints.channel(channel.id), true).then(data => {
- data.id = channel.id;
- resolve(this.rest.client.actions.ChannelDelete.handle(data).channel);
- }).catch(reject);
+ if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel);
+ if (!channel) return Promise.reject(new Error('No channel to delete.'));
+ return this.rest.makeRequest('del', Constants.Endpoints.channel(channel.id), true).then(data => {
+ data.id = channel.id;
+ return this.client.actions.ChannelDelete.handle(data).channel;
});
}
- updateChannel(channel, data) {
- return new Promise((resolve, reject) => {
- data.name = (data.name || channel.name).trim();
- data.topic = data.topic || channel.topic;
- data.position = data.position || channel.position;
- data.bitrate = data.bitrate || channel.bitrate;
-
- this.rest.makeRequest('patch', Constants.Endpoints.channel(channel.id), true, data).then(newData => {
- resolve(this.rest.client.actions.ChannelUpdate.handle(newData).updated);
- }).catch(reject);
- });
+ updateChannel(channel, _data) {
+ const data = {};
+ data.name = (_data.name || channel.name).trim();
+ data.topic = _data.topic || channel.topic;
+ data.position = _data.position || channel.position;
+ data.bitrate = _data.bitrate || channel.bitrate;
+ data.user_limit = _data.userLimit || channel.userLimit;
+ return this.rest.makeRequest('patch', Constants.Endpoints.channel(channel.id), true, data).then(newData =>
+ this.client.actions.ChannelUpdate.handle(newData).updated
+ );
}
leaveGuild(guild) {
- if (guild.ownerID === this.rest.client.user.id) return this.deleteGuild(guild);
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('del', Constants.Endpoints.meGuild(guild.id), true).then(() => {
- resolve(this.rest.client.actions.GuildDelete.handle({ id: guild.id }).guild);
- }).catch(reject);
- });
+ if (guild.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.'));
+ return this.rest.makeRequest('del', Constants.Endpoints.meGuild(guild.id), true).then(() =>
+ this.client.actions.GuildDelete.handle({ id: guild.id }).guild
+ );
}
createGuild(options) {
- options.icon = this.rest.client.resolver.resolveBase64(options.icon) || null;
+ options.icon = this.client.resolver.resolveBase64(options.icon) || null;
options.region = options.region || 'us-central';
return new Promise((resolve, reject) => {
- this.rest.makeRequest('post', Constants.Endpoints.guilds, true, options)
- .then(data => {
- if (this.rest.client.guilds.has(data.id)) resolve(this.rest.client.guilds.get(data.id));
+ this.rest.makeRequest('post', Constants.Endpoints.guilds, true, options).then(data => {
+ if (this.client.guilds.has(data.id)) {
+ resolve(this.client.guilds.get(data.id));
+ return;
+ }
+
const handleGuild = guild => {
- if (guild.id === data.id) resolve(guild);
- this.rest.client.removeListener('guildCreate', handleGuild);
+ if (guild.id === data.id) {
+ this.client.removeListener('guildCreate', handleGuild);
+ this.client.clearTimeout(timeout);
+ resolve(guild);
+ }
};
- this.rest.client.on('guildCreate', handleGuild);
- this.rest.client.setTimeout(() => {
- this.rest.client.removeListener('guildCreate', handleGuild);
- reject(new Error('Took too long to receive guild data'));
+ this.client.on('guildCreate', handleGuild);
+
+ const timeout = this.client.setTimeout(() => {
+ this.client.removeListener('guildCreate', handleGuild);
+ reject(new Error('Took too long to receive guild data.'));
}, 10000);
- }).catch(reject);
+ }, reject);
});
}
// untested but probably will work
deleteGuild(guild) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('del', Constants.Endpoints.guild(guild.id), true).then(() => {
- resolve(this.rest.client.actions.GuildDelete.handle({ id: guild.id }).guild);
- }).catch(reject);
- });
+ return this.rest.makeRequest('del', Constants.Endpoints.guild(guild.id), true).then(() =>
+ this.client.actions.GuildDelete.handle({ id: guild.id }).guild
+ );
}
getUser(userID) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.user(userID), true).then((data) => {
- resolve(this.rest.client.actions.UserGet.handle(data).user);
- }).catch(reject);
- });
+ return this.rest.makeRequest('get', Constants.Endpoints.user(userID), true).then(data =>
+ this.client.actions.UserGet.handle(data).user
+ );
}
- updateCurrentUser(_data) {
- return new Promise((resolve, reject) => {
- const user = this.rest.client.user;
-
- const data = {};
- data.username = _data.username || user.username;
- data.avatar = this.rest.client.resolver.resolveBase64(_data.avatar) || user.avatar;
- if (!user.bot) {
- data.email = _data.email || user.email;
- data.password = this.rest.client.password;
- if (_data.new_password) data.new_password = _data.newPassword;
- }
-
- this.rest.makeRequest('patch', Constants.Endpoints.me, true, data)
- .then(newData => resolve(this.rest.client.actions.UserUpdate.handle(newData).updated))
- .catch(reject);
- });
+ updateCurrentUser(_data, password) {
+ const user = this.client.user;
+ const data = {};
+ data.username = _data.username || user.username;
+ data.avatar = this.client.resolver.resolveBase64(_data.avatar) || user.avatar;
+ if (!user.bot) {
+ data.email = _data.email || user.email;
+ data.password = password;
+ if (_data.new_password) data.new_password = _data.newPassword;
+ }
+ return this.rest.makeRequest('patch', Constants.Endpoints.me, true, data).then(newData =>
+ this.client.actions.UserUpdate.handle(newData).updated
+ );
}
updateGuild(guild, _data) {
- return new Promise((resolve, reject) => {
- const data = {};
- if (_data.name) data.name = _data.name;
- if (_data.region) data.region = _data.region;
- if (_data.verificationLevel) data.verification_level = Number(_data.verificationLevel);
- if (_data.afkChannel) data.afk_channel_id = this.rest.client.resolver.resolveChannel(_data.afkChannel).id;
- if (_data.afkTimeout) data.afk_timeout = Number(_data.afkTimeout);
- if (_data.icon) data.icon = this.rest.client.resolver.resolveBase64(_data.icon);
- if (_data.owner) data.owner_id = this.rest.client.resolver.resolveUser(_data.owner).id;
- if (_data.splash) data.splash = this.rest.client.resolver.resolveBase64(_data.splash);
-
- this.rest.makeRequest('patch', Constants.Endpoints.guild(guild.id), true, data)
- .then(newData => resolve(this.rest.client.actions.GuildUpdate.handle(newData).updated))
- .catch(reject);
- });
+ const data = {};
+ if (_data.name) data.name = _data.name;
+ if (_data.region) data.region = _data.region;
+ if (_data.verificationLevel) data.verification_level = Number(_data.verificationLevel);
+ if (_data.afkChannel) data.afk_channel_id = this.client.resolver.resolveChannel(_data.afkChannel).id;
+ if (_data.afkTimeout) data.afk_timeout = Number(_data.afkTimeout);
+ if (_data.icon) data.icon = this.client.resolver.resolveBase64(_data.icon);
+ if (_data.owner) data.owner_id = this.client.resolver.resolveUser(_data.owner).id;
+ if (_data.splash) data.splash = this.client.resolver.resolveBase64(_data.splash);
+ return this.rest.makeRequest('patch', Constants.Endpoints.guild(guild.id), true, data).then(newData =>
+ this.client.actions.GuildUpdate.handle(newData).updated
+ );
}
kickGuildMember(guild, member) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('del', Constants.Endpoints.guildMember(guild.id, member.id), true).then(() => {
- resolve(this.rest.client.actions.GuildMemberRemove.handle({
- guild_id: guild.id,
- user: member.user,
- }).member);
- }).catch(reject);
- });
+ return this.rest.makeRequest('del', Constants.Endpoints.guildMember(guild.id, member.id), true).then(() =>
+ this.client.actions.GuildMemberRemove.handle({
+ guild_id: guild.id,
+ user: member.user,
+ }).member
+ );
}
createGuildRole(guild) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('post', Constants.Endpoints.guildRoles(guild.id), true).then(role => {
- resolve(this.rest.client.actions.GuildRoleCreate.handle({
- guild_id: guild.id,
- role,
- }).role);
- }).catch(reject);
- });
+ return this.rest.makeRequest('post', Constants.Endpoints.guildRoles(guild.id), true).then(role =>
+ this.client.actions.GuildRoleCreate.handle({
+ guild_id: guild.id,
+ role,
+ }).role
+ );
}
deleteGuildRole(role) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('del', Constants.Endpoints.guildRole(role.guild.id, role.id), true).then(() => {
- resolve(this.rest.client.actions.GuildRoleDelete.handle({
- guild_id: role.guild.id,
- role_id: role.id,
- }).role);
- }).catch(reject);
- });
+ return this.rest.makeRequest('del', Constants.Endpoints.guildRole(role.guild.id, role.id), true).then(() =>
+ this.client.actions.GuildRoleDelete.handle({
+ guild_id: role.guild.id,
+ role_id: role.id,
+ }).role
+ );
}
setChannelOverwrite(channel, payload) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('put', `${Constants.Endpoints.channelPermissions(channel.id)}/${payload.id}`, true, payload)
- .then(resolve)
- .catch(reject);
- });
+ return this.rest.makeRequest(
+ 'put', `${Constants.Endpoints.channelPermissions(channel.id)}/${payload.id}`, true, payload
+ );
}
deletePermissionOverwrites(overwrite) {
- return new Promise((resolve, reject) => {
- const endpoint = `${Constants.Endpoints.channelPermissions(overwrite.channel.id)}/${overwrite.id}`;
- this.rest.makeRequest('del', endpoint, true)
- .then(() => resolve(overwrite))
- .catch(reject);
- });
+ return this.rest.makeRequest(
+ 'del', `${Constants.Endpoints.channelPermissions(overwrite.channel.id)}/${overwrite.id}`, true
+ ).then(() => overwrite);
}
getChannelMessages(channel, payload = {}) {
- return new Promise((resolve, reject) => {
- const params = [];
- if (payload.limit) params.push(`limit=${payload.limit}`);
- if (payload.around) params.push(`around=${payload.around}`);
- else if (payload.before) params.push(`before=${payload.before}`);
- else if (payload.after) params.push(`after=${payload.after}`);
+ const params = [];
+ if (payload.limit) params.push(`limit=${payload.limit}`);
+ if (payload.around) params.push(`around=${payload.around}`);
+ else if (payload.before) params.push(`before=${payload.before}`);
+ else if (payload.after) params.push(`after=${payload.after}`);
- let endpoint = Constants.Endpoints.channelMessages(channel.id);
- if (params.length > 0) endpoint += `?${params.join('&')}`;
- this.rest.makeRequest('get', endpoint, true)
- .then(resolve)
- .catch(reject);
- });
+ let endpoint = Constants.Endpoints.channelMessages(channel.id);
+ if (params.length > 0) endpoint += `?${params.join('&')}`;
+ return this.rest.makeRequest('get', endpoint, true);
}
getChannelMessage(channel, messageID) {
- return new Promise((resolve, reject) => {
- const msg = channel.messages.get(messageID);
- if (msg) return resolve(msg);
-
- const endpoint = Constants.Endpoints.channelMessage(channel.id, messageID);
- return this.rest.makeRequest('get', endpoint, true)
- .then(resolve)
- .catch(reject);
- });
+ const msg = channel.messages.get(messageID);
+ if (msg) return Promise.resolve(msg);
+ return this.rest.makeRequest('get', Constants.Endpoints.channelMessage(channel.id, messageID), true);
}
getGuildMember(guild, user) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.guildMember(guild.id, user.id), true).then((data) => {
- resolve(this.rest.client.actions.GuildMemberGet.handle(guild, data).member);
- }).catch(reject);
- });
+ return this.rest.makeRequest('get', Constants.Endpoints.guildMember(guild.id, user.id), true).then(data =>
+ this.client.actions.GuildMemberGet.handle(guild, data).member
+ );
}
updateGuildMember(member, data) {
- return new Promise((resolve, reject) => {
- if (data.channel) data.channel_id = this.rest.client.resolver.resolveChannel(data.channel).id;
- if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role);
+ if (data.channel) data.channel_id = this.client.resolver.resolveChannel(data.channel).id;
+ if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role);
- let endpoint = Constants.Endpoints.guildMember(member.guild.id, member.id);
- // fix your endpoints, discord ;-;
- if (member.id === this.rest.client.user.id) {
- if (Object.keys(data).length === 1 && Object.keys(data)[0] === 'nick') {
- endpoint = Constants.Endpoints.stupidInconsistentGuildEndpoint(member.guild.id);
- }
+ let endpoint = Constants.Endpoints.guildMember(member.guild.id, member.id);
+ // fix your endpoints, discord ;-;
+ if (member.id === this.client.user.id) {
+ const keys = Object.keys(data);
+ if (keys.length === 1 && keys[0] === 'nick') {
+ endpoint = Constants.Endpoints.guildMemberNickname(member.guild.id);
}
+ }
- this.rest.makeRequest('patch', endpoint, true, data)
- .then(resData => resolve(member.guild._updateMember(member, resData).mem))
- .catch(reject);
+ return this.rest.makeRequest('patch', endpoint, true, data).then(newData =>
+ member.guild._updateMember(member, newData).mem
+ );
+ }
+
+ addMemberRole(member, role) {
+ return this.rest.makeRequest('put', Constants.Endpoints.guildMemberRole(member.guild.id, member.id, role.id), true)
+ .then(() => {
+ if (!member._roles.includes(role.id)) member._roles.push(role.id);
+ return member;
+ });
+ }
+
+ removeMemberRole(member, role) {
+ return this.rest.makeRequest(
+ 'delete',
+ Constants.Endpoints.guildMemberRole(member.guild.id, member.id, role.id),
+ true
+ ).then(() => {
+ const index = member._roles.indexOf(role.id);
+ if (index >= 0) member._roles.splice(index, 1);
+ return member;
});
}
sendTyping(channelID) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('post', `${Constants.Endpoints.channel(channelID)}/typing`, true)
- .then(resolve)
- .catch(reject);
- });
+ return this.rest.makeRequest('post', `${Constants.Endpoints.channel(channelID)}/typing`, true);
}
banGuildMember(guild, member, deleteDays = 0) {
- return new Promise((resolve, reject) => {
- const id = this.rest.client.resolver.resolveUserID(member);
- if (!id) throw new Error('Couldn\'t resolve the user ID to ban.');
-
- this.rest.makeRequest('put',
- `${Constants.Endpoints.guildBans(guild.id)}/${id}?delete-message-days=${deleteDays}`, true, {
- 'delete-message-days': deleteDays,
- }).then(() => {
- if (member instanceof GuildMember) {
- resolve(member);
- return;
- }
- const user = this.rest.client.resolver.resolveUser(id);
- if (user) {
- member = this.rest.client.resolver.resolveGuildMember(guild, user);
- resolve(member || user);
- return;
- }
- resolve(id);
- }).catch(reject);
+ const id = this.client.resolver.resolveUserID(member);
+ if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.'));
+ return this.rest.makeRequest(
+ 'put', `${Constants.Endpoints.guildBans(guild.id)}/${id}?delete-message-days=${deleteDays}`, true, {
+ 'delete-message-days': deleteDays,
+ }
+ ).then(() => {
+ if (member instanceof GuildMember) return member;
+ const user = this.client.resolver.resolveUser(id);
+ if (user) {
+ member = this.client.resolver.resolveGuildMember(guild, user);
+ return member || user;
+ }
+ return id;
});
}
unbanGuildMember(guild, member) {
return new Promise((resolve, reject) => {
- const id = this.rest.client.resolver.resolveUserID(member);
- if (!id) throw new Error('Couldn\'t resolve the user ID to ban.');
+ const id = this.client.resolver.resolveUserID(member);
+ if (!id) throw new Error('Couldn\'t resolve the user ID to unban.');
const listener = (eGuild, eUser) => {
if (eGuild.id === guild.id && eUser.id === id) {
- this.rest.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
+ this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
+ this.client.clearTimeout(timeout);
resolve(eUser);
}
};
- this.rest.client.on(Constants.Events.GUILD_BAN_REMOVE, listener);
- this.rest.makeRequest('del', `${Constants.Endpoints.guildBans(guild.id)}/${id}`, true).catch(reject);
+ this.client.on(Constants.Events.GUILD_BAN_REMOVE, listener);
+
+ const timeout = this.client.setTimeout(() => {
+ this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
+ reject(new Error('Took too long to receive the ban remove event.'));
+ }, 10000);
+
+ this.rest.makeRequest('del', `${Constants.Endpoints.guildBans(guild.id)}/${id}`, true).catch(err => {
+ this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener);
+ this.client.clearTimeout(timeout);
+ reject(err);
+ });
});
}
getGuildBans(guild) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.guildBans(guild.id), true).then(banItems => {
- const bannedUsers = new Collection();
- for (const banItem of banItems) {
- const user = this.rest.client.dataManager.newUser(banItem.user);
- bannedUsers.set(user.id, user);
- }
- resolve(bannedUsers);
- }).catch(reject);
+ return this.rest.makeRequest('get', Constants.Endpoints.guildBans(guild.id), true).then(banItems => {
+ const bannedUsers = new Collection();
+ for (const banItem of banItems) {
+ const user = this.client.dataManager.newUser(banItem.user);
+ bannedUsers.set(user.id, user);
+ }
+ return bannedUsers;
});
}
updateGuildRole(role, _data) {
- return new Promise((resolve, reject) => {
- const data = {};
- data.name = _data.name || role.name;
- data.position = typeof _data.position !== 'undefined' ? _data.position : role.position;
- data.color = _data.color || role.color;
- if (typeof data.color === 'string' && data.color.startsWith('#')) {
- data.color = parseInt(data.color.replace('#', ''), 16);
- }
- data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
- data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable;
+ const data = {};
+ data.name = _data.name || role.name;
+ data.position = typeof _data.position !== 'undefined' ? _data.position : role.position;
+ data.color = _data.color || role.color;
+ if (typeof data.color === 'string' && data.color.startsWith('#')) {
+ data.color = parseInt(data.color.replace('#', ''), 16);
+ }
+ data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
+ data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable;
- if (_data.permissions) {
- let perms = 0;
- for (let perm of _data.permissions) {
- if (typeof perm === 'string') perm = Constants.PermissionFlags[perm];
- perms |= perm;
- }
- data.permissions = perms;
- } else {
- data.permissions = role.permissions;
+ if (_data.permissions) {
+ let perms = 0;
+ for (let perm of _data.permissions) {
+ if (typeof perm === 'string') perm = Constants.PermissionFlags[perm];
+ perms |= perm;
}
+ data.permissions = perms;
+ } else {
+ data.permissions = role.permissions;
+ }
- this.rest.makeRequest('patch', Constants.Endpoints.guildRole(role.guild.id, role.id), true, data).then(_role => {
- resolve(this.rest.client.actions.GuildRoleUpdate.handle({
- role: _role,
- guild_id: role.guild.id,
- }).updated);
- }).catch(reject);
- });
+ return this.rest.makeRequest(
+ 'patch', Constants.Endpoints.guildRole(role.guild.id, role.id), true, data
+ ).then(_role =>
+ this.client.actions.GuildRoleUpdate.handle({
+ role: _role,
+ guild_id: role.guild.id,
+ }).updated
+ );
}
pinMessage(message) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('put', `${Constants.Endpoints.channel(message.channel.id)}/pins/${message.id}`, true)
- .then(() => resolve(message))
- .catch(reject);
- });
+ return this.rest.makeRequest('put', `${Constants.Endpoints.channel(message.channel.id)}/pins/${message.id}`, true)
+ .then(() => message);
}
unpinMessage(message) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('del', `${Constants.Endpoints.channel(message.channel.id)}/pins/${message.id}`, true)
- .then(() => resolve(message))
- .catch(reject);
- });
+ return this.rest.makeRequest('del', `${Constants.Endpoints.channel(message.channel.id)}/pins/${message.id}`, true)
+ .then(() => message);
}
getChannelPinnedMessages(channel) {
@@ -499,131 +446,85 @@ class RESTMethods {
}
createChannelInvite(channel, options) {
- return new Promise((resolve, reject) => {
- const payload = {};
- payload.temporary = options.temporary;
- payload.max_age = options.maxAge;
- payload.max_uses = options.maxUses;
-
- this.rest.makeRequest('post', `${Constants.Endpoints.channelInvites(channel.id)}`, true, payload)
- .then(invite => resolve(new Invite(this.rest.client, invite)))
- .catch(reject);
- });
+ const payload = {};
+ payload.temporary = options.temporary;
+ payload.max_age = options.maxAge;
+ payload.max_uses = options.maxUses;
+ return this.rest.makeRequest('post', `${Constants.Endpoints.channelInvites(channel.id)}`, true, payload)
+ .then(invite => new Invite(this.client, invite));
}
deleteInvite(invite) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('del', Constants.Endpoints.invite(invite.code), true)
- .then(() => resolve(invite))
- .catch(reject);
- });
+ return this.rest.makeRequest('del', Constants.Endpoints.invite(invite.code), true).then(() => invite);
}
getInvite(code) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.invite(code), true)
- .then(invite => resolve(new Invite(this.rest.client, invite)))
- .catch(reject);
- });
+ return this.rest.makeRequest('get', Constants.Endpoints.invite(code), true).then(invite =>
+ new Invite(this.client, invite)
+ );
}
getGuildInvites(guild) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.guildInvites(guild.id), true).then(inviteItems => {
- const invites = new Collection();
- for (const inviteItem of inviteItems) {
- const invite = new Invite(this.rest.client, inviteItem);
- invites.set(invite.code, invite);
- }
- resolve(invites);
- }).catch(reject);
+ return this.rest.makeRequest('get', Constants.Endpoints.guildInvites(guild.id), true).then(inviteItems => {
+ const invites = new Collection();
+ for (const inviteItem of inviteItems) {
+ const invite = new Invite(this.client, inviteItem);
+ invites.set(invite.code, invite);
+ }
+ return invites;
});
}
pruneGuildMembers(guild, days, dry) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest(dry ? 'get' : 'post', `${Constants.Endpoints.guildPrune(guild.id)}?days=${days}`, true)
- .then(data => {
- resolve(data.pruned);
- }).catch(reject);
- });
+ return this.rest.makeRequest(dry ? 'get' : 'post', `${Constants.Endpoints.guildPrune(guild.id)}?days=${days}`, true)
+ .then(data => data.pruned);
}
createEmoji(guild, image, name) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('post', `${Constants.Endpoints.guildEmojis(guild.id)}`, true, { name: name, image: image })
- .then(data => {
- resolve(this.rest.client.actions.EmojiCreate.handle(data, guild).emoji);
- }).catch(reject);
- });
+ return this.rest.makeRequest('post', `${Constants.Endpoints.guildEmojis(guild.id)}`, true, { name, image })
+ .then(data => this.client.actions.EmojiCreate.handle(data, guild).emoji);
}
deleteEmoji(emoji) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('delete', `${Constants.Endpoints.guildEmojis(emoji.guild.id)}/${emoji.id}`, true)
- .then(() => {
- resolve(this.rest.client.actions.EmojiDelete.handle(emoji).data);
- }).catch(reject);
- });
+ return this.rest.makeRequest('delete', `${Constants.Endpoints.guildEmojis(emoji.guild.id)}/${emoji.id}`, true)
+ .then(() => this.client.actions.EmojiDelete.handle(emoji).data);
}
getWebhook(id, token) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.webhook(id, token), require('util').isUndefined(token))
- .then(data => {
- resolve(new Webhook(this.rest.client, data));
- }).catch(reject);
- });
+ return this.rest.makeRequest('get', Constants.Endpoints.webhook(id, token), !token).then(data =>
+ new Webhook(this.client, data)
+ );
}
getGuildWebhooks(guild) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.guildWebhooks(guild.id), true)
- .then(data => {
- const hooks = new Collection();
- for (const hook of data) {
- hooks.set(hook.id, new Webhook(this.rest.client, hook));
- }
- resolve(hooks);
- }).catch(reject);
+ return this.rest.makeRequest('get', Constants.Endpoints.guildWebhooks(guild.id), true).then(data => {
+ const hooks = new Collection();
+ for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
+ return hooks;
});
}
getChannelWebhooks(channel) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.channelWebhooks(channel.id), true)
- .then(data => {
- const hooks = new Collection();
- for (const hook of data) {
- hooks.set(hook.id, new Webhook(this.rest.client, hook));
- }
- resolve(hooks);
- }).catch(reject);
+ return this.rest.makeRequest('get', Constants.Endpoints.channelWebhooks(channel.id), true).then(data => {
+ const hooks = new Collection();
+ for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
+ return hooks;
});
}
createWebhook(channel, name, avatar) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('post', Constants.Endpoints.channelWebhooks(channel.id), true, {
- name,
- avatar,
- })
- .then(data => {
- resolve(new Webhook(this.rest.client, data));
- }).catch(reject);
- });
+ return this.rest.makeRequest('post', Constants.Endpoints.channelWebhooks(channel.id), true, { name, avatar })
+ .then(data => new Webhook(this.client, data));
}
editWebhook(webhook, name, avatar) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('patch', Constants.Endpoints.webhook(webhook.id, webhook.token), false, {
- name,
- avatar,
- }).then(data => {
- webhook.name = data.name;
- webhook.avatar = data.avatar;
- resolve(webhook);
- }).catch(reject);
+ return this.rest.makeRequest('patch', Constants.Endpoints.webhook(webhook.id, webhook.token), false, {
+ name,
+ avatar,
+ }).then(data => {
+ webhook.name = data.name;
+ webhook.avatar = data.avatar;
+ return webhook;
});
}
@@ -632,93 +533,120 @@ class RESTMethods {
}
sendWebhookMessage(webhook, content, { avatarURL, tts, disableEveryone, embeds } = {}, file = null) {
- return new Promise((resolve, reject) => {
- if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
-
- if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
- content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
+ if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content);
+ if (content) {
+ if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) {
+ content = content.replace(/@(everyone|here)/g, '@\u200b$1');
}
-
- this.rest.makeRequest('post', `${Constants.Endpoints.webhook(webhook.id, webhook.token)}?wait=true`, false, {
- content: content, username: webhook.name, avatar_url: avatarURL, tts: tts, file: file, embeds: embeds,
- })
- .then(data => {
- resolve(data);
- }).catch(reject);
+ }
+ return this.rest.makeRequest('post', `${Constants.Endpoints.webhook(webhook.id, webhook.token)}?wait=true`, false, {
+ username: webhook.name,
+ avatar_url: avatarURL,
+ content,
+ tts,
+ file,
+ embeds,
});
}
sendSlackWebhookMessage(webhook, body) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest(
- 'post',
- `${Constants.Endpoints.webhook(webhook.id, webhook.token)}/slack?wait=true`,
- false,
- body
- ).then(data => {
- resolve(data);
- }).catch(reject);
- });
- }
-
- addFriend(user) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('post', Constants.Endpoints.relationships('@me'), true, {
- discriminator: user.discriminator,
- username: user.username,
- }).then(() => {
- resolve(user);
- }).catch(reject);
- });
- }
-
- removeFriend(user) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true)
- .then(() => {
- resolve(user);
- }).catch(reject);
- });
+ return this.rest.makeRequest(
+ 'post', `${Constants.Endpoints.webhook(webhook.id, webhook.token)}/slack?wait=true`, false, body
+ );
}
fetchUserProfile(user) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('get', Constants.Endpoints.userProfile(user.id), true)
- .then(data => {
- resolve(new UserProfile(user, data));
- }).catch(reject);
- });
+ return this.rest.makeRequest('get', Constants.Endpoints.userProfile(user.id), true).then(data =>
+ new UserProfile(user, data)
+ );
+ }
+
+ fetchMeMentions(options) {
+ if (options.guild) options.guild = options.guild.id ? options.guild.id : options.guild;
+ return this.rest.makeRequest(
+ 'get',
+ Constants.Endpoints.meMentions(options.limit, options.roles, options.everyone, options.guild)
+ ).then(res => res.body.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)));
+ }
+
+ addFriend(user) {
+ return this.rest.makeRequest('post', Constants.Endpoints.relationships('@me'), true, {
+ username: user.username,
+ discriminator: user.discriminator,
+ }).then(() => user);
+ }
+
+ removeFriend(user) {
+ return this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true)
+ .then(() => user);
}
blockUser(user) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('put', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true, { type: 2 })
- .then(() => {
- resolve(user);
- }).catch(reject);
- });
+ return this.rest.makeRequest('put', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true, { type: 2 })
+ .then(() => user);
}
unblockUser(user) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true)
- .then(() => {
- resolve(user);
- }).catch(reject);
- });
+ return this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true)
+ .then(() => user);
}
setRolePositions(guildID, roles) {
- return new Promise((resolve, reject) => {
- this.rest.makeRequest('patch', Constants.Endpoints.guildRoles(guildID), true, roles)
- .then(() => {
- resolve(this.rest.client.actions.GuildRolesPositionUpdate.handle({
- guild_id: guildID,
- roles,
- }).guild);
- })
- .catch(reject);
- });
+ return this.rest.makeRequest('patch', Constants.Endpoints.guildRoles(guildID), true, roles).then(() =>
+ this.client.actions.GuildRolesPositionUpdate.handle({
+ guild_id: guildID,
+ roles,
+ }).guild
+ );
+ }
+
+ addMessageReaction(message, emoji) {
+ return this.rest.makeRequest(
+ 'put', Constants.Endpoints.selfMessageReaction(message.channel.id, message.id, emoji), true
+ ).then(() =>
+ this.client.actions.MessageReactionAdd.handle({
+ user_id: this.client.user.id,
+ message_id: message.id,
+ emoji: parseEmoji(emoji),
+ channel_id: message.channel.id,
+ }).reaction
+ );
+ }
+
+ removeMessageReaction(message, emoji, user) {
+ let endpoint = Constants.Endpoints.selfMessageReaction(message.channel.id, message.id, emoji);
+ if (user.id !== this.client.user.id) {
+ endpoint = Constants.Endpoints.userMessageReaction(message.channel.id, message.id, emoji, null, user.id);
+ }
+ return this.rest.makeRequest('delete', endpoint, true).then(() =>
+ this.client.actions.MessageReactionRemove.handle({
+ user_id: user.id,
+ message_id: message.id,
+ emoji: parseEmoji(emoji),
+ channel_id: message.channel.id,
+ }).reaction
+ );
+ }
+
+ removeMessageReactions(message) {
+ return this.rest.makeRequest('delete', Constants.Endpoints.messageReactions(message.channel.id, message.id), true)
+ .then(() => message);
+ }
+
+ getMessageReactionUsers(message, emoji, limit = 100) {
+ return this.rest.makeRequest(
+ 'get', Constants.Endpoints.messageReaction(message.channel.id, message.id, emoji, limit), true
+ );
+ }
+
+ getMyApplication() {
+ return this.rest.makeRequest('get', Constants.Endpoints.myApplication, true).then(app =>
+ new ClientOAuth2Application(this.client, app)
+ );
+ }
+
+ setNote(user, note) {
+ return this.rest.makeRequest('put', Constants.Endpoints.note(user.id), true, { note }).then(() => user);
}
}
diff --git a/src/client/rest/RequestHandlers/Burst.js b/src/client/rest/RequestHandlers/Burst.js
index e43df3952..2cc1a590c 100644
--- a/src/client/rest/RequestHandlers/Burst.js
+++ b/src/client/rest/RequestHandlers/Burst.js
@@ -29,7 +29,9 @@ class BurstRequestHandler extends RequestHandler {
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
- this.handleNext((this.requestResetTime - Date.now()) + this.timeDifference + 1000);
+ this.handleNext(
+ this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset
+ );
}
if (err) {
if (err.status === 429) {
@@ -38,10 +40,8 @@ class BurstRequestHandler extends RequestHandler {
this.restManager.client.setTimeout(() => {
this.globalLimit = false;
this.handle();
- }, Number(res.headers['retry-after']) + 500);
- if (res.headers['x-ratelimit-global']) {
- this.globalLimit = true;
- }
+ }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
+ if (res.headers['x-ratelimit-global']) this.globalLimit = true;
} else {
item.reject(err);
}
diff --git a/src/client/rest/RequestHandlers/Sequential.js b/src/client/rest/RequestHandlers/Sequential.js
index c971c198f..0abf36db2 100644
--- a/src/client/rest/RequestHandlers/Sequential.js
+++ b/src/client/rest/RequestHandlers/Sequential.js
@@ -60,10 +60,8 @@ class SequentialRequestHandler extends RequestHandler {
this.waiting = false;
this.globalLimit = false;
resolve();
- }, Number(res.headers['retry-after']) + 500);
- if (res.headers['x-ratelimit-global']) {
- this.globalLimit = true;
- }
+ }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
+ if (res.headers['x-ratelimit-global']) this.globalLimit = true;
} else {
this.queue.shift();
this.waiting = false;
@@ -76,10 +74,13 @@ class SequentialRequestHandler extends RequestHandler {
const data = res && res.body ? res.body : {};
item.resolve(data);
if (this.requestRemaining === 0) {
- this.restManager.client.setTimeout(() => {
- this.waiting = false;
- resolve(data);
- }, (this.requestResetTime - Date.now()) + this.timeDifference + 1000);
+ this.restManager.client.setTimeout(
+ () => {
+ this.waiting = false;
+ resolve(data);
+ },
+ this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset
+ );
} else {
this.waiting = false;
resolve(data);
diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js
index e32eae4bf..ea8374540 100644
--- a/src/client/voice/ClientVoiceManager.js
+++ b/src/client/voice/ClientVoiceManager.js
@@ -23,7 +23,7 @@ class ClientVoiceManager {
this.connections = new Collection();
/**
- * Pending connection attempts, maps Guild ID to VoiceChannel
+ * Pending connection attempts, maps guild ID to VoiceChannel
* @type {Collection}
*/
this.pending = new Collection();
@@ -55,7 +55,7 @@ class ClientVoiceManager {
throw new Error('There is no permission set for the client user in this channel - are they part of the guild?');
}
if (!permissions.hasPermission('CONNECT')) {
- throw new Error('You do not have permission to connect to this voice channel.');
+ throw new Error('You do not have permission to join this voice channel.');
}
options = mergeDefault({
@@ -79,10 +79,7 @@ class ClientVoiceManager {
joinChannel(channel) {
return new Promise((resolve, reject) => {
if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.');
-
- if (!channel.joinable) {
- throw new Error('You do not have permission to join this voice channel');
- }
+ if (!channel.joinable) throw new Error('You do not have permission to join this voice channel.');
const existingConnection = this.connections.get(channel.guild.id);
if (existingConnection) {
@@ -142,7 +139,7 @@ class PendingVoiceConnection extends EventEmitter {
/**
* An object containing data required to connect to the voice servers with
- * @type {object}
+ * @type {Object}
*/
this.data = {};
diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js
index 28bd60aaa..ac44ff866 100644
--- a/src/client/voice/VoiceConnection.js
+++ b/src/client/voice/VoiceConnection.js
@@ -7,7 +7,7 @@ const EventEmitter = require('events').EventEmitter;
const fs = require('fs');
/**
- * Represents a connection to a Voice Channel in Discord.
+ * Represents a connection to a voice channel in Discord.
* ```js
* // obtained using:
* voiceChannel.join().then(connection => {
@@ -17,9 +17,9 @@ const fs = require('fs');
* @extends {EventEmitter}
*/
class VoiceConnection extends EventEmitter {
-
constructor(pendingConnection) {
super();
+
/**
* The Voice Manager that instantiated this connection
* @type {ClientVoiceManager}
@@ -46,7 +46,7 @@ class VoiceConnection extends EventEmitter {
/**
* The authentication data needed to connect to the voice server
- * @type {object}
+ * @type {Object}
* @private
*/
this.authentication = pendingConnection.data;
@@ -70,7 +70,7 @@ class VoiceConnection extends EventEmitter {
/**
* Warning info from the connection
* @event VoiceConnection#warn
- * @param {string|error} warning the warning
+ * @param {string|Error} warning the warning
*/
this.emit('warn', e);
this.player.cleanup();
@@ -83,9 +83,16 @@ class VoiceConnection extends EventEmitter {
*/
this.ssrcMap = new Map();
+ /**
+ * Whether this connection is ready
+ * @type {boolean}
+ * @private
+ */
+ this.ready = false;
+
/**
* Object that wraps contains the `ws` and `udp` sockets of this voice connection
- * @type {object}
+ * @type {Object}
* @private
*/
this.sockets = {};
@@ -106,8 +113,7 @@ class VoiceConnection extends EventEmitter {
speaking: true,
delay: 0,
},
- })
- .catch(e => {
+ }).catch(e => {
this.emit('debug', e);
});
}
@@ -155,8 +161,7 @@ class VoiceConnection extends EventEmitter {
this.sockets.udp.findEndpointAddress()
.then(address => {
this.sockets.udp.createUDPSocket(address);
- })
- .catch(e => this.emit('error', e));
+ }, e => this.emit('error', e));
});
this.sockets.ws.once('sessionDescription', (mode, secret) => {
this.authentication.encryptionMode = mode;
@@ -167,6 +172,7 @@ class VoiceConnection extends EventEmitter {
* @event VoiceConnection#ready
*/
this.emit('ready');
+ this.ready = true;
});
this.sockets.ws.on('speaking', data => {
const guild = this.channel.guild;
@@ -253,7 +259,7 @@ class VoiceConnection extends EventEmitter {
*/
playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
const options = { seek, volume, passes };
- return this.player.playPCMStream(stream, options);
+ return this.player.playPCMStream(stream, null, options);
}
/**
diff --git a/src/client/voice/VoiceUDPClient.js b/src/client/voice/VoiceUDPClient.js
index 8246478c3..b7b0c0cfe 100644
--- a/src/client/voice/VoiceUDPClient.js
+++ b/src/client/voice/VoiceUDPClient.js
@@ -3,18 +3,6 @@ const dns = require('dns');
const Constants = require('../../util/Constants');
const EventEmitter = require('events').EventEmitter;
-function parseLocalPacket(message) {
- try {
- const packet = new Buffer(message);
- let address = '';
- for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]);
- const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
- return { address, port };
- } catch (error) {
- return { error };
- }
-}
-
/**
* Represents a UDP Client for a Voice Connection
* @extends {EventEmitter}
@@ -142,4 +130,16 @@ class VoiceConnectionUDPClient extends EventEmitter {
}
}
+function parseLocalPacket(message) {
+ try {
+ const packet = new Buffer(message);
+ let address = '';
+ for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]);
+ const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
+ return { address, port };
+ } catch (error) {
+ return { error };
+ }
+}
+
module.exports = VoiceConnectionUDPClient;
diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js
index ebc2a3103..bafa5dde3 100644
--- a/src/client/voice/VoiceWebSocket.js
+++ b/src/client/voice/VoiceWebSocket.js
@@ -1,8 +1,14 @@
-const WebSocket = require('ws');
const Constants = require('../../util/Constants');
const SecretKey = require('./util/SecretKey');
const EventEmitter = require('events').EventEmitter;
+let WebSocket;
+try {
+ WebSocket = require('uws');
+} catch (err) {
+ WebSocket = require('ws');
+}
+
/**
* Represents a Voice Connection's WebSocket
* @extends {EventEmitter}
diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js
index 961ad97f4..e08a36537 100644
--- a/src/client/voice/dispatcher/StreamDispatcher.js
+++ b/src/client/voice/dispatcher/StreamDispatcher.js
@@ -10,9 +10,7 @@ nonce.fill(0);
* // obtained using:
* voiceChannel.join().then(connection => {
* // you can play a file or a stream here:
- * connection.playFile('./file.mp3').then(dispatcher => {
- *
- * });
+ * const dispatcher = connection.playFile('./file.mp3');
* });
* ```
* @extends {EventEmitter}
@@ -116,9 +114,10 @@ class StreamDispatcher extends EventEmitter {
/**
* Stops the current stream permanently and emits an `end` event.
+ * @param {string} [reason='user'] An optional reason for stopping the dispatcher.
*/
- end() {
- this._triggerTerminalState('end', 'user requested');
+ end(reason = 'user') {
+ this._triggerTerminalState('end', reason);
}
_setSpeaking(value) {
@@ -136,7 +135,7 @@ class StreamDispatcher extends EventEmitter {
const packet = this._createPacket(sequence, timestamp, this.player.opusEncoder.encode(buffer));
while (repeats--) {
this.player.voiceConnection.sockets.udp.send(packet)
- .catch(e => this.emit('debug', `failed to send a packet ${e}`));
+ .catch(e => this.emit('debug', `Failed to send a packet ${e}`));
}
}
@@ -223,7 +222,7 @@ class StreamDispatcher extends EventEmitter {
buffer = this._applyVolume(buffer);
data.count++;
- data.sequence = (data.sequence + 1) < (65536) ? data.sequence + 1 : 0;
+ data.sequence = (data.sequence + 1) < 65536 ? data.sequence + 1 : 0;
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
this._sendBuffer(buffer, data.sequence, data.timestamp);
@@ -235,12 +234,14 @@ class StreamDispatcher extends EventEmitter {
}
}
- _triggerEnd() {
+ _triggerEnd(reason) {
/**
* Emitted once the stream has ended. Attach a `once` listener to this.
* @event StreamDispatcher#end
+ * @param {string} reason The reason for the end of the dispatcher. If it ended because it reached the end of the
+ * stream, this would be `stream`. If you invoke `.end()` without specifying a reason, this would be `user`.
*/
- this.emit('end');
+ this.emit('end', reason);
}
_triggerError(err) {
@@ -282,7 +283,7 @@ class StreamDispatcher extends EventEmitter {
return;
}
- this.stream.on('end', err => this._triggerTerminalState('end', err));
+ this.stream.on('end', err => this._triggerTerminalState('end', err || 'stream'));
this.stream.on('error', err => this._triggerTerminalState('error', err));
const data = this.streamingData;
diff --git a/src/client/voice/pcm/FfmpegConverterEngine.js b/src/client/voice/pcm/FfmpegConverterEngine.js
index 34c5d509a..8fb725bda 100644
--- a/src/client/voice/pcm/FfmpegConverterEngine.js
+++ b/src/client/voice/pcm/FfmpegConverterEngine.js
@@ -67,7 +67,14 @@ class FfmpegConverterEngine extends ConverterEngine {
}
function chooseCommand() {
- for (const cmd of ['ffmpeg', 'avconv', './ffmpeg', './avconv']) {
+ for (const cmd of [
+ 'ffmpeg',
+ 'avconv',
+ './ffmpeg',
+ './avconv',
+ 'node_modules\\ffmpeg-binaries\\bin\\ffmpeg',
+ 'node_modules/ffmpeg-binaries/bin/ffmpeg',
+ ]) {
if (!ChildProcess.spawnSync(cmd, ['-h']).error) return cmd;
}
throw new Error(
diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js
index 8f7110613..b67af5993 100644
--- a/src/client/websocket/WebSocketManager.js
+++ b/src/client/websocket/WebSocketManager.js
@@ -1,9 +1,30 @@
-const WebSocket = require('ws');
+const browser = typeof window !== 'undefined';
const EventEmitter = require('events').EventEmitter;
const Constants = require('../../util/Constants');
+const convertArrayBuffer = require('../../util/ConvertArrayBuffer');
+const pako = require('pako');
const zlib = require('zlib');
const PacketManager = require('./packets/WebSocketPacketManager');
+let WebSocket, erlpack;
+let serialize = JSON.stringify;
+if (browser) {
+ WebSocket = window.WebSocket; // eslint-disable-line no-undef
+} else {
+ try {
+ WebSocket = require('uws');
+ } catch (err) {
+ WebSocket = require('ws');
+ }
+
+ try {
+ erlpack = require('erlpack');
+ serialize = erlpack.pack;
+ } catch (err) {
+ erlpack = null;
+ }
+}
+
/**
* The WebSocket Manager of the Client
* @private
@@ -64,9 +85,11 @@ class WebSocketManager extends EventEmitter {
* @type {Object}
*/
this.disabledEvents = {};
- for (const event in client.options.disabledEvents) this.disabledEvents[event] = true;
+ for (const event of client.options.disabledEvents) this.disabledEvents[event] = true;
this.first = true;
+
+ this.lastHeartbeatAck = true;
}
/**
@@ -78,15 +101,21 @@ class WebSocketManager extends EventEmitter {
this.normalReady = false;
if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING;
this.ws = new WebSocket(gateway);
- this.ws.onopen = () => this.eventOpen();
- this.ws.onclose = (d) => this.eventClose(d);
- this.ws.onmessage = (e) => this.eventMessage(e);
- this.ws.onerror = (e) => this.eventError(e);
+ if (browser) this.ws.binaryType = 'arraybuffer';
+ this.ws.onopen = this.eventOpen.bind(this);
+ this.ws.onmessage = this.eventMessage.bind(this);
+ this.ws.onclose = this.eventClose.bind(this);
+ this.ws.onerror = this.eventError.bind(this);
this._queue = [];
- this._remaining = 3;
+ this._remaining = 120;
+ this.client.setInterval(() => {
+ this._remaining = 120;
+ this._remainingReset = Date.now();
+ }, 60e3);
}
connect(gateway) {
+ gateway = `${gateway}&encoding=${erlpack ? 'etf' : 'json'}`;
if (this.first) {
this._connect(gateway);
this.first = false;
@@ -95,6 +124,22 @@ class WebSocketManager extends EventEmitter {
}
}
+ heartbeat(normal) {
+ if (normal && !this.lastHeartbeatAck) {
+ this.ws.close(1007);
+ return;
+ }
+
+ this.client.emit('debug', 'Sending heartbeat');
+ this.client._pingTimestamp = Date.now();
+ this.client.ws.send({
+ op: Constants.OPCodes.HEARTBEAT,
+ d: this.sequence,
+ }, true);
+
+ this.lastHeartbeatAck = false;
+ }
+
/**
* Sends a packet to the gateway
* @param {Object} data An object that can be JSON stringified
@@ -102,10 +147,10 @@ class WebSocketManager extends EventEmitter {
*/
send(data, force = false) {
if (force) {
- this._send(JSON.stringify(data));
+ this._send(serialize(data));
return;
}
- this._queue.push(JSON.stringify(data));
+ this._queue.push(serialize(data));
this.doQueue();
}
@@ -124,19 +169,15 @@ class WebSocketManager extends EventEmitter {
doQueue() {
const item = this._queue[0];
- if (this.ws.readyState === WebSocket.OPEN && item) {
- if (this._remaining === 0) {
- this.client.setTimeout(() => {
- this.doQueue();
- }, 1000);
- return;
- }
- this._remaining--;
- this._send(item);
- this._queue.shift();
- this.doQueue();
- this.client.setTimeout(() => this._remaining++, 1000);
+ if (!(this.ws.readyState === WebSocket.OPEN && item)) return;
+ if (this.remaining === 0) {
+ this.client.setTimeout(this.doQueue.bind(this), Date.now() - this.remainingReset);
+ return;
}
+ this._remaining--;
+ this._send(item);
+ this._queue.shift();
+ this.doQueue();
}
/**
@@ -144,6 +185,7 @@ class WebSocketManager extends EventEmitter {
*/
eventOpen() {
this.client.emit('debug', 'Connection to gateway opened');
+ this.lastHeartbeatAck = true;
if (this.status === Constants.Status.RECONNECTING) this._sendResume();
else this._sendNewIdentify();
}
@@ -187,18 +229,26 @@ class WebSocketManager extends EventEmitter {
this.sequence = -1;
}
+ /**
+ * @external CloseEvent
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent}
+ */
+
/**
* Run whenever the connection to the gateway is closed, it will try to reconnect the client.
- * @param {Object} event The received websocket data
+ * @param {CloseEvent} event The WebSocket close event
*/
eventClose(event) {
this.emit('close', event);
+ this.client.clearInterval(this.client.manager.heartbeatInterval);
+ this.status = Constants.Status.DISCONNECTED;
+ this._queue = [];
/**
* Emitted whenever the client websocket is disconnected
* @event Client#disconnect
+ * @param {CloseEvent} event The WebSocket close event
*/
- clearInterval(this.client.manager.heartbeatInterval);
- if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT);
+ if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT, event);
if (event.code === 4004) return;
if (event.code === 4010) return;
if (!this.reconnecting && event.code !== 1000) this.tryReconnect();
@@ -211,18 +261,45 @@ class WebSocketManager extends EventEmitter {
* @returns {boolean}
*/
eventMessage(event) {
- let packet;
- try {
- if (event.binary) event.data = zlib.inflateSync(event.data).toString();
- packet = JSON.parse(event.data);
- } catch (e) {
- return this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE));
+ const data = this.tryParseEventData(event.data);
+ if (data === null) {
+ this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE));
+ return false;
}
- this.client.emit('raw', packet);
+ this.client.emit('raw', data);
- if (packet.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
- return this.packetManager.handle(packet);
+ if (data.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(data.d.heartbeat_interval);
+ return this.packetManager.handle(data);
+ }
+
+ /**
+ * Parses the raw data from a websocket event, inflating it if necessary
+ * @param {*} data Event data
+ * @returns {Object}
+ */
+ parseEventData(data) {
+ if (erlpack) {
+ if (data instanceof ArrayBuffer) data = convertArrayBuffer(data);
+ return erlpack.unpack(data);
+ } else {
+ if (data instanceof ArrayBuffer) data = pako.inflate(data, { to: 'string' });
+ else if (data instanceof Buffer) data = zlib.inflateSync(data).toString();
+ return JSON.parse(data);
+ }
+ }
+
+ /**
+ * Tries to call `parseEventData()` and return its result, or returns `null` upon thrown errors.
+ * @param {*} data Event data
+ * @returns {?Object}
+ */
+ tryParseEventData(data) {
+ try {
+ return this.parseEventData(data);
+ } catch (err) {
+ return null;
+ }
}
/**
@@ -264,7 +341,7 @@ class WebSocketManager extends EventEmitter {
this.status = Constants.Status.NEARLY;
if (this.client.options.fetchAllMembers) {
const promises = this.client.guilds.map(g => g.fetchMembers());
- Promise.all(promises).then(() => this._emitReady()).catch(e => {
+ Promise.all(promises).then(() => this._emitReady(), e => {
this.client.emit(Constants.Events.WARN, 'Error in pre-ready guild member fetching');
this.client.emit(Constants.Events.ERROR, e);
this._emitReady();
@@ -280,6 +357,7 @@ class WebSocketManager extends EventEmitter {
* Tries to reconnect the client, changing the status to Constants.Status.RECONNECTING.
*/
tryReconnect() {
+ if (this.status === Constants.Status.RECONNECTING || this.status === Constants.Status.CONNECTING) return;
this.status = Constants.Status.RECONNECTING;
this.ws.close();
this.packetManager.handleQueue();
diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js
index 6d49ee257..78f57777e 100644
--- a/src/client/websocket/packets/WebSocketPacketManager.js
+++ b/src/client/websocket/packets/WebSocketPacketManager.js
@@ -15,43 +15,47 @@ class WebSocketPacketManager {
this.handlers = {};
this.queue = [];
- this.register(Constants.WSEvents.READY, 'Ready');
- this.register(Constants.WSEvents.GUILD_CREATE, 'GuildCreate');
- this.register(Constants.WSEvents.GUILD_DELETE, 'GuildDelete');
- this.register(Constants.WSEvents.GUILD_UPDATE, 'GuildUpdate');
- this.register(Constants.WSEvents.GUILD_BAN_ADD, 'GuildBanAdd');
- this.register(Constants.WSEvents.GUILD_BAN_REMOVE, 'GuildBanRemove');
- this.register(Constants.WSEvents.GUILD_MEMBER_ADD, 'GuildMemberAdd');
- this.register(Constants.WSEvents.GUILD_MEMBER_REMOVE, 'GuildMemberRemove');
- this.register(Constants.WSEvents.GUILD_MEMBER_UPDATE, 'GuildMemberUpdate');
- this.register(Constants.WSEvents.GUILD_ROLE_CREATE, 'GuildRoleCreate');
- this.register(Constants.WSEvents.GUILD_ROLE_DELETE, 'GuildRoleDelete');
- this.register(Constants.WSEvents.GUILD_ROLE_UPDATE, 'GuildRoleUpdate');
- this.register(Constants.WSEvents.GUILD_MEMBERS_CHUNK, 'GuildMembersChunk');
- this.register(Constants.WSEvents.CHANNEL_CREATE, 'ChannelCreate');
- this.register(Constants.WSEvents.CHANNEL_DELETE, 'ChannelDelete');
- this.register(Constants.WSEvents.CHANNEL_UPDATE, 'ChannelUpdate');
- this.register(Constants.WSEvents.PRESENCE_UPDATE, 'PresenceUpdate');
- this.register(Constants.WSEvents.USER_UPDATE, 'UserUpdate');
- this.register(Constants.WSEvents.VOICE_STATE_UPDATE, 'VoiceStateUpdate');
- this.register(Constants.WSEvents.TYPING_START, 'TypingStart');
- this.register(Constants.WSEvents.MESSAGE_CREATE, 'MessageCreate');
- this.register(Constants.WSEvents.MESSAGE_DELETE, 'MessageDelete');
- this.register(Constants.WSEvents.MESSAGE_UPDATE, 'MessageUpdate');
- this.register(Constants.WSEvents.VOICE_SERVER_UPDATE, 'VoiceServerUpdate');
- this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk');
- this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate');
- this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync');
- this.register(Constants.WSEvents.RELATIONSHIP_ADD, 'RelationshipAdd');
- this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, 'RelationshipRemove');
+ this.register(Constants.WSEvents.READY, require('./handlers/Ready'));
+ this.register(Constants.WSEvents.GUILD_CREATE, require('./handlers/GuildCreate'));
+ this.register(Constants.WSEvents.GUILD_DELETE, require('./handlers/GuildDelete'));
+ this.register(Constants.WSEvents.GUILD_UPDATE, require('./handlers/GuildUpdate'));
+ this.register(Constants.WSEvents.GUILD_BAN_ADD, require('./handlers/GuildBanAdd'));
+ this.register(Constants.WSEvents.GUILD_BAN_REMOVE, require('./handlers/GuildBanRemove'));
+ this.register(Constants.WSEvents.GUILD_MEMBER_ADD, require('./handlers/GuildMemberAdd'));
+ this.register(Constants.WSEvents.GUILD_MEMBER_REMOVE, require('./handlers/GuildMemberRemove'));
+ this.register(Constants.WSEvents.GUILD_MEMBER_UPDATE, require('./handlers/GuildMemberUpdate'));
+ this.register(Constants.WSEvents.GUILD_ROLE_CREATE, require('./handlers/GuildRoleCreate'));
+ this.register(Constants.WSEvents.GUILD_ROLE_DELETE, require('./handlers/GuildRoleDelete'));
+ 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.CHANNEL_CREATE, require('./handlers/ChannelCreate'));
+ this.register(Constants.WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete'));
+ this.register(Constants.WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate'));
+ this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate'));
+ this.register(Constants.WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate'));
+ this.register(Constants.WSEvents.USER_UPDATE, require('./handlers/UserUpdate'));
+ this.register(Constants.WSEvents.USER_NOTE_UPDATE, require('./handlers/UserNoteUpdate'));
+ this.register(Constants.WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate'));
+ this.register(Constants.WSEvents.TYPING_START, require('./handlers/TypingStart'));
+ this.register(Constants.WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate'));
+ this.register(Constants.WSEvents.MESSAGE_DELETE, require('./handlers/MessageDelete'));
+ this.register(Constants.WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate'));
+ this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk'));
+ this.register(Constants.WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate'));
+ this.register(Constants.WSEvents.GUILD_SYNC, require('./handlers/GuildSync'));
+ this.register(Constants.WSEvents.RELATIONSHIP_ADD, require('./handlers/RelationshipAdd'));
+ 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_ALL, require('./handlers/MessageReactionRemoveAll'));
}
get client() {
return this.ws.client;
}
- register(event, handle) {
- const Handler = require(`./handlers/${handle}`);
+ register(event, Handler) {
this.handlers[event] = new Handler(this);
}
@@ -74,12 +78,28 @@ class WebSocketPacketManager {
}
if (packet.op === Constants.OPCodes.INVALID_SESSION) {
- this.ws.sessionID = null;
- this.ws._sendNewIdentify();
+ if (packet.d) {
+ setTimeout(() => {
+ this.ws._sendResume();
+ }, 2500);
+ } else {
+ this.ws.sessionID = null;
+ this.ws._sendNewIdentify();
+ }
return false;
}
- if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) this.ws.client.emit('debug', 'Heartbeat acknowledged');
+ if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) {
+ this.ws.client._pong(this.ws.client._pingTimestamp);
+ this.ws.lastHeartbeatAck = true;
+ this.ws.client.emit('debug', 'Heartbeat acknowledged');
+ } else if (packet.op === Constants.OPCodes.HEARTBEAT) {
+ this.client.ws.send({
+ op: Constants.OPCodes.HEARTBEAT,
+ d: this.client.ws.sequence,
+ });
+ this.ws.client.emit('debug', 'Received gateway heartbeat');
+ }
if (this.ws.status === Constants.Status.RECONNECTING) {
this.ws.reconnecting = false;
diff --git a/src/client/websocket/packets/handlers/ChannelCreate.js b/src/client/websocket/packets/handlers/ChannelCreate.js
index d0488d7ec..04cb2985a 100644
--- a/src/client/websocket/packets/handlers/ChannelCreate.js
+++ b/src/client/websocket/packets/handlers/ChannelCreate.js
@@ -9,7 +9,7 @@ class ChannelCreateHandler extends AbstractHandler {
}
/**
- * Emitted whenever a Channel is created.
+ * Emitted whenever a channel is created.
* @event Client#channelCreate
* @param {Channel} channel The channel that was created
*/
diff --git a/src/client/websocket/packets/handlers/ChannelDelete.js b/src/client/websocket/packets/handlers/ChannelDelete.js
index ec49df0d5..b25f585df 100644
--- a/src/client/websocket/packets/handlers/ChannelDelete.js
+++ b/src/client/websocket/packets/handlers/ChannelDelete.js
@@ -12,7 +12,7 @@ class ChannelDeleteHandler extends AbstractHandler {
}
/**
- * Emitted whenever a Channel is deleted.
+ * Emitted whenever a channel is deleted.
* @event Client#channelDelete
* @param {Channel} channel The channel that was deleted
*/
diff --git a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js
index 65d862959..636df81e5 100644
--- a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js
+++ b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js
@@ -21,7 +21,7 @@ class ChannelPinsUpdate extends AbstractHandler {
}
/**
- * Emitted whenever the pins of a Channel are updated. Due to the nature of the WebSocket event, not much information
+ * 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.
* @event Client#channelPinsUpdate
* @param {Channel} channel The channel that the pins update occured in
diff --git a/src/client/websocket/packets/handlers/GuildDelete.js b/src/client/websocket/packets/handlers/GuildDelete.js
index 9b74d56f6..35e3c53d8 100644
--- a/src/client/websocket/packets/handlers/GuildDelete.js
+++ b/src/client/websocket/packets/handlers/GuildDelete.js
@@ -11,7 +11,7 @@ class GuildDeleteHandler extends AbstractHandler {
}
/**
- * Emitted whenever a Guild is deleted/left.
+ * Emitted whenever a guild is deleted/left.
* @event Client#guildDelete
* @param {Guild} guild The guild that was deleted
*/
diff --git a/src/client/websocket/packets/handlers/GuildEmojiUpdate.js b/src/client/websocket/packets/handlers/GuildEmojiUpdate.js
deleted file mode 100644
index 5f983cd5d..000000000
--- a/src/client/websocket/packets/handlers/GuildEmojiUpdate.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const AbstractHandler = require('./AbstractHandler');
-
-class GuildEmojiUpdate extends AbstractHandler {
- handle(packet) {
- const client = this.packetManager.client;
- const data = packet.d;
- const guild = client.guilds.get(data.guild_id);
- if (!guild) return;
- client.actions.EmojiUpdate.handle(data, guild);
- }
-}
-
-module.exports = GuildEmojiUpdate;
diff --git a/src/client/websocket/packets/handlers/GuildEmojisUpdate.js b/src/client/websocket/packets/handlers/GuildEmojisUpdate.js
new file mode 100644
index 000000000..523f2de2c
--- /dev/null
+++ b/src/client/websocket/packets/handlers/GuildEmojisUpdate.js
@@ -0,0 +1,40 @@
+const AbstractHandler = require('./AbstractHandler');
+
+function mappify(iterable) {
+ const map = new Map();
+ for (const x of iterable) map.set(...x);
+ return map;
+}
+
+class GuildEmojisUpdate extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+ const guild = client.guilds.get(data.guild_id);
+ if (!guild || !guild.emojis) return;
+
+ const deletions = mappify(guild.emojis.entries());
+
+ for (const emoji of data.emojis) {
+ // determine type of emoji event
+ const cachedEmoji = guild.emojis.get(emoji.id);
+ if (cachedEmoji) {
+ deletions.delete(emoji.id);
+ if (!cachedEmoji.equals(emoji, true)) {
+ // emoji updated
+ client.actions.GuildEmojiUpdate.handle(cachedEmoji, emoji);
+ }
+ } else {
+ // emoji added
+ client.actions.GuildEmojiCreate.handle(guild, emoji);
+ }
+ }
+
+ for (const emoji of deletions.values()) {
+ // emoji deleted
+ client.actions.GuildEmojiDelete.handle(emoji);
+ }
+ }
+}
+
+module.exports = GuildEmojisUpdate;
diff --git a/src/client/websocket/packets/handlers/GuildMembersChunk.js b/src/client/websocket/packets/handlers/GuildMembersChunk.js
index 1a58e1cea..02a3c3cbd 100644
--- a/src/client/websocket/packets/handlers/GuildMembersChunk.js
+++ b/src/client/websocket/packets/handlers/GuildMembersChunk.js
@@ -8,19 +8,19 @@ class GuildMembersChunkHandler extends AbstractHandler {
const client = this.packetManager.client;
const data = packet.d;
const guild = client.guilds.get(data.guild_id);
- const members = [];
+ if (!guild) return;
- if (guild) {
- for (const member of data.members) members.push(guild._addMember(member, false));
- }
+ const members = data.members.map(member => guild._addMember(member, false));
guild._checkChunks();
client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members);
+
+ client.ws.lastHeartbeatAck = true;
}
}
/**
- * Emitted whenever a chunk of Guild members is received (all members come from the same guild)
+ * Emitted whenever a chunk of guild members is received (all members come from the same guild)
* @event Client#guildMembersChunk
* @param {GuildMember[]} members The members in the chunk
*/
diff --git a/src/client/websocket/packets/handlers/MessageReactionAdd.js b/src/client/websocket/packets/handlers/MessageReactionAdd.js
new file mode 100644
index 000000000..6a5702efc
--- /dev/null
+++ b/src/client/websocket/packets/handlers/MessageReactionAdd.js
@@ -0,0 +1,11 @@
+const AbstractHandler = require('./AbstractHandler');
+
+class MessageReactionAddHandler extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+ client.actions.MessageReactionAdd.handle(data);
+ }
+}
+
+module.exports = MessageReactionAddHandler;
diff --git a/src/client/websocket/packets/handlers/MessageReactionRemove.js b/src/client/websocket/packets/handlers/MessageReactionRemove.js
new file mode 100644
index 000000000..2afaee703
--- /dev/null
+++ b/src/client/websocket/packets/handlers/MessageReactionRemove.js
@@ -0,0 +1,11 @@
+const AbstractHandler = require('./AbstractHandler');
+
+class MessageReactionRemove extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+ client.actions.MessageReactionRemove.handle(data);
+ }
+}
+
+module.exports = MessageReactionRemove;
diff --git a/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js b/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js
new file mode 100644
index 000000000..303da9ca0
--- /dev/null
+++ b/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js
@@ -0,0 +1,11 @@
+const AbstractHandler = require('./AbstractHandler');
+
+class MessageReactionRemoveAll extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+ client.actions.MessageReactionRemoveAll.handle(data);
+ }
+}
+
+module.exports = MessageReactionRemoveAll;
diff --git a/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js
index 9edacd76a..09d78a01b 100644
--- a/src/client/websocket/packets/handlers/PresenceUpdate.js
+++ b/src/client/websocket/packets/handlers/PresenceUpdate.js
@@ -64,7 +64,7 @@ class PresenceUpdateHandler extends AbstractHandler {
*/
/**
- * Emitted whenever a member becomes available in a large Guild
+ * Emitted whenever a member becomes available in a large guild
* @event Client#guildMemberAvailable
* @param {GuildMember} member The member that became available
*/
diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js
index bb35ea47b..10bc6b257 100644
--- a/src/client/websocket/packets/handlers/Ready.js
+++ b/src/client/websocket/packets/handlers/Ready.js
@@ -1,13 +1,14 @@
const AbstractHandler = require('./AbstractHandler');
-const getStructure = name => require(`../../../../structures/${name}`);
-const ClientUser = getStructure('ClientUser');
+const ClientUser = require('../../../../structures/ClientUser');
class ReadyHandler extends AbstractHandler {
handle(packet) {
const client = this.packetManager.client;
const data = packet.d;
+ client.ws.heartbeat();
+
const clientUser = new ClientUser(client, data.user);
client.user = clientUser;
client.readyAt = new Date();
@@ -31,6 +32,15 @@ class ReadyHandler extends AbstractHandler {
client._setPresence(presence.user.id, presence);
}
+ if (data.notes) {
+ for (const user in data.notes) {
+ let note = data.notes[user];
+ if (!note.length) note = null;
+
+ client.user.notes.set(user, note);
+ }
+ }
+
if (!client.user.bot && client.options.sync) client.setInterval(client.syncGuilds.bind(client), 30000);
client.once('ready', client.syncGuilds.bind(client));
diff --git a/src/client/websocket/packets/handlers/UserNoteUpdate.js b/src/client/websocket/packets/handlers/UserNoteUpdate.js
new file mode 100644
index 000000000..1e4777a39
--- /dev/null
+++ b/src/client/websocket/packets/handlers/UserNoteUpdate.js
@@ -0,0 +1,12 @@
+const AbstractHandler = require('./AbstractHandler');
+
+class UserNoteUpdateHandler extends AbstractHandler {
+ handle(packet) {
+ const client = this.packetManager.client;
+ const data = packet.d;
+
+ client.actions.UserNoteUpdate.handle(data);
+ }
+}
+
+module.exports = UserNoteUpdateHandler;
diff --git a/src/index.js b/src/index.js
index 2b8a19e6f..85326bfc1 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,6 +11,7 @@ module.exports = {
fetchRecommendedShards: require('./util/FetchRecommendedShards'),
Channel: require('./structures/Channel'),
+ ClientOAuth2Application: require('./structures/ClientOAuth2Application'),
ClientUser: require('./structures/ClientUser'),
DMChannel: require('./structures/DMChannel'),
Emoji: require('./structures/Emoji'),
@@ -25,10 +26,14 @@ module.exports = {
MessageAttachment: require('./structures/MessageAttachment'),
MessageCollector: require('./structures/MessageCollector'),
MessageEmbed: require('./structures/MessageEmbed'),
+ MessageReaction: require('./structures/MessageReaction'),
+ OAuth2Application: require('./structures/OAuth2Application'),
PartialGuild: require('./structures/PartialGuild'),
PartialGuildChannel: require('./structures/PartialGuildChannel'),
PermissionOverwrites: require('./structures/PermissionOverwrites'),
Presence: require('./structures/Presence').Presence,
+ ReactionEmoji: require('./structures/ReactionEmoji'),
+ RichEmbed: require('./structures/RichEmbed'),
Role: require('./structures/Role'),
TextChannel: require('./structures/TextChannel'),
User: require('./structures/User'),
@@ -36,4 +41,7 @@ module.exports = {
Webhook: require('./structures/Webhook'),
version: require('../package').version,
+ Constants: require('./util/Constants'),
};
+
+if (typeof window !== 'undefined') window.Discord = module.exports; // eslint-disable-line no-undef
diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js
index bafa0a383..ab9b923e1 100644
--- a/src/sharding/Shard.js
+++ b/src/sharding/Shard.js
@@ -10,7 +10,7 @@ class Shard {
/**
* @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
+ * @param {Array} [args=[]] Command line arguments to pass to the script
*/
constructor(manager, id, args = []) {
/**
@@ -134,21 +134,29 @@ class Shard {
if (message) {
// Shard is requesting a property fetch
if (message._sFetchProp) {
- this.manager.fetchClientValues(message._sFetchProp)
- .then(results => this.send({ _sFetchProp: message._sFetchProp, _result: results }))
- .catch(err => this.send({ _sFetchProp: message._sFetchProp, _error: makePlainError(err) }));
+ this.manager.fetchClientValues(message._sFetchProp).then(
+ results => this.send({ _sFetchProp: message._sFetchProp, _result: results }),
+ err => this.send({ _sFetchProp: message._sFetchProp, _error: makePlainError(err) })
+ );
return;
}
// Shard is requesting an eval broadcast
if (message._sEval) {
- this.manager.broadcastEval(message._sEval)
- .then(results => this.send({ _sEval: message._sEval, _result: results }))
- .catch(err => this.send({ _sEval: message._sEval, _error: makePlainError(err) }));
+ this.manager.broadcastEval(message._sEval).then(
+ results => this.send({ _sEval: message._sEval, _result: results }),
+ err => this.send({ _sEval: message._sEval, _error: makePlainError(err) })
+ );
return;
}
}
+ /**
+ * Emitted upon recieving a message from a shard
+ * @event ShardingManager#message
+ * @param {Shard} shard Shard that sent the message
+ * @param {*} message Message that was received
+ */
this.manager.emit('message', this, message);
}
}
diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js
index 51c16fa8f..6449941f5 100644
--- a/src/sharding/ShardClientUtil.js
+++ b/src/sharding/ShardClientUtil.js
@@ -119,21 +119,22 @@ class ShardClientUtil {
* @private
*/
_respond(type, message) {
- this.send(message).catch(err =>
- this.client.emit('error', `Error when sending ${type} response to master process: ${err}`)
- );
+ this.send(message).catch(err => {
+ err.message = `Error when sending ${type} response to master process: ${err.message}`;
+ this.client.emit('error', err);
+ });
}
/**
* Creates/gets the singleton of this class
* @param {Client} client Client to use
- * @returns {ShardUtil}
+ * @returns {ShardClientUtil}
*/
static singleton(client) {
if (!this._singleton) {
this._singleton = new this(client);
} else {
- client.emit('error', 'Multiple clients created in child process; only the first will handle sharding helpers.');
+ client.emit('warn', 'Multiple clients created in child process; only the first will handle sharding helpers.');
}
return this._singleton;
}
diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js
index 66814babb..671b5d7b9 100644
--- a/src/sharding/ShardingManager.js
+++ b/src/sharding/ShardingManager.js
@@ -10,7 +10,6 @@ const fetchRecommendedShards = require('../util/FetchRecommendedShards');
* This is a utility class that can be used to help you spawn shards of your Client. Each shard is completely separate
* from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.
* If you do not select an amount of shards, the manager will automatically decide the best amount.
- * The Sharding Manager is still experimental
* @extends {EventEmitter}
*/
class ShardingManager extends EventEmitter {
@@ -105,19 +104,17 @@ class ShardingManager extends EventEmitter {
* @returns {Promise>}
*/
spawn(amount = this.totalShards, delay = 5500) {
- return new Promise((resolve, reject) => {
- if (amount === 'auto') {
- fetchRecommendedShards(this.token).then(count => {
- this.totalShards = count;
- resolve(this._spawn(count, delay));
- }).catch(reject);
- } else {
- if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
- if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
- if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
- resolve(this._spawn(amount, delay));
- }
- });
+ if (amount === 'auto') {
+ return fetchRecommendedShards(this.token).then(count => {
+ this.totalShards = count;
+ return this._spawn(count, delay);
+ });
+ } else {
+ if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
+ if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
+ if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
+ return this._spawn(amount, delay);
+ }
}
/**
diff --git a/src/structures/Channel.js b/src/structures/Channel.js
index 2cdab15de..b37b14b41 100644
--- a/src/structures/Channel.js
+++ b/src/structures/Channel.js
@@ -1,14 +1,15 @@
/**
- * Represents any Channel on Discord
+ * Represents any channel on Discord
*/
class Channel {
constructor(client, data) {
/**
* The client that instantiated the Channel
+ * @name Channel#client
* @type {Client}
+ * @readonly
*/
- this.client = client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: client });
/**
* The type of the channel, either:
diff --git a/src/structures/ClientOAuth2Application.js b/src/structures/ClientOAuth2Application.js
new file mode 100644
index 000000000..46e125040
--- /dev/null
+++ b/src/structures/ClientOAuth2Application.js
@@ -0,0 +1,26 @@
+const User = require('./User');
+const OAuth2Application = require('./OAuth2Application');
+
+/**
+ * Represents the client's OAuth2 Application
+ * @extends {OAuth2Application}
+ */
+class ClientOAuth2Application extends OAuth2Application {
+ setup(data) {
+ super.setup(data);
+
+ /**
+ * The app's flags
+ * @type {number}
+ */
+ this.flags = data.flags;
+
+ /**
+ * The app's owner
+ * @type {User}
+ */
+ this.owner = new User(this.client, data.owner);
+ }
+}
+
+module.exports = ClientOAuth2Application;
diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js
index cd7c3891a..d526af6a7 100644
--- a/src/structures/ClientUser.js
+++ b/src/structures/ClientUser.js
@@ -2,7 +2,7 @@ const User = require('./User');
const Collection = require('../util/Collection');
/**
- * Represents the logged in client's Discord User
+ * Represents the logged in client's Discord user
* @extends {User}
*/
class ClientUser extends User {
@@ -25,17 +25,24 @@ class ClientUser extends User {
/**
* A Collection of friends for the logged in user.
- * This is only filled for user accounts, not bot accounts!
+ * This is only filled when using a user account.
* @type {Collection}
*/
this.friends = new Collection();
/**
* A Collection of blocked users for the logged in user.
- * This is only filled for user accounts, not bot accounts!
+ * This is only filled when using a user account.
* @type {Collection}
*/
this.blocked = new Collection();
+
+ /**
+ * A Collection of notes for the logged in user.
+ * This is only filled when using a user account.
+ * @type {Collection}
+ */
+ this.notes = new Collection();
}
edit(data) {
@@ -47,6 +54,7 @@ class ClientUser extends User {
* Changing usernames in Discord is heavily rate limited, with only 2 requests
* every hour. Use this sparingly!
* @param {string} username The new username
+ * @param {string} [password] Current password (only for user accounts)
* @returns {Promise}
* @example
* // set username
@@ -54,43 +62,45 @@ class ClientUser extends User {
* .then(user => console.log(`My new username is ${user.username}`))
* .catch(console.error);
*/
- setUsername(username) {
- return this.client.rest.methods.updateCurrentUser({ username });
+ setUsername(username, password) {
+ return this.client.rest.methods.updateCurrentUser({ username }, password);
}
/**
- * If this user is a "self bot" or logged in using a normal user's details (which should be avoided), you can set the
- * email here.
- * @param {string} email The new email
+ * Changes the email for the client user's account.
+ * This is only available when using a user account.
+ * @param {string} email New email to change to
+ * @param {string} password Current password
* @returns {Promise}
* @example
* // set email
- * client.user.setEmail('bob@gmail.com')
+ * client.user.setEmail('bob@gmail.com', 'some amazing password 123')
* .then(user => console.log(`My new email is ${user.email}`))
* .catch(console.error);
*/
- setEmail(email) {
- return this.client.rest.methods.updateCurrentUser({ email });
+ setEmail(email, password) {
+ return this.client.rest.methods.updateCurrentUser({ email }, password);
}
/**
- * If this user is a "self bot" or logged in using a normal user's details (which should be avoided), you can set the
- * password here.
- * @param {string} password The new password
+ * Changes the password for the client user's account.
+ * This is only available when using a user account.
+ * @param {string} newPassword New password to change to
+ * @param {string} oldPassword Current password
* @returns {Promise}
* @example
* // set password
- * client.user.setPassword('password123')
+ * client.user.setPassword('some new amazing password 456', 'some amazing password 123')
* .then(user => console.log('New password set!'))
* .catch(console.error);
*/
- setPassword(password) {
- return this.client.rest.methods.updateCurrentUser({ password });
+ setPassword(newPassword, oldPassword) {
+ return this.client.rest.methods.updateCurrentUser({ password: newPassword }, oldPassword);
}
/**
* Set the avatar of the logged in Client.
- * @param {FileResolvable|Base64Resolveable} avatar The new avatar
+ * @param {BufferResolvable|Base64Resolvable} avatar The new avatar
* @returns {Promise}
* @example
* // set avatar
@@ -99,94 +109,28 @@ class ClientUser extends User {
* .catch(console.error);
*/
setAvatar(avatar) {
- return new Promise(resolve => {
- if (avatar.startsWith('data:')) {
- resolve(this.client.rest.methods.updateCurrentUser({ avatar }));
- } else {
- this.client.resolver.resolveFile(avatar).then(data => {
- resolve(this.client.rest.methods.updateCurrentUser({ avatar: data }));
- });
- }
- });
+ if (avatar.startsWith('data:')) {
+ return this.client.rest.methods.updateCurrentUser({ avatar });
+ } else {
+ return this.client.resolver.resolveBuffer(avatar).then(data =>
+ this.client.rest.methods.updateCurrentUser({ avatar: data })
+ );
+ }
}
/**
- * Set the status of the logged in user.
- * @param {string} status can be `online`, `idle`, `invisible` or `dnd` (do not disturb)
- * @returns {Promise}
+ * Data resembling a raw Discord presence
+ * @typedef {Object} PresenceData
+ * @property {PresenceStatus} [status] Status of the user
+ * @property {boolean} [afk] Whether the user is AFK
+ * @property {Object} [game] Game the user is playing
+ * @property {string} [game.name] Name of the game
+ * @property {string} [game.url] Twitch stream URL
*/
- setStatus(status) {
- return this.setPresence({ status });
- }
/**
- * Set the current game of the logged in user.
- * @param {string} game the game being played
- * @param {string} [streamingURL] an optional URL to a twitch stream, if one is available.
- * @returns {Promise}
- */
- setGame(game, streamingURL) {
- return this.setPresence({ game: {
- name: game,
- url: streamingURL,
- } });
- }
-
- /**
- * Set/remove the AFK flag for the current user.
- * @param {boolean} afk whether or not the user is AFK.
- * @returns {Promise}
- */
- setAFK(afk) {
- return this.setPresence({ afk });
- }
-
- /**
- * Send a friend request
- * This is only available for user accounts, not bot accounts!
- * @param {UserResolvable} user The user to send the friend request to.
- * @returns {Promise} The user the friend request was sent to.
- */
- addFriend(user) {
- user = this.client.resolver.resolveUser(user);
- return this.client.rest.methods.addFriend(user);
- }
-
- /**
- * Remove a friend
- * This is only available for user accounts, not bot accounts!
- * @param {UserResolvable} user The user to remove from your friends
- * @returns {Promise} The user that was removed
- */
- removeFriend(user) {
- user = this.client.resolver.resolveUser(user);
- return this.client.rest.methods.removeFriend(user);
- }
-
- /**
- * Creates a guild
- * This is only available for user accounts, not bot accounts!
- * @param {string} name The name of the guild
- * @param {string} region The region for the server
- * @param {FileResolvable|Base64Resolvable} [icon=null] The icon for the guild
- * @returns {Promise} The guild that was created
- */
- createGuild(name, region, icon = null) {
- return new Promise(resolve => {
- if (!icon) resolve(this.client.rest.methods.createGuild({ name, icon, region }));
- if (icon.startsWith('data:')) {
- resolve(this.client.rest.methods.createGuild({ name, icon, region }));
- } else {
- this.client.resolver.resolveFile(icon).then(data => {
- resolve(this.client.rest.methods.createGuild({ name, icon: data, region }));
- });
- }
- });
- }
-
- /**
- * Set the full presence of the current user.
- * @param {Object} data the data to provide
+ * Sets the full presence of the client user.
+ * @param {PresenceData} data Data for the presence
* @returns {Promise}
*/
setPresence(data) {
@@ -231,6 +175,100 @@ class ClientUser extends User {
resolve(this);
});
}
+
+ /**
+ * A user's status. Must be one of:
+ * - `online`
+ * - `idle`
+ * - `invisible`
+ * - `dnd` (do not disturb)
+ * @typedef {string} PresenceStatus
+ */
+
+ /**
+ * Sets the status of the client user.
+ * @param {PresenceStatus} status Status to change to
+ * @returns {Promise}
+ */
+ setStatus(status) {
+ return this.setPresence({ status });
+ }
+
+ /**
+ * Sets the game the client user is playing.
+ * @param {string} game Game being played
+ * @param {string} [streamingURL] Twitch stream URL
+ * @returns {Promise}
+ */
+ setGame(game, streamingURL) {
+ return this.setPresence({ game: {
+ name: game,
+ url: streamingURL,
+ } });
+ }
+
+ /**
+ * Sets/removes the AFK flag for the client user.
+ * @param {boolean} afk Whether or not the user is AFK
+ * @returns {Promise}
+ */
+ setAFK(afk) {
+ return this.setPresence({ afk });
+ }
+
+ /**
+ * Fetches messages that mentioned the client's user
+ * @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|string} [options.guild] Limit the search to a specific guild
+ * @returns {Promise}
+ */
+ fetchMentions(options = { limit: 25, roles: true, everyone: true, guild: null }) {
+ return this.client.rest.methods.fetchMentions(options);
+ }
+
+ /**
+ * Send a friend request
+ * This is only available when using a user account.
+ * @param {UserResolvable} user The user to send the friend request to.
+ * @returns {Promise} The user the friend request was sent to.
+ */
+ addFriend(user) {
+ user = this.client.resolver.resolveUser(user);
+ return this.client.rest.methods.addFriend(user);
+ }
+
+ /**
+ * Remove a friend
+ * This is only available when using a user account.
+ * @param {UserResolvable} user The user to remove from your friends
+ * @returns {Promise} The user that was removed
+ */
+ removeFriend(user) {
+ user = this.client.resolver.resolveUser(user);
+ return this.client.rest.methods.removeFriend(user);
+ }
+
+ /**
+ * Creates a guild
+ * This is only available when using a user account.
+ * @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
+ * @returns {Promise} The guild that was created
+ */
+ createGuild(name, region, icon = null) {
+ if (!icon) return this.client.rest.methods.createGuild({ name, icon, region });
+ if (icon.startsWith('data:')) {
+ return this.client.rest.methods.createGuild({ name, icon, region });
+ } else {
+ return this.client.resolver.resolveBuffer(icon).then(data =>
+ this.client.rest.methods.createGuild({ name, icon: data, region })
+ );
+ }
+ }
}
module.exports = ClientUser;
diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js
index f7f53c322..de49c8b3f 100644
--- a/src/structures/DMChannel.js
+++ b/src/structures/DMChannel.js
@@ -3,7 +3,7 @@ const TextBasedChannel = require('./interface/TextBasedChannel');
const Collection = require('../util/Collection');
/**
- * Represents a Direct Message Channel between two users.
+ * Represents a direct message channel between two users.
* @extends {Channel}
* @implements {TextBasedChannel}
*/
@@ -37,8 +37,9 @@ class DMChannel extends Channel {
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
+ send() { return; }
sendMessage() { return; }
- sendTTSMessage() { return; }
+ sendEmbed() { return; }
sendFile() { return; }
sendCode() { return; }
fetchMessage() { return; }
diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js
index 0349b6f70..d8a62e17e 100644
--- a/src/structures/Emoji.js
+++ b/src/structures/Emoji.js
@@ -2,19 +2,20 @@ const Constants = require('../util/Constants');
const Collection = require('../util/Collection');
/**
- * Represents a Custom Emoji
+ * Represents a custom emoji
*/
class Emoji {
constructor(guild, data) {
/**
* The Client that instantiated this object
+ * @name Emoji#client
* @type {Client}
+ * @readonly
*/
- this.client = guild.client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: guild.client });
/**
- * The Guild this emoji is part of
+ * The guild this emoji is part of
* @type {Guild}
*/
this.guild = guild;
@@ -24,13 +25,13 @@ class Emoji {
setup(data) {
/**
- * The ID of the Emoji
+ * The ID of the emoji
* @type {string}
*/
this.id = data.id;
/**
- * The name of the Emoji
+ * The name of the emoji
* @type {string}
*/
this.name = data.name;
@@ -87,7 +88,7 @@ class Emoji {
* @readonly
*/
get url() {
- return `${Constants.Endpoints.CDN}/emojis/${this.id}.png`;
+ return Constants.Endpoints.emoji(this.id);
}
/**
@@ -101,6 +102,39 @@ class Emoji {
toString() {
return this.requiresColons ? `<:${this.name}:${this.id}>` : this.name;
}
+
+ /**
+ * Whether this emoji is the same as another one
+ * @param {Emoji|Object} other the emoji to compare it to
+ * @returns {boolean} whether the emoji is equal to the given emoji or not
+ */
+ equals(other) {
+ if (other instanceof Emoji) {
+ return (
+ other.id === this.id &&
+ other.name === this.name &&
+ other.managed === this.managed &&
+ other.requiresColons === this.requiresColons
+ );
+ } else {
+ return (
+ other.id === this.id &&
+ other.name === this.name
+ );
+ }
+ }
+
+ /**
+ * The identifier of this emoji, used for message reactions
+ * @readonly
+ * @type {string}
+ */
+ get identifier() {
+ if (this.id) {
+ return `${this.name}:${this.id}`;
+ }
+ return encodeURIComponent(this.name);
+ }
}
module.exports = Emoji;
diff --git a/src/structures/EvaluatedPermissions.js b/src/structures/EvaluatedPermissions.js
index de92c10d7..ae8a643ce 100644
--- a/src/structures/EvaluatedPermissions.js
+++ b/src/structures/EvaluatedPermissions.js
@@ -57,7 +57,7 @@ class EvaluatedPermissions {
* Checks whether the user has all specified permissions, and lists any missing permissions.
* @param {PermissionResolvable[]} permissions The permissions to check for
* @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions
- * @returns {array}
+ * @returns {PermissionResolvable[]}
*/
missingPermissions(permissions, explicit = false) {
return permissions.filter(p => !this.hasPermission(p, explicit));
diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js
index cfeea2555..84fe4f994 100644
--- a/src/structures/GroupDMChannel.js
+++ b/src/structures/GroupDMChannel.js
@@ -1,7 +1,6 @@
const Channel = require('./Channel');
const TextBasedChannel = require('./interface/TextBasedChannel');
const Collection = require('../util/Collection');
-const arraysEqual = require('../util/ArraysEqual');
/*
{ type: 3,
@@ -90,7 +89,7 @@ class GroupDMChannel extends Channel {
* Whether this channel equals another channel. It compares all properties, so for most operations
* it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often
* what most users need.
- * @param {GroupDMChannel} channel The channel to compare to
+ * @param {GroupDMChannel} channel Channel to compare with
* @returns {boolean}
*/
equals(channel) {
@@ -101,16 +100,14 @@ class GroupDMChannel extends Channel {
this.ownerID === channel.ownerID;
if (equal) {
- const thisIDs = this.recipients.keyArray();
- const otherIDs = channel.recipients.keyArray();
- return arraysEqual(thisIDs, otherIDs);
+ return this.recipients.equals(channel.recipients);
}
return equal;
}
/**
- * When concatenated with a string, this automatically concatenates the Channel's name instead of the Channel object.
+ * When concatenated with a string, this automatically concatenates the channel's name instead of the Channel object.
* @returns {string}
* @example
* // logs: Hello from My Group DM!
@@ -124,8 +121,9 @@ class GroupDMChannel extends Channel {
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
+ send() { return; }
sendMessage() { return; }
- sendTTSMessage() { return; }
+ sendEmbed() { return; }
sendFile() { return; }
sendCode() { return; }
fetchMessage() { return; }
diff --git a/src/structures/Guild.js b/src/structures/Guild.js
index e78c204e0..acd5b6fde 100644
--- a/src/structures/Guild.js
+++ b/src/structures/Guild.js
@@ -9,7 +9,7 @@ const cloneObject = require('../util/CloneObject');
const arraysEqual = require('../util/ArraysEqual');
/**
- * Represents a Guild (or a Server) on Discord.
+ * Represents a guild (or a server) on Discord.
* It's recommended to see if a guild is available before performing operations or reading data from it. You can
* check this with `guild.available`.
*/
@@ -17,33 +17,40 @@ class Guild {
constructor(client, data) {
/**
* The Client that created the instance of the the Guild.
+ * @name Guild#client
* @type {Client}
+ * @readonly
*/
- this.client = client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: client });
/**
- * A Collection of members that are in this Guild. The key is the member's ID, the value is the member.
+ * A collection of members that are in this guild. The key is the member's ID, the value is the member.
* @type {Collection}
*/
this.members = new Collection();
/**
- * A Collection of channels that are in this Guild. The key is the channel's ID, the value is the channel.
+ * A collection of channels that are in this guild. The key is the channel's ID, the value is the channel.
* @type {Collection}
*/
this.channels = new Collection();
/**
- * A Collection of roles that are in this Guild. The key is the role's ID, the value is the role.
+ * A collection of roles that are in this guild. The key is the role's ID, the value is the role.
* @type {Collection}
*/
this.roles = new Collection();
+ /**
+ * A collection of presences in this guild
+ * @type {Collection}
+ */
+ this.presences = new Collection();
+
if (!data) return;
if (data.unavailable) {
/**
- * Whether the Guild is available to access. If it is not available, it indicates a server outage.
+ * Whether the guild is available to access. If it is not available, it indicates a server outage.
* @type {boolean}
*/
this.available = false;
@@ -90,7 +97,7 @@ class Guild {
this.region = data.region;
/**
- * The full amount of members in this Guild as of `READY`
+ * The full amount of members in this guild as of `READY`
* @type {number}
*/
this.memberCount = data.member_count || this.memberCount;
@@ -101,12 +108,6 @@ class Guild {
*/
this.large = data.large || this.large;
- /**
- * A collection of presences in this Guild
- * @type {Collection}
- */
- this.presences = new Collection();
-
/**
* An array of guild features.
* @type {Object[]}
@@ -114,7 +115,13 @@ class Guild {
this.features = data.features;
/**
- * A Collection of emojis that are in this Guild. The key is the emoji's ID, the value is the emoji.
+ * The ID of the application that created this guild (if applicable)
+ * @type {?string}
+ */
+ this.applicationID = data.application_id;
+
+ /**
+ * A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji.
* @type {Collection}
*/
this.emojis = new Collection();
@@ -242,7 +249,17 @@ class Guild {
}
/**
- * The owner of the Guild
+ * Gets the URL to this guild's splash (if it has one, otherwise it returns null)
+ * @type {?string}
+ * @readonly
+ */
+ get splashURL() {
+ if (!this.splash) return null;
+ return Constants.Endpoints.guildSplash(this.id, this.splash);
+ }
+
+ /**
+ * The owner of the guild
* @type {GuildMember}
* @readonly
*/
@@ -256,6 +273,7 @@ class Guild {
* @readonly
*/
get voiceConnection() {
+ if (this.client.browser) return null;
return this.client.voice.connections.get(this.id) || null;
}
@@ -269,7 +287,7 @@ class Guild {
}
/**
- * Returns the GuildMember form of a User object, if the User is present in the guild.
+ * Returns the GuildMember form of a User object, if the user is present in the guild.
* @param {UserResolvable} user The user that you want to obtain the GuildMember of
* @returns {?GuildMember}
* @example
@@ -281,7 +299,7 @@ class Guild {
}
/**
- * Fetch a Collection of banned users in this Guild.
+ * Fetch a collection of banned users in this guild.
* @returns {Promise>}
*/
fetchBans() {
@@ -289,7 +307,7 @@ class Guild {
}
/**
- * Fetch a Collection of invites to this Guild. Resolves with a Collection mapping invites by their codes.
+ * Fetch a collection of invites to this guild. Resolves with a collection mapping invites by their codes.
* @returns {Promise>}
*/
fetchInvites() {
@@ -318,7 +336,7 @@ class Guild {
}
/**
- * Fetches all the members in the Guild, even if they are offline. If the Guild has less than 250 members,
+ * Fetches all the members in the guild, even if they are offline. If the guild has less than 250 members,
* this should not be necessary.
* @param {string} [query=''] An optional query to provide when fetching members
* @returns {Promise}
@@ -344,6 +362,19 @@ class Guild {
});
}
+ /**
+ * The data for editing a guild
+ * @typedef {Object} GuildEditData
+ * @property {string} [name] The name of the guild
+ * @property {string} [region] The region of the guild
+ * @property {number} [verificationLevel] The verification level of the guild
+ * @property {ChannelResolvable} [afkChannel] The AFK channel of the guild
+ * @property {number} [afkTimeout] The AFK timeout of the guild
+ * @property {Base64Resolvable} [icon] The icon of the guild
+ * @property {GuildMemberResolvable} [owner] The owner of the guild
+ * @property {Base64Resolvable} [splash] The splash screen of the guild
+ */
+
/**
* Updates the Guild with new information - e.g. a new name.
* @param {GuildEditData} data The data to update the guild with
@@ -362,8 +393,8 @@ class Guild {
}
/**
- * Edit the name of the Guild.
- * @param {string} name The new name of the Guild
+ * Edit the name of the guild.
+ * @param {string} name The new name of the guild
* @returns {Promise}
* @example
* // edit the guild name
@@ -376,8 +407,8 @@ class Guild {
}
/**
- * Edit the region of the Guild.
- * @param {Region} region The new region of the guild.
+ * Edit the region of the guild.
+ * @param {string} region The new region of the guild.
* @returns {Promise}
* @example
* // edit the guild region
@@ -390,8 +421,8 @@ class Guild {
}
/**
- * Edit the verification level of the Guild.
- * @param {VerificationLevel} verificationLevel The new verification level of the guild
+ * Edit the verification level of the guild.
+ * @param {number} verificationLevel The new verification level of the guild
* @returns {Promise}
* @example
* // edit the guild verification level
@@ -404,8 +435,8 @@ class Guild {
}
/**
- * Edit the AFK channel of the Guild.
- * @param {GuildChannelResolvable} afkChannel The new AFK channel
+ * Edit the AFK channel of the guild.
+ * @param {ChannelResolvable} afkChannel The new AFK channel
* @returns {Promise}
* @example
* // edit the guild AFK channel
@@ -418,7 +449,7 @@ class Guild {
}
/**
- * Edit the AFK timeout of the Guild.
+ * Edit the AFK timeout of the guild.
* @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK
* @returns {Promise}
* @example
@@ -432,7 +463,7 @@ class Guild {
}
/**
- * Set a new Guild Icon.
+ * Set a new guild icon.
* @param {Base64Resolvable} icon The new icon of the guild
* @returns {Promise}
* @example
@@ -446,8 +477,8 @@ class Guild {
}
/**
- * Sets a new owner of the Guild.
- * @param {GuildMemberResolvable} owner The new owner of the Guild
+ * Sets a new owner of the guild.
+ * @param {GuildMemberResolvable} owner The new owner of the guild
* @returns {Promise}
* @example
* // edit the guild owner
@@ -460,7 +491,7 @@ class Guild {
}
/**
- * Set a new Guild Splash Logo.
+ * Set a new guild splash screen.
* @param {Base64Resolvable} splash The new splash screen of the guild
* @returns {Promise}
* @example
@@ -490,7 +521,7 @@ class Guild {
}
/**
- * Unbans a user from the Guild.
+ * Unbans a user from the guild.
* @param {UserResolvable} user The user to unban
* @returns {Promise}
* @example
@@ -525,16 +556,18 @@ class Guild {
}
/**
- * Syncs this guild (already done automatically every 30 seconds). Only applicable to user accounts.
+ * Syncs this guild (already done automatically every 30 seconds).
+ * This is only available when using a user account.
*/
sync() {
if (!this.client.user.bot) this.client.syncGuilds([this]);
}
/**
- * Creates a new Channel in the Guild.
+ * Creates a new channel in the guild.
* @param {string} name The name of the new channel
* @param {string} type The type of the new channel, either `text` or `voice`
+ * @param {Array} overwrites Permission overwrites to apply to the new channel
* @returns {Promise}
* @example
* // create a new text channel
@@ -542,8 +575,8 @@ class Guild {
* .then(channel => console.log(`Created new channel ${channel}`))
* .catch(console.error);
*/
- createChannel(name, type) {
- return this.client.rest.methods.createChannel(this, name, type);
+ createChannel(name, type, overwrites) {
+ return this.client.rest.methods.createChannel(this, name, type, overwrites);
}
/**
@@ -569,7 +602,7 @@ class Guild {
/**
* Creates a new custom emoji in the guild.
- * @param {FileResolveable} attachment The image for the emoji.
+ * @param {BufferResolvable} attachment The image for the emoji.
* @param {string} name The name for the emoji.
* @returns {Promise} The created emoji.
* @example
@@ -584,13 +617,14 @@ class Guild {
* .catch(console.error);
*/
createEmoji(attachment, name) {
- return new Promise((resolve, reject) => {
- this.client.resolver.resolveFile(attachment).then(file => {
- let base64 = new Buffer(file, 'binary').toString('base64');
- let dataURI = `data:;base64,${base64}`;
- this.client.rest.methods.createEmoji(this, dataURI, name)
- .then(resolve).catch(reject);
- }).catch(reject);
+ return new Promise(resolve => {
+ if (attachment.startsWith('data:')) {
+ resolve(this.client.rest.methods.createEmoji(this, attachment, name));
+ } else {
+ this.client.resolver.resolveBuffer(attachment).then(data =>
+ resolve(this.client.rest.methods.createEmoji(this, data, name))
+ );
+ }
});
}
@@ -637,21 +671,34 @@ class Guild {
* @returns {Promise}
*/
setRolePosition(role, position) {
- if (role instanceof Role) {
- role = role.id;
- } else if (typeof role !== 'string') {
- return Promise.reject(new Error('Supplied role is not a role or string'));
+ if (typeof role === 'string') {
+ role = this.roles.get(role);
+ if (!role) return Promise.reject(new Error('Supplied role is not a role or string.'));
}
position = Number(position);
- if (isNaN(position)) {
- return Promise.reject(new Error('Supplied position is not a number'));
+ if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.'));
+
+ const lowestAffected = Math.min(role.position, position);
+ const highestAffected = Math.max(role.position, position);
+
+ const rolesToUpdate = this.roles.filter(r => r.position >= lowestAffected && r.position <= highestAffected);
+
+ // stop role positions getting stupidly inflated
+ if (position > role.position) {
+ position = rolesToUpdate.first().position;
+ } else {
+ position = rolesToUpdate.last().position;
}
- const updatedRoles = this.roles.array().map(r => ({
- id: r.id,
- position: r.id === role ? position : r.position < position ? r.position : r.position + 1,
- }));
+ const updatedRoles = [];
+
+ for (const uRole of rolesToUpdate.values()) {
+ updatedRoles.push({
+ id: uRole.id,
+ position: uRole.id === role.id ? position : uRole.position + (position < role.position ? 1 : -1),
+ });
+ }
return this.client.rest.methods.setRolePositions(this.id, updatedRoles);
}
@@ -660,7 +707,7 @@ class Guild {
* Whether this Guild equals another Guild. It compares all properties, so for most operations
* it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often
* what most users need.
- * @param {Guild} guild The guild to compare
+ * @param {Guild} guild Guild to compare with
* @returns {boolean}
*/
equals(guild) {
@@ -691,7 +738,7 @@ class Guild {
}
/**
- * When concatenated with a string, this automatically concatenates the Guild's name instead of the Guild object.
+ * When concatenated with a string, this automatically concatenates the guild's name instead of the Guild object.
* @returns {string}
* @example
* // logs: Hello from My Guild!
@@ -712,7 +759,7 @@ class Guild {
const member = new GuildMember(this, guildUser);
this.members.set(member.id, member);
- if (this._rawVoiceStates && this._rawVoiceStates.get(member.user.id)) {
+ if (this._rawVoiceStates && this._rawVoiceStates.has(member.user.id)) {
const voiceState = this._rawVoiceStates.get(member.user.id);
member.serverMute = voiceState.mute;
member.serverDeaf = voiceState.deaf;
@@ -720,7 +767,11 @@ class Guild {
member.selfDeaf = voiceState.self_deaf;
member.voiceSessionID = voiceState.session_id;
member.voiceChannelID = voiceState.channel_id;
- this.channels.get(voiceState.channel_id).members.set(member.user.id, member);
+ if (this.client.channels.has(voiceState.channel_id)) {
+ this.client.channels.get(voiceState.channel_id).members.set(member.user.id, member);
+ } else {
+ this.client.emit('warn', `Member ${member.id} added in guild ${this.id} with an uncached voice channel`);
+ }
}
/**
@@ -746,7 +797,7 @@ class Guild {
if (this.client.ws.status === Constants.Status.READY && notSame) {
/**
- * Emitted whenever a Guild Member changes - i.e. new role, removed role, nickname
+ * Emitted whenever a guild member changes - i.e. new role, removed role, nickname
* @event Client#guildMemberUpdate
* @param {GuildMember} oldMember The member before the update
* @param {GuildMember} newMember The member after the update
@@ -770,7 +821,7 @@ class Guild {
if (member && member.speaking !== speaking) {
member.speaking = speaking;
/**
- * Emitted once a Guild Member starts/stops speaking
+ * Emitted once a guild member starts/stops speaking
* @event Client#guildMemberSpeaking
* @param {GuildMember} member The member that started/stopped speaking
* @param {boolean} speaking Whether or not the member is speaking
diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js
index 7d220cf6c..a93078972 100644
--- a/src/structures/GuildChannel.js
+++ b/src/structures/GuildChannel.js
@@ -4,10 +4,9 @@ const PermissionOverwrites = require('./PermissionOverwrites');
const EvaluatedPermissions = require('./EvaluatedPermissions');
const Constants = require('../util/Constants');
const Collection = require('../util/Collection');
-const arraysEqual = require('../util/ArraysEqual');
/**
- * Represents a Guild Channel (i.e. Text Channels and Voice Channels)
+ * Represents a guild channel (i.e. text channels and voice channels)
* @extends {Channel}
*/
class GuildChannel extends Channel {
@@ -25,7 +24,7 @@ class GuildChannel extends Channel {
super.setup(data);
/**
- * The name of the Guild Channel
+ * The name of the guild channel
* @type {string}
*/
this.name = data.name;
@@ -66,11 +65,11 @@ class GuildChannel extends Channel {
const overwrites = this.overwritesFor(member, true, roles);
for (const overwrite of overwrites.role.concat(overwrites.member)) {
- permissions &= ~overwrite.denyData;
- permissions |= overwrite.allowData;
+ permissions &= ~overwrite.deny;
+ permissions |= overwrite.allow;
}
- const admin = Boolean(permissions & (Constants.PermissionFlags.ADMINISTRATOR));
+ const admin = Boolean(permissions & Constants.PermissionFlags.ADMINISTRATOR);
if (admin) permissions = Constants.ALL_PERMISSIONS;
return new EvaluatedPermissions(member, permissions);
@@ -144,8 +143,8 @@ class GuildChannel extends Channel {
const prevOverwrite = this.permissionOverwrites.get(userOrRole.id);
if (prevOverwrite) {
- payload.allow = prevOverwrite.allowData;
- payload.deny = prevOverwrite.denyData;
+ payload.allow = prevOverwrite.allow;
+ payload.deny = prevOverwrite.deny;
}
for (const perm in options) {
@@ -155,18 +154,41 @@ class GuildChannel extends Channel {
} else if (options[perm] === false) {
payload.allow &= ~(Constants.PermissionFlags[perm] || 0);
payload.deny |= Constants.PermissionFlags[perm] || 0;
+ } else if (options[perm] === null) {
+ payload.allow &= ~(Constants.PermissionFlags[perm] || 0);
+ payload.deny &= ~(Constants.PermissionFlags[perm] || 0);
}
}
return this.client.rest.methods.setChannelOverwrite(this, payload);
}
+ /**
+ * The data for a guild channel
+ * @typedef {Object} ChannelData
+ * @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 {number} [bitrate] The bitrate of the voice channel
+ * @property {number} [userLimit] The user limit of the channel
+ */
+
+ /**
+ * Edits the channel
+ * @param {ChannelData} data The new data for the channel
+ * @returns {Promise}
+ * @example
+ * // edit a channel
+ * channel.edit({name: 'new-channel'})
+ * .then(c => console.log(`Edited channel ${c}`))
+ * .catch(console.error);
+ */
edit(data) {
return this.client.rest.methods.updateChannel(this, data);
}
/**
- * Set a new name for the Guild Channel
+ * Set a new name for the guild channel
* @param {string} name The new name for the guild channel
* @returns {Promise}
* @example
@@ -176,11 +198,11 @@ class GuildChannel extends Channel {
* .catch(console.error);
*/
setName(name) {
- return this.client.rest.methods.updateChannel(this, { name });
+ return this.edit({ name });
}
/**
- * Set a new position for the Guild Channel
+ * Set a new position for the guild channel
* @param {number} position The new position for the guild channel
* @returns {Promise}
* @example
@@ -194,7 +216,7 @@ class GuildChannel extends Channel {
}
/**
- * Set a new topic for the Guild Channel
+ * Set a new topic for the guild channel
* @param {string} topic The new topic for the guild channel
* @returns {Promise}
* @example
@@ -208,15 +230,15 @@ class GuildChannel extends Channel {
}
/**
- * Options given when creating a Guild Channel Invite
+ * Options given when creating a guild channel invite
* @typedef {Object} InviteOptions
* @property {boolean} [temporary=false] Whether the invite should kick users after 24hrs if they are not given a role
* @property {number} [maxAge=0] Time in seconds the invite expires in
- * @property {maxUses} [maxUses=0] Maximum amount of uses for this invite
+ * @property {number} [maxUses=0] Maximum amount of uses for this invite
*/
/**
- * Create an invite to this Guild Channel
+ * Create an invite to this guild channel
* @param {InviteOptions} [options={}] The options for the invite
* @returns {Promise}
*/
@@ -224,10 +246,20 @@ class GuildChannel extends Channel {
return this.client.rest.methods.createChannelInvite(this, options);
}
+ /**
+ * Clone this channel
+ * @param {string} [name=this.name] Optional name for the new channel, otherwise it has the name of this channel
+ * @param {boolean} [withPermissions=true] Whether to clone the channel with this channel's permission overwrites
+ * @returns {Promise}
+ */
+ clone(name = this.name, withPermissions = true) {
+ return this.guild.createChannel(name, this.type, withPermissions ? this.permissionOverwrites : []);
+ }
+
/**
* Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel.
* In most cases, a simple `channel.id === channel2.id` will do, and is much faster too.
- * @param {GuildChannel} channel The channel to compare this channel to
+ * @param {GuildChannel} channel Channel to compare with
* @returns {boolean}
*/
equals(channel) {
@@ -240,9 +272,7 @@ class GuildChannel extends Channel {
if (equal) {
if (this.permissionOverwrites && channel.permissionOverwrites) {
- const thisIDSet = this.permissionOverwrites.keyArray();
- const otherIDSet = channel.permissionOverwrites.keyArray();
- equal = arraysEqual(thisIDSet, otherIDSet);
+ equal = this.permissionOverwrites.equals(channel.permissionOverwrites);
} else {
equal = !this.permissionOverwrites && !channel.permissionOverwrites;
}
@@ -252,7 +282,7 @@ class GuildChannel extends Channel {
}
/**
- * When concatenated with a string, this automatically returns the Channel's mention instead of the Channel object.
+ * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object.
* @returns {string}
* @example
* // Outputs: Hello from #general
diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js
index 408efedc8..60a498a3e 100644
--- a/src/structures/GuildMember.js
+++ b/src/structures/GuildMember.js
@@ -6,17 +6,18 @@ const Collection = require('../util/Collection');
const Presence = require('./Presence').Presence;
/**
- * Represents a Member of a Guild on Discord
+ * Represents a member of a guild on Discord
* @implements {TextBasedChannel}
*/
class GuildMember {
constructor(guild, data) {
/**
- * The client that instantiated this GuildMember
+ * The Client that instantiated this GuildMember
+ * @name GuildMember#client
* @type {Client}
+ * @readonly
*/
- this.client = guild.client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: guild.client });
/**
* The guild that this member is part of
@@ -32,6 +33,12 @@ class GuildMember {
this._roles = [];
if (data) this.setup(data);
+
+ /**
+ * The ID of the last message sent by the member in their guild, if one was sent.
+ * @type {?string}
+ */
+ this.lastMessageID = null;
}
setup(data) {
@@ -78,7 +85,7 @@ class GuildMember {
this.speaking = false;
/**
- * The nickname of this Guild Member, if they have one
+ * The nickname of this guild member, if they have one
* @type {?string}
*/
this.nickname = data.nick || null;
@@ -103,7 +110,7 @@ class GuildMember {
}
/**
- * The presence of this Guild Member
+ * The presence of this guild member
* @type {Presence}
* @readonly
*/
@@ -167,7 +174,7 @@ class GuildMember {
}
/**
- * The ID of this User
+ * The ID of this user
* @type {string}
* @readonly
*/
@@ -175,6 +182,15 @@ class GuildMember {
return this.user.id;
}
+ /**
+ * The nickname of the member, or their username if they don't have one
+ * @type {string}
+ * @readonly
+ */
+ get displayName() {
+ return this.nickname || this.user.username;
+ }
+
/**
* The overall set of permissions for the guild member, taking only roles into account
* @type {EvaluatedPermissions}
@@ -187,7 +203,7 @@ class GuildMember {
const roles = this.roles;
for (const role of roles.values()) permissions |= role.permissions;
- const admin = Boolean(permissions & (Constants.PermissionFlags.ADMINISTRATOR));
+ const admin = Boolean(permissions & Constants.PermissionFlags.ADMINISTRATOR);
if (admin) permissions = Constants.ALL_PERMISSIONS;
return new EvaluatedPermissions(this, permissions);
@@ -256,14 +272,14 @@ 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
* @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions
- * @returns {array}
+ * @returns {PermissionResolvable[]}
*/
missingPermissions(permissions, explicit = false) {
return permissions.filter(p => !this.hasPermission(p, explicit));
}
/**
- * Edit a Guild Member
+ * Edit a guild member
* @param {GuildmemberEditData} data The data to edit the member with
* @returns {Promise}
*/
@@ -290,7 +306,7 @@ class GuildMember {
}
/**
- * Moves the Guild Member to the given channel.
+ * Moves the guild member to the given channel.
* @param {ChannelResolvable} channel The channel to move the member to
* @returns {Promise}
*/
@@ -299,7 +315,7 @@ class GuildMember {
}
/**
- * Sets the Roles applied to the member.
+ * Sets the roles applied to the member.
* @param {Collection|Role[]|string[]} roles The roles or role IDs to apply
* @returns {Promise}
*/
@@ -308,12 +324,13 @@ class GuildMember {
}
/**
- * Adds a single Role to the member.
+ * Adds a single role to the member.
* @param {Role|string} role The role or ID of the role to add
* @returns {Promise}
*/
addRole(role) {
- return this.addRoles([role]);
+ if (!(role instanceof Role)) role = this.guild.roles.get(role);
+ return this.client.rest.methods.addMemberRole(this, role);
}
/**
@@ -333,12 +350,13 @@ class GuildMember {
}
/**
- * Removes a single Role from the member.
+ * Removes a single role from the member.
* @param {Role|string} role The role or ID of the role to remove
* @returns {Promise}
*/
removeRole(role) {
- return this.removeRoles([role]);
+ if (!(role instanceof Role)) role = this.guild.roles.get(role);
+ return this.client.rest.methods.removeMemberRole(this, role);
}
/**
@@ -363,8 +381,8 @@ class GuildMember {
}
/**
- * Set the nickname for the Guild Member
- * @param {string} nick The nickname for the Guild Member
+ * Set the nickname for the guild member
+ * @param {string} nick The nickname for the guild member
* @returns {Promise}
*/
setNickname(nick) {
@@ -372,7 +390,7 @@ class GuildMember {
}
/**
- * Deletes any DMs with this Guild Member
+ * Deletes any DMs with this guild member
* @returns {Promise}
*/
deleteDM() {
@@ -380,7 +398,7 @@ class GuildMember {
}
/**
- * Kick this member from the Guild
+ * Kick this member from the guild
* @returns {Promise}
*/
kick() {
@@ -388,7 +406,7 @@ class GuildMember {
}
/**
- * Ban this Guild Member
+ * Ban this guild member
* @param {number} [deleteDays=0] The amount of days worth of messages from this member that should
* also be deleted. Between `0` and `7`.
* @returns {Promise}
@@ -401,7 +419,7 @@ class GuildMember {
}
/**
- * When concatenated with a string, this automatically concatenates the User's mention instead of the Member object.
+ * When concatenated with a string, this automatically concatenates the user's mention instead of the Member object.
* @returns {string}
* @example
* // logs: Hello from <@123456789>!
@@ -412,8 +430,9 @@ class GuildMember {
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
+ send() { return; }
sendMessage() { return; }
- sendTTSMessage() { return; }
+ sendEmbed() { return; }
sendFile() { return; }
sendCode() { return; }
}
diff --git a/src/structures/Invite.js b/src/structures/Invite.js
index bcf165182..b4b34dafd 100644
--- a/src/structures/Invite.js
+++ b/src/structures/Invite.js
@@ -24,25 +24,26 @@ const Constants = require('../util/Constants');
*/
/**
- * Represents an Invitation to a Guild Channel.
+ * Represents an invitation to a guild channel.
* The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing.
*/
class Invite {
constructor(client, data) {
/**
* The client that instantiated the invite
+ * @name Invite#client
* @type {Client}
+ * @readonly
*/
- this.client = client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: client });
this.setup(data);
}
setup(data) {
/**
- * The Guild the invite is for. If this Guild is already known, this will be a Guild object. If the Guild is
- * unknown, this will be a Partial Guild.
+ * The guild the invite is for. If this guild is already known, this will be a Guild object. If the guild is
+ * unknown, this will be a PartialGuild object.
* @type {Guild|PartialGuild}
*/
this.guild = this.client.guilds.get(data.guild.id) || new PartialGuild(this.client, data.guild);
@@ -86,8 +87,8 @@ class Invite {
}
/**
- * The Channel the invite is for. If this Channel is already known, this will be a GuildChannel object.
- * If the Channel is unknown, this will be a Partial Guild Channel.
+ * The channel the invite is for. If this channel is already known, this will be a GuildChannel object.
+ * If the channel is unknown, this will be a PartialGuildChannel object.
* @type {GuildChannel|PartialGuildChannel}
*/
this.channel = this.client.channels.get(data.channel.id) || new PartialGuildChannel(this.client, data.channel);
@@ -144,7 +145,7 @@ class Invite {
}
/**
- * When concatenated with a string, this automatically concatenates the Invite's URL instead of the object.
+ * When concatenated with a string, this automatically concatenates the invite's URL instead of the object.
* @returns {string}
* @example
* // logs: Invite: https://discord.gg/A1b2C3
diff --git a/src/structures/Message.js b/src/structures/Message.js
index e9e093ac5..7fcc5b4b8 100644
--- a/src/structures/Message.js
+++ b/src/structures/Message.js
@@ -1,20 +1,25 @@
const Attachment = require('./MessageAttachment');
const Embed = require('./MessageEmbed');
+const MessageReaction = require('./MessageReaction');
const Collection = require('../util/Collection');
const Constants = require('../util/Constants');
const escapeMarkdown = require('../util/EscapeMarkdown');
+// Done purely for GuildMember, which would cause a bad circular dependency
+const Discord = require('..');
+
/**
- * Represents a Message on Discord
+ * Represents a message on Discord
*/
class Message {
constructor(channel, data, client) {
/**
- * The client that instantiated the Message
+ * The Client that instantiated the Message
+ * @name Message#client
* @type {Client}
+ * @readonly
*/
- this.client = client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: client });
/**
* The channel that the message was sent in
@@ -25,7 +30,7 @@ class Message {
if (data) this.setup(data);
}
- setup(data) {
+ setup(data) { // eslint-disable-line complexity
/**
* The ID of the message (unique in the channel it was sent)
* @type {string}
@@ -51,7 +56,7 @@ class Message {
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
+ * 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}
*/
@@ -83,7 +88,7 @@ class Message {
/**
* A list of embeds in the message - e.g. YouTube Player
- * @type {Embed[]}
+ * @type {MessageEmbed[]}
*/
this.embeds = data.embeds.map(e => new Embed(this, e));
@@ -148,6 +153,25 @@ class Message {
}
this._edits = [];
+
+ /**
+ * A collection of reactions to this message, mapped by the reaction "id".
+ * @type {Collection}
+ */
+ this.reactions = new Collection();
+
+ if (data.reactions && data.reactions.length > 0) {
+ for (const reaction of data.reactions) {
+ const id = reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name;
+ this.reactions.set(id, new MessageReaction(this, reaction.emoji, reaction.count, reaction.me));
+ }
+ }
+
+ /**
+ * ID of the webhook that sent the message, if applicable
+ * @type {?string}
+ */
+ this.webhookID = data.webhook_id || null;
}
patch(data) { // eslint-disable-line complexity
@@ -199,6 +223,15 @@ class Message {
if (chan) this.mentions.channels.set(chan.id, chan);
}
}
+ if (data.reactions) {
+ this.reactions = new Collection();
+ if (data.reactions.length > 0) {
+ for (const reaction of data.reactions) {
+ const id = reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name;
+ this.reactions.set(id, new MessageReaction(this, data.emoji, data.count, data.me));
+ }
+ }
+ }
}
/**
@@ -266,14 +299,16 @@ class Message {
});
}
- /**
+ /**
* An array of cached versions of the message, including the current version.
* Sorted from latest (first) to oldest (last).
* @type {Message[]}
* @readonly
*/
get edits() {
- return this._edits.slice().unshift(this);
+ const copy = this._edits.slice();
+ copy.unshift(this);
+ return copy;
}
/**
@@ -317,9 +352,30 @@ class Message {
return this.mentions.users.has(data) || this.mentions.channels.has(data) || this.mentions.roles.has(data);
}
+ /**
+ * Whether or not a guild member is mentioned in this message. Takes into account
+ * user mentions, role mentions, and @everyone/@here mentions.
+ * @param {GuildMember|User} member Member/user to check for a mention of
+ * @returns {boolean}
+ */
+ isMemberMentioned(member) {
+ if (this.mentions.everyone) return true;
+ if (this.mentions.users.has(member.id)) return true;
+ if (member instanceof Discord.GuildMember && member.roles.some(r => this.mentions.roles.has(r.id))) return true;
+ return false;
+ }
+
+ /**
+ * Options that can be passed into editMessage
+ * @typedef {Object} MessageEditOptions
+ * @property {Object} [embed] An embed to be added/edited
+ * @property {string|boolean} [code] Language for optional codeblock formatting to apply
+ */
+
/**
* Edit the content of the message
- * @param {StringResolvable} content The new content for the message
+ * @param {StringResolvable} [content] The new content for the message
+ * @param {MessageEditOptions} [options] The options to provide
* @returns {Promise}
* @example
* // update the content of a message
@@ -327,8 +383,14 @@ class Message {
* .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
* .catch(console.error);
*/
- edit(content) {
- return this.client.rest.methods.updateMessage(this, content);
+ edit(content, options) {
+ if (!options && typeof content === 'object') {
+ options = content;
+ content = '';
+ } else if (!options) {
+ options = {};
+ }
+ return this.client.rest.methods.updateMessage(this, content, options);
}
/**
@@ -358,6 +420,26 @@ class Message {
return this.client.rest.methods.unpinMessage(this);
}
+ /**
+ * Add a reaction to the message
+ * @param {string|Emoji|ReactionEmoji} emoji Emoji to react with
+ * @returns {Promise}
+ */
+ react(emoji) {
+ emoji = this.client.resolver.resolveEmojiIdentifier(emoji);
+ if (!emoji) throw new TypeError('Emoji must be a string or Emoji/ReactionEmoji');
+
+ return this.client.rest.methods.addMessageReaction(this, emoji);
+ }
+
+ /**
+ * Remove all reactions from a message
+ * @returns {Promise}
+ */
+ clearReactions() {
+ return this.client.rest.methods.removeMessageReactions(this);
+ }
+
/**
* Deletes the message
* @param {number} [timeout=0] How long to wait to delete the message in milliseconds
@@ -369,13 +451,15 @@ class Message {
* .catch(console.error);
*/
delete(timeout = 0) {
- return new Promise((resolve, reject) => {
- this.client.setTimeout(() => {
- this.client.rest.methods.deleteMessage(this)
- .then(resolve)
- .catch(reject);
- }, timeout);
- });
+ if (timeout <= 0) {
+ return this.client.rest.methods.deleteMessage(this);
+ } else {
+ return new Promise(resolve => {
+ this.client.setTimeout(() => {
+ resolve(this.delete());
+ }, timeout);
+ });
+ }
}
/**
@@ -385,21 +469,22 @@ class Message {
* @returns {Promise}
* @example
* // reply to a message
- * message.reply('Hey, I'm a reply!')
+ * message.reply('Hey, I\'m a reply!')
* .then(msg => console.log(`Sent a reply to ${msg.author}`))
* .catch(console.error);
*/
reply(content, options = {}) {
- content = this.client.resolver.resolveString(content);
- const prepend = this.guild ? `${this.author}, ` : '';
- content = `${prepend}${content}`;
+ content = `${this.guild || this.channel.type === 'group' ? `${this.author}, ` : ''}${content}`;
+ return this.channel.send(content, options);
+ }
- if (options.split) {
- if (typeof options.split !== 'object') options.split = {};
- if (!options.split.prepend) options.split.prepend = prepend;
- }
-
- return this.client.rest.methods.sendMessage(this.channel, content, options);
+ /**
+ * Fetches the webhook used to create this message.
+ * @returns {Promise}
+ */
+ fetchWebhook() {
+ if (!this.webhookID) return Promise.reject(new Error('The message was not sent by a webhook.'));
+ return this.client.fetchWebhook(this.webhookID);
}
/**
@@ -433,7 +518,7 @@ class Message {
}
/**
- * When concatenated with a string, this automatically concatenates the Message's content instead of the object.
+ * When concatenated with a string, this automatically concatenates the message's content instead of the object.
* @returns {string}
* @example
* // logs: Message: This is a message!
@@ -442,6 +527,42 @@ class Message {
toString() {
return this.content;
}
+
+ _addReaction(emoji, user) {
+ const emojiID = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name;
+ let reaction;
+ if (this.reactions.has(emojiID)) {
+ reaction = this.reactions.get(emojiID);
+ if (!reaction.me) reaction.me = user.id === this.client.user.id;
+ } else {
+ reaction = new MessageReaction(this, emoji, 0, user.id === this.client.user.id);
+ this.reactions.set(emojiID, reaction);
+ }
+ if (!reaction.users.has(user.id)) {
+ reaction.users.set(user.id, user);
+ reaction.count++;
+ return reaction;
+ }
+ return null;
+ }
+
+ _removeReaction(emoji, user) {
+ const emojiID = emoji.id || emoji;
+ if (this.reactions.has(emojiID)) {
+ const reaction = this.reactions.get(emojiID);
+ if (reaction.users.has(user.id)) {
+ reaction.users.delete(user.id);
+ reaction.count--;
+ if (user.id === this.client.user.id) reaction.me = false;
+ return reaction;
+ }
+ }
+ return null;
+ }
+
+ _clearReactions() {
+ this.reactions.clear();
+ }
}
module.exports = Message;
diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js
index e9573a7be..29dfb524e 100644
--- a/src/structures/MessageAttachment.js
+++ b/src/structures/MessageAttachment.js
@@ -1,14 +1,15 @@
/**
- * Represents an Attachment in a Message
+ * Represents an attachment in a message
*/
class MessageAttachment {
constructor(message, data) {
/**
- * The Client that instantiated this Message.
+ * The Client that instantiated this MessageAttachment.
+ * @name MessageAttachment#client
* @type {Client}
+ * @readonly
*/
- this.client = message.client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: message.client });
/**
* The message this attachment is part of.
diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js
index 375bc845a..f84ecbda3 100644
--- a/src/structures/MessageCollector.js
+++ b/src/structures/MessageCollector.js
@@ -16,7 +16,7 @@ class MessageCollector extends EventEmitter {
* return false; // failed the filter test
* }
* ```
- * @typedef {function} CollectorFilterFunction
+ * @typedef {Function} CollectorFilterFunction
*/
/**
@@ -54,7 +54,7 @@ class MessageCollector extends EventEmitter {
this.options = options;
/**
- * Whether this collector has stopped collecting Messages.
+ * Whether this collector has stopped collecting messages.
* @type {boolean}
*/
this.ended = false;
@@ -81,7 +81,7 @@ class MessageCollector extends EventEmitter {
if (this.filter(message, this)) {
this.collected.set(message.id, message);
/**
- * Emitted whenever the Collector receives a Message that passes the filter test.
+ * Emitted whenever the collector receives a message that passes the filter test.
* @param {Message} message The received message
* @param {MessageCollector} collector The collector the message passed through
* @event MessageCollector#message
@@ -138,7 +138,7 @@ class MessageCollector extends EventEmitter {
/**
* Emitted when the Collector stops collecting.
* @param {Collection} collection A collection of messages collected
- * during the lifetime of the Collector, mapped by the ID of the Messages.
+ * during the lifetime of the collector, mapped by the ID of the messages.
* @param {string} reason The reason for the end of the collector. If it ended because it reached the specified time
* limit, this would be `time`. If you invoke `.stop()` without specifying a reason, this would be `user`. If it
* ended because it reached its message limit, it will be `limit`.
diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js
index 850e443d8..1249c422b 100644
--- a/src/structures/MessageEmbed.js
+++ b/src/structures/MessageEmbed.js
@@ -1,14 +1,15 @@
/**
- * Represents an embed in an image - e.g. preview of image
+ * Represents an embed in a message (image/video preview, rich embed, etc.)
*/
class MessageEmbed {
constructor(message, data) {
/**
* The client that instantiated this embed
+ * @name MessageEmbed#client
* @type {Client}
+ * @readonly
*/
- this.client = message.client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: message.client });
/**
* The message this embed is part of
@@ -20,18 +21,18 @@ class MessageEmbed {
}
setup(data) {
- /**
- * The title of this embed, if there is one
- * @type {?string}
- */
- this.title = data.title;
-
/**
* The type of this embed
* @type {string}
*/
this.type = data.type;
+ /**
+ * The title of this embed, if there is one
+ * @type {?string}
+ */
+ this.title = data.title;
+
/**
* The description of this embed, if there is one
* @type {?string}
@@ -44,6 +45,25 @@ class MessageEmbed {
*/
this.url = data.url;
+ /**
+ * The color of the embed
+ * @type {number}
+ */
+ this.color = data.color;
+
+ /**
+ * The fields of this embed
+ * @type {MessageEmbedField[]}
+ */
+ this.fields = [];
+ if (data.fields) for (const field of data.fields) this.fields.push(new MessageEmbedField(this, field));
+
+ /**
+ * The timestamp of this embed
+ * @type {number}
+ */
+ this.createdTimestamp = data.timestamp;
+
/**
* The thumbnail of this embed, if there is one
* @type {MessageEmbedThumbnail}
@@ -61,11 +81,36 @@ class MessageEmbed {
* @type {MessageEmbedProvider}
*/
this.provider = data.provider ? new MessageEmbedProvider(this, data.provider) : null;
+
+ /**
+ * The footer of this embed
+ * @type {MessageEmbedFooter}
+ */
+ this.footer = data.footer ? new MessageEmbedFooter(this, data.footer) : null;
+ }
+
+ /**
+ * The date this embed was created
+ * @type {Date}
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ /**
+ * The hexadecimal version of the embed color, with a leading hash.
+ * @type {string}
+ * @readonly
+ */
+ get hexColor() {
+ let col = this.color.toString(16);
+ while (col.length < 6) col = `0${col}`;
+ return `#${col}`;
}
}
/**
- * Represents a thumbnail for a Message embed
+ * Represents a thumbnail for a message embed
*/
class MessageEmbedThumbnail {
constructor(embed, data) {
@@ -106,7 +151,7 @@ class MessageEmbedThumbnail {
}
/**
- * Represents a Provider for a Message embed
+ * Represents a provider for a message embed
*/
class MessageEmbedProvider {
constructor(embed, data) {
@@ -135,7 +180,7 @@ class MessageEmbedProvider {
}
/**
- * Represents a Author for a Message embed
+ * Represents an author for a message embed
*/
class MessageEmbedAuthor {
constructor(embed, data) {
@@ -160,11 +205,89 @@ class MessageEmbedAuthor {
* @type {string}
*/
this.url = data.url;
+
+ /**
+ * The icon URL of this author
+ * @type {string}
+ */
+ this.iconURL = data.icon_url;
+ }
+}
+
+/**
+ * Represents a field for a message embed
+ */
+class MessageEmbedField {
+ constructor(embed, data) {
+ /**
+ * The embed this footer is part of
+ * @type {MessageEmbed}
+ */
+ this.embed = embed;
+
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The name of this field
+ * @type {string}
+ */
+ this.name = data.name;
+
+ /**
+ * The value of this field
+ * @type {string}
+ */
+ this.value = data.value;
+
+ /**
+ * If this field is displayed inline
+ * @type {boolean}
+ */
+ this.inline = data.inline;
+ }
+}
+
+/**
+ * Represents the footer of a message embed
+ */
+class MessageEmbedFooter {
+ constructor(embed, data) {
+ /**
+ * The embed this footer is part of
+ * @type {MessageEmbed}
+ */
+ this.embed = embed;
+
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The text in this footer
+ * @type {string}
+ */
+ this.text = data.text;
+
+ /**
+ * The icon URL of this footer
+ * @type {string}
+ */
+ this.iconURL = data.icon_url;
+
+ /**
+ * The proxy icon URL of this footer
+ * @type {string}
+ */
+ this.proxyIconUrl = data.proxy_icon_url;
}
}
MessageEmbed.Thumbnail = MessageEmbedThumbnail;
MessageEmbed.Provider = MessageEmbedProvider;
MessageEmbed.Author = MessageEmbedAuthor;
+MessageEmbed.Field = MessageEmbedField;
+MessageEmbed.Footer = MessageEmbedFooter;
module.exports = MessageEmbed;
diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js
new file mode 100644
index 000000000..30c555f9d
--- /dev/null
+++ b/src/structures/MessageReaction.js
@@ -0,0 +1,92 @@
+const Collection = require('../util/Collection');
+const Emoji = require('./Emoji');
+const ReactionEmoji = require('./ReactionEmoji');
+
+/**
+ * Represents a reaction to a message
+ */
+class MessageReaction {
+ constructor(message, emoji, count, me) {
+ /**
+ * The message that this reaction refers to
+ * @type {Message}
+ */
+ this.message = message;
+
+ /**
+ * Whether the client has given this reaction
+ * @type {boolean}
+ */
+ this.me = me;
+
+ /**
+ * The number of people that have given the same reaction.
+ * @type {number}
+ */
+ this.count = count || 0;
+
+ /**
+ * The users that have given this reaction, mapped by their ID.
+ * @type {Collection}
+ */
+ this.users = new Collection();
+
+ this._emoji = new ReactionEmoji(this, emoji.name, emoji.id);
+ }
+
+ /**
+ * The emoji of this reaction, either an Emoji object for known custom emojis, or a ReactionEmoji
+ * object which has fewer properties. Whatever the prototype of the emoji, it will still have
+ * `name`, `id`, `identifier` and `toString()`
+ * @type {Emoji|ReactionEmoji}
+ */
+ get emoji() {
+ if (this._emoji instanceof Emoji) return this._emoji;
+ // check to see if the emoji has become known to the client
+ if (this._emoji.id) {
+ const emojis = this.message.client.emojis;
+ if (emojis.has(this._emoji.id)) {
+ const emoji = emojis.get(this._emoji.id);
+ this._emoji = emoji;
+ return emoji;
+ }
+ }
+ return this._emoji;
+ }
+
+ /**
+ * Removes a user from this reaction.
+ * @param {UserResolvable} [user=this.message.client.user] User to remove the reaction of
+ * @returns {Promise}
+ */
+ remove(user = this.message.client.user) {
+ const message = this.message;
+ user = this.message.client.resolver.resolveUserID(user);
+ if (!user) return Promise.reject('Couldn\'t resolve the user ID to remove from the reaction.');
+ return message.client.rest.methods.removeMessageReaction(
+ message, this.emoji.identifier, user
+ );
+ }
+
+ /**
+ * 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
+ * @returns {Promise>}
+ */
+ fetchUsers(limit = 100) {
+ const message = this.message;
+ return message.client.rest.methods.getMessageReactionUsers(
+ message, this.emoji.identifier, limit
+ ).then(users => {
+ this.users = new Collection();
+ for (const rawUser of users) {
+ const user = this.message.client.dataManager.newUser(rawUser);
+ this.users.set(user.id, user);
+ }
+ this.count = this.users.size;
+ return users;
+ });
+ }
+}
+
+module.exports = MessageReaction;
diff --git a/src/structures/OAuth2Application.js b/src/structures/OAuth2Application.js
new file mode 100644
index 000000000..b7c728581
--- /dev/null
+++ b/src/structures/OAuth2Application.js
@@ -0,0 +1,82 @@
+/**
+ * Represents an OAuth2 Application
+ */
+class OAuth2Application {
+ constructor(client, data) {
+ /**
+ * The client that instantiated the application
+ * @name OAuth2Application#client
+ * @type {Client}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The ID of the app
+ * @type {string}
+ */
+ this.id = data.id;
+
+ /**
+ * The name of the app
+ * @type {string}
+ */
+ this.name = data.name;
+
+ /**
+ * The app's description
+ * @type {string}
+ */
+ this.description = data.description;
+
+ /**
+ * The app's icon hash
+ * @type {string}
+ */
+ this.icon = data.icon;
+
+ /**
+ * The app's icon URL
+ * @type {string}
+ */
+ this.iconURL = `https://cdn.discordapp.com/app-icons/${this.id}/${this.icon}.jpg`;
+
+ /**
+ * The app's RPC origins
+ * @type {Array}
+ */
+ this.rpcOrigins = data.rpc_origins;
+ }
+
+ /**
+ * The timestamp the app was created at
+ * @type {number}
+ * @readonly
+ */
+ get createdTimestamp() {
+ return (this.id / 4194304) + 1420070400000;
+ }
+
+ /**
+ * The time the app was created
+ * @type {Date}
+ * @readonly
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ /**
+ * When concatenated with a string, this automatically concatenates the app name rather than the app object.
+ * @returns {string}
+ */
+ toString() {
+ return this.name;
+ }
+}
+
+module.exports = OAuth2Application;
diff --git a/src/structures/PartialGuild.js b/src/structures/PartialGuild.js
index d605cc274..407212e2f 100644
--- a/src/structures/PartialGuild.js
+++ b/src/structures/PartialGuild.js
@@ -6,16 +6,17 @@
*/
/**
- * Represents a Guild that the client only has limited information for - e.g. from invites.
+ * Represents a guild that the client only has limited information for - e.g. from invites.
*/
class PartialGuild {
constructor(client, data) {
/**
- * The client that instantiated this PartialGuild
+ * The Client that instantiated this PartialGuild
+ * @name PartialGuild#client
* @type {Client}
+ * @readonly
*/
- this.client = client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: client });
this.setup(data);
}
diff --git a/src/structures/PartialGuildChannel.js b/src/structures/PartialGuildChannel.js
index 47e33884d..e58a6bb91 100644
--- a/src/structures/PartialGuildChannel.js
+++ b/src/structures/PartialGuildChannel.js
@@ -5,35 +5,36 @@ const Constants = require('../util/Constants');
*/
/**
- * Represents a Guild Channel that the client only has limited information for - e.g. from invites.
+ * Represents a guild channel that the client only has limited information for - e.g. from invites.
*/
class PartialGuildChannel {
constructor(client, data) {
/**
- * The client that instantiated this PartialGuildChannel
+ * The Client that instantiated this PartialGuildChannel
+ * @name PartialGuildChannel#client
* @type {Client}
+ * @readonly
*/
- this.client = client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: client });
this.setup(data);
}
setup(data) {
/**
- * The ID of this Guild Channel
+ * The ID of this guild channel
* @type {string}
*/
this.id = data.id;
/**
- * The name of this Guild Channel
+ * The name of this guild channel
* @type {string}
*/
this.name = data.name;
/**
- * The type of this Guild Channel - `text` or `voice`
+ * The type of this guild channel - `text` or `voice`
* @type {string}
*/
this.type = Constants.ChannelTypes.text === data.type ? 'text' : 'voice';
diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js
index b1b2944fc..9b2f536f5 100644
--- a/src/structures/PermissionOverwrites.js
+++ b/src/structures/PermissionOverwrites.js
@@ -1,20 +1,22 @@
/**
- * Represents a permission overwrite for a Role or Member in a Guild Channel.
+ * Represents a permission overwrite for a role or member in a guild channel.
*/
class PermissionOverwrites {
constructor(guildChannel, data) {
/**
* The GuildChannel this overwrite is for
+ * @name PermissionOverwrites#channel
* @type {GuildChannel}
+ * @readonly
*/
- this.channel = guildChannel;
+ Object.defineProperty(this, 'channel', { value: guildChannel });
if (data) this.setup(data);
}
setup(data) {
/**
- * The ID of this overwrite, either a User ID or a Role ID
+ * The ID of this overwrite, either a user ID or a role ID
* @type {string}
*/
this.id = data.id;
@@ -25,8 +27,8 @@ class PermissionOverwrites {
*/
this.type = data.type;
- this.denyData = data.deny;
- this.allowData = data.allow;
+ this.deny = data.deny;
+ this.allow = data.allow;
}
/**
diff --git a/src/structures/Presence.js b/src/structures/Presence.js
index d84a4c9bc..ddca5fb34 100644
--- a/src/structures/Presence.js
+++ b/src/structures/Presence.js
@@ -1,11 +1,8 @@
/**
- * Represents a User's presence
+ * Represents a user's presence
*/
class Presence {
- constructor(data) {
- if (!data) {
- data = {};
- }
+ constructor(data = {}) {
/**
* The status of the presence:
*
@@ -31,20 +28,20 @@ class Presence {
/**
* Whether this presence is equal to another
- * @param {Presence} other the presence to compare
+ * @param {Presence} presence Presence to compare with
* @returns {boolean}
*/
- equals(other) {
- return (
- other &&
- this.status === other.status &&
- this.game ? this.game.equals(other.game) : !other.game
+ equals(presence) {
+ return this === presence || (
+ presence &&
+ this.status === presence.status &&
+ this.game ? this.game.equals(presence.game) : !presence.game
);
}
}
/**
- * Represents a Game that is part of a User's presence.
+ * Represents a game that is part of a user's presence.
*/
class Game {
constructor(data) {
@@ -78,15 +75,15 @@ class Game {
/**
* Whether this game is equal to another game
- * @param {Game} other the other game to compare
+ * @param {Game} game Game to compare with
* @returns {boolean}
*/
- equals(other) {
- return (
- other &&
- this.name === other.name &&
- this.type === other.type &&
- this.url === other.url
+ equals(game) {
+ return this === game || (
+ game &&
+ this.name === game.name &&
+ this.type === game.type &&
+ this.url === game.url
);
}
}
diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js
new file mode 100644
index 000000000..b6d2cdbd6
--- /dev/null
+++ b/src/structures/ReactionEmoji.js
@@ -0,0 +1,49 @@
+/**
+ * Represents a limited emoji set used for both custom and unicode emojis. Custom emojis
+ * will use this class opposed to the Emoji class when the client doesn't know enough
+ * information about them.
+ */
+class ReactionEmoji {
+ constructor(reaction, name, id) {
+ /**
+ * The message reaction this emoji refers to
+ * @type {MessageReaction}
+ */
+ this.reaction = reaction;
+
+ /**
+ * The name of this reaction emoji.
+ * @type {string}
+ */
+ this.name = name;
+
+ /**
+ * The ID of this reaction emoji.
+ * @type {string}
+ */
+ this.id = id;
+ }
+
+ /**
+ * The identifier of this emoji, used for message reactions
+ * @readonly
+ * @type {string}
+ */
+ get identifier() {
+ if (this.id) return `${this.name}:${this.id}`;
+ return encodeURIComponent(this.name);
+ }
+
+ /**
+ * Creates the text required to form a graphical emoji on Discord.
+ * @example
+ * // send the emoji used in a reaction to the channel the reaction is part of
+ * reaction.message.channel.sendMessage(`The emoji used is ${reaction.emoji}`);
+ * @returns {string}
+ */
+ toString() {
+ return this.id ? `<:${this.name}:${this.id}>` : this.name;
+ }
+}
+
+module.exports = ReactionEmoji;
diff --git a/src/structures/RichEmbed.js b/src/structures/RichEmbed.js
new file mode 100644
index 000000000..fbd9383d3
--- /dev/null
+++ b/src/structures/RichEmbed.js
@@ -0,0 +1,204 @@
+/**
+ * A rich embed to be sent with a message
+ * @param {Object} [data] Data to set in the rich embed
+ */
+class RichEmbed {
+ constructor(data = {}) {
+ /**
+ * Title for this Embed
+ * @type {string}
+ */
+ this.title = data.title;
+
+ /**
+ * Description for this Embed
+ * @type {string}
+ */
+ this.description = data.description;
+
+ /**
+ * URL for this Embed
+ * @type {string}
+ */
+ this.url = data.url;
+
+ /**
+ * Color for this Embed
+ * @type {number}
+ */
+ this.color = data.color;
+
+ /**
+ * Author for this Embed
+ * @type {Object}
+ */
+ this.author = data.author;
+
+ /**
+ * Timestamp for this Embed
+ * @type {Date}
+ */
+ this.timestamp = data.timestamp;
+
+ /**
+ * Fields for this Embed
+ * @type {Object[]}
+ */
+ this.fields = data.fields || [];
+
+ /**
+ * Thumbnail for this Embed
+ * @type {Object}
+ */
+ this.thumbnail = data.thumbnail;
+
+ /**
+ * Image for this Embed
+ * @type {Object}
+ */
+ this.image = data.image;
+
+ /**
+ * Footer for this Embed
+ * @type {Object}
+ */
+ this.footer = data.footer;
+ }
+
+ /**
+ * Sets the title of this embed
+ * @param {StringResolvable} title The title
+ * @returns {RichEmbed} This embed
+ */
+ setTitle(title) {
+ title = resolveString(title);
+ if (title.length > 256) throw new RangeError('RichEmbed titles may not exceed 256 characters.');
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * Sets the description of this embed
+ * @param {StringResolvable} description The description
+ * @returns {RichEmbed} This embed
+ */
+ setDescription(description) {
+ description = resolveString(description);
+ if (description.length > 2048) throw new RangeError('RichEmbed descriptions may not exceed 2048 characters.');
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * Sets the URL of this embed
+ * @param {string} url The URL
+ * @returns {RichEmbed} This embed
+ */
+ setURL(url) {
+ this.url = url;
+ return this;
+ }
+
+ /**
+ * Sets the color of this embed
+ * @param {string|number|number[]} color The color to set
+ * @returns {RichEmbed} This embed
+ */
+ setColor(color) {
+ let radix = 10;
+ if (color instanceof Array) {
+ color = (color[0] << 16) + (color[1] << 8) + color[2];
+ } else if (typeof color === 'string' && color.startsWith('#')) {
+ radix = 16;
+ color = color.replace('#', '');
+ }
+ color = parseInt(color, radix);
+ if (color < 0 || color > 0xFFFFFF) {
+ throw new RangeError('RichEmbed color must be within the range 0 - 16777215 (0xFFFFFF).');
+ } else if (color && isNaN(color)) {
+ throw new TypeError('Unable to convert RichEmbed color to a number.');
+ }
+ this.color = color;
+ return this;
+ }
+
+ /**
+ * Sets the author of this embed
+ * @param {StringResolvable} name The name of the author
+ * @param {string} [icon] The icon URL of the author
+ * @param {string} [url] The URL of the author
+ * @returns {RichEmbed} This embed
+ */
+ setAuthor(name, icon, url) {
+ this.author = { name: resolveString(name), icon_url: icon, url };
+ return this;
+ }
+
+ /**
+ * Sets the timestamp of this embed
+ * @param {Date} [timestamp=current date] The timestamp
+ * @returns {RichEmbed} This embed
+ */
+ setTimestamp(timestamp = new Date()) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ /**
+ * Adds a field to the embed (max 25)
+ * @param {StringResolvable} name The name of the field
+ * @param {StringResolvable} value The value of the field
+ * @param {boolean} [inline=false] Set the field to display inline
+ * @returns {RichEmbed} This embed
+ */
+ addField(name, value, inline = false) {
+ if (this.fields.length >= 25) throw new RangeError('RichEmbeds may not exceed 25 fields.');
+ name = resolveString(name);
+ if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.');
+ value = resolveString(value);
+ if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.');
+ this.fields.push({ name: String(name), value: value, inline });
+ return this;
+ }
+
+ /**
+ * Set the thumbnail of this embed
+ * @param {string} url The URL of the thumbnail
+ * @returns {RichEmbed} This embed
+ */
+ setThumbnail(url) {
+ this.thumbnail = { url };
+ return this;
+ }
+
+ /**
+ * Set the image of this embed
+ * @param {string} url The URL of the thumbnail
+ * @returns {RichEmbed} This embed
+ */
+ setImage(url) {
+ this.image = { url };
+ return this;
+ }
+
+ /**
+ * Sets the footer of this embed
+ * @param {StringResolvable} text The text of the footer
+ * @param {string} [icon] The icon URL of the footer
+ * @returns {RichEmbed} This embed
+ */
+ setFooter(text, icon) {
+ text = resolveString(text);
+ if (text.length > 2048) throw new RangeError('RichEmbed footer text may not exceed 2048 characters.');
+ this.footer = { text, icon_url: icon };
+ return this;
+ }
+}
+
+module.exports = RichEmbed;
+
+function resolveString(data) {
+ if (typeof data === 'string') return data;
+ if (data instanceof Array) return data.join('\n');
+ return String(data);
+}
diff --git a/src/structures/Role.js b/src/structures/Role.js
index 4bbb6c55b..c15ff4be0 100644
--- a/src/structures/Role.js
+++ b/src/structures/Role.js
@@ -1,16 +1,17 @@
const Constants = require('../util/Constants');
/**
- * Represents a Role on Discord
+ * Represents a role on Discord
*/
class Role {
constructor(guild, data) {
/**
* The client that instantiated the role
+ * @name Role#client
* @type {Client}
+ * @readonly
*/
- this.client = guild.client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: guild.client });
/**
* The guild that the role belongs to
@@ -109,6 +110,18 @@ class Role {
return this.guild.members.filter(m => m.roles.has(this.id));
}
+ /**
+ * Whether the role is editable by the client user.
+ * @type {boolean}
+ * @readonly
+ */
+ get editable() {
+ if (this.managed) return false;
+ const clientMember = this.guild.member(this.client.user);
+ if (!clientMember.hasPermission(Constants.PermissionFlags.MANAGE_ROLES_OR_PERMISSIONS)) return false;
+ return clientMember.highestRole.comparePositionTo(this) > 0;
+ }
+
/**
* Get an object mapping permission names to whether or not the role enables that permission
* @returns {Object}
@@ -163,6 +176,17 @@ class Role {
return this.constructor.comparePositions(this, role);
}
+ /**
+ * The data for a role
+ * @typedef {Object} RoleData
+ * @property {string} [name] The name of the role
+ * @property {number|string} [color] The color of the role, either a hex string or a base 10 number
+ * @property {boolean} [hoist] Whether or not the role should be hoisted
+ * @property {number} [position] The position of the role
+ * @property {string[]} [permissions] The permissions of the role
+ * @property {boolean} [mentionable] Whether or not the role should be mentionable
+ */
+
/**
* Edits the role
* @param {RoleData} data The new data for the role
@@ -188,7 +212,7 @@ class Role {
* .catch(console.error);
*/
setName(name) {
- return this.client.rest.methods.updateGuildRole(this, { name });
+ return this.edit({ name });
}
/**
@@ -202,7 +226,7 @@ class Role {
* .catch(console.error);
*/
setColor(color) {
- return this.client.rest.methods.updateGuildRole(this, { color });
+ return this.edit({ color });
}
/**
@@ -216,7 +240,7 @@ class Role {
* .catch(console.error);
*/
setHoist(hoist) {
- return this.client.rest.methods.updateGuildRole(this, { hoist });
+ return this.edit({ hoist });
}
/**
@@ -230,7 +254,7 @@ class Role {
* .catch(console.error);
*/
setPosition(position) {
- return this.guild.setRolePosition(this, position);
+ return this.guild.setRolePosition(this, position).then(() => this);
}
/**
@@ -244,7 +268,7 @@ class Role {
* .catch(console.error);
*/
setPermissions(permissions) {
- return this.client.rest.methods.updateGuildRole(this, { permissions });
+ return this.edit({ permissions });
}
/**
@@ -258,7 +282,7 @@ class Role {
* .catch(console.error);
*/
setMentionable(mentionable) {
- return this.client.rest.methods.updateGuildRole(this, { mentionable });
+ return this.edit({ mentionable });
}
/**
@@ -278,7 +302,7 @@ class Role {
* Whether this role equals another role. It compares all properties, so for most operations
* it is advisable to just compare `role.id === role2.id` as it is much faster and is often
* what most users need.
- * @param {Role} role The role to compare to
+ * @param {Role} role Role to compare with
* @returns {boolean}
*/
equals(role) {
@@ -293,10 +317,11 @@ class Role {
}
/**
- * When concatenated with a string, this automatically concatenates the Role mention rather than the Role object.
+ * When concatenated with a string, this automatically concatenates the role mention rather than the Role object.
* @returns {string}
*/
toString() {
+ if (this.id === this.guild.id) return '@everyone';
return `<@&${this.id}>`;
}
diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js
index 4b69f91a6..9697abd82 100644
--- a/src/structures/TextChannel.js
+++ b/src/structures/TextChannel.js
@@ -3,7 +3,7 @@ const TextBasedChannel = require('./interface/TextBasedChannel');
const Collection = require('../util/Collection');
/**
- * Represents a Server Text Channel on Discord.
+ * Represents a guild text channel on Discord.
* @extends {GuildChannel}
* @implements {TextBasedChannel}
*/
@@ -19,7 +19,7 @@ class TextChannel extends GuildChannel {
super.setup(data);
/**
- * The topic of the Text Channel, if there is one.
+ * The topic of the text channel, if there is one.
* @type {?string}
*/
this.topic = data.topic;
@@ -53,7 +53,7 @@ class TextChannel extends GuildChannel {
/**
* Create a webhook for the channel.
* @param {string} name The name of the webhook.
- * @param {FileResolvable} avatar The avatar for the webhook.
+ * @param {BufferResolvable} avatar The avatar for the webhook.
* @returns {Promise} webhook The created webhook.
* @example
* channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png')
@@ -61,22 +61,21 @@ class TextChannel extends GuildChannel {
* .catch(console.error)
*/
createWebhook(name, avatar) {
- return new Promise((resolve, reject) => {
- if (avatar) {
- this.client.resolver.resolveFile(avatar).then(file => {
- let base64 = new Buffer(file, 'binary').toString('base64');
- let dataURI = `data:;base64,${base64}`;
- this.client.rest.methods.createWebhook(this, name, dataURI).then(resolve).catch(reject);
- }).catch(reject);
+ return new Promise(resolve => {
+ if (avatar.startsWith('data:')) {
+ resolve(this.client.rest.methods.createWebhook(this, name, avatar));
} else {
- this.client.rest.methods.createWebhook(this, name).then(resolve).catch(reject);
+ this.client.resolver.resolveBuffer(avatar).then(data =>
+ resolve(this.client.rest.methods.createWebhook(this, name, data))
+ );
}
});
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
+ send() { return; }
sendMessage() { return; }
- sendTTSMessage() { return; }
+ sendEmbed() { return; }
sendFile() { return; }
sendCode() { return; }
fetchMessage() { return; }
diff --git a/src/structures/User.js b/src/structures/User.js
index b9ac5b522..f7148289a 100644
--- a/src/structures/User.js
+++ b/src/structures/User.js
@@ -3,36 +3,37 @@ const Constants = require('../util/Constants');
const Presence = require('./Presence').Presence;
/**
- * Represents a User on Discord.
+ * Represents a user on Discord.
* @implements {TextBasedChannel}
*/
class User {
constructor(client, data) {
/**
* The Client that created the instance of the the User.
+ * @name User#client
* @type {Client}
+ * @readonly
*/
- this.client = client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: client });
if (data) this.setup(data);
}
setup(data) {
/**
- * The ID of the User
+ * The ID of the user
* @type {string}
*/
this.id = data.id;
/**
- * The username of the User
+ * The username of the user
* @type {string}
*/
this.username = data.username;
/**
- * A discriminator based on username for the User
+ * A discriminator based on username for the user
* @type {string}
*/
this.discriminator = data.discriminator;
@@ -44,16 +45,23 @@ class User {
this.avatar = data.avatar;
/**
- * Whether or not the User is a Bot.
+ * Whether or not the user is a bot.
* @type {boolean}
*/
this.bot = Boolean(data.bot);
+
+ /**
+ * The ID of the last message sent by the user, if one was sent.
+ * @type {?string}
+ */
+ this.lastMessageID = null;
}
patch(data) {
for (const prop of ['id', 'username', 'discriminator', 'avatar', 'bot']) {
if (typeof data[prop] !== 'undefined') this[prop] = data[prop];
}
+ if (data.token) this.client.token = data.token;
}
/**
@@ -97,6 +105,36 @@ class User {
return Constants.Endpoints.avatar(this.id, this.avatar);
}
+ /**
+ * A link to the user's default avatar
+ * @type {string}
+ * @readonly
+ */
+ get defaultAvatarURL() {
+ let defaultAvatars = Object.values(Constants.DefaultAvatars);
+ let defaultAvatar = this.discriminator % defaultAvatars.length;
+ return Constants.Endpoints.assets(`${defaultAvatars[defaultAvatar]}.png`);
+ }
+
+ /**
+ * A link to the user's avatar if they have one. Otherwise a link to their default avatar will be returned
+ * @type {string}
+ * @readonly
+ */
+ get displayAvatarURL() {
+ return this.avatarURL || this.defaultAvatarURL;
+ }
+
+ /**
+ * The note that is set for the user
+ * This is only available when using a user account.
+ * @type {?string}
+ * @readonly
+ */
+ get note() {
+ return this.client.user.notes.get(this.id) || null;
+ }
+
/**
* Check whether the user is typing in a channel.
* @param {ChannelResolvable} channel The channel to check in
@@ -128,7 +166,15 @@ class User {
}
/**
- * Deletes a DM Channel (if one exists) between the Client and the User. Resolves with the Channel if successful.
+ * The DM between the client's user and this user
+ * @type {?DMChannel}
+ */
+ get dmChannel() {
+ return this.client.channels.filter(c => c.type === 'dm').find(c => c.recipient.id === this.id);
+ }
+
+ /**
+ * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful.
* @returns {Promise}
*/
deleteDM() {
@@ -137,6 +183,7 @@ class User {
/**
* Sends a friend request to the user
+ * This is only available when using a user account.
* @returns {Promise}
*/
addFriend() {
@@ -145,6 +192,7 @@ class User {
/**
* Removes the user from your friends
+ * This is only available when using a user account.
* @returns {Promise}
*/
removeFriend() {
@@ -153,6 +201,7 @@ class User {
/**
* Blocks the user
+ * This is only available when using a user account.
* @returns {Promise}
*/
block() {
@@ -161,6 +210,7 @@ class User {
/**
* Unblocks the user
+ * This is only available when using a user account.
* @returns {Promise}
*/
unblock() {
@@ -169,6 +219,7 @@ class User {
/**
* Get the profile of the user
+ * This is only available when using a user account.
* @returns {Promise}
*/
fetchProfile() {
@@ -176,9 +227,19 @@ class User {
}
/**
- * Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
+ * Sets a note for the user
+ * This is only available when using a user account.
+ * @param {string} note The note to set for the user
+ * @returns {Promise}
+ */
+ setNote(note) {
+ return this.client.rest.methods.setNote(this, note);
+ }
+
+ /**
+ * Checks if the user is equal to another. It compares ID, username, discriminator, avatar, and bot flags.
* It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
- * @param {User} user The user to compare
+ * @param {User} user User to compare with
* @returns {boolean}
*/
equals(user) {
@@ -193,7 +254,7 @@ class User {
}
/**
- * When concatenated with a string, this automatically concatenates the User's mention instead of the User object.
+ * When concatenated with a string, this automatically concatenates the user's mention instead of the User object.
* @returns {string}
* @example
* // logs: Hello from <@123456789>!
@@ -204,8 +265,9 @@ class User {
}
// These are here only for documentation purposes - they are implemented by TextBasedChannel
+ send() { return; }
sendMessage() { return; }
- sendTTSMessage() { return; }
+ sendEmbed() { return; }
sendFile() { return; }
sendCode() { return; }
}
diff --git a/src/structures/UserConnection.js b/src/structures/UserConnection.js
index d25df0300..6ee9fc5b4 100644
--- a/src/structures/UserConnection.js
+++ b/src/structures/UserConnection.js
@@ -1,5 +1,5 @@
/**
- * Represents a User Connection object (or "platform identity")
+ * Represents a user connection (or "platform identity")
*/
class UserConnection {
constructor(user, data) {
@@ -33,7 +33,7 @@ class UserConnection {
/**
* Whether the connection is revoked
- * @type {Boolean}
+ * @type {boolean}
*/
this.revoked = data.revoked;
diff --git a/src/structures/UserProfile.js b/src/structures/UserProfile.js
index 4150b3a72..77f097ca9 100644
--- a/src/structures/UserProfile.js
+++ b/src/structures/UserProfile.js
@@ -13,14 +13,15 @@ class UserProfile {
this.user = user;
/**
- * The Client that created the instance of the the User.
+ * The Client that created the instance of the the UserProfile.
+ * @name UserProfile#client
* @type {Client}
+ * @readonly
*/
- this.client = this.user.client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: user.client });
/**
- * Guilds that the ClientUser and the User share
+ * Guilds that the client user and the user share
* @type {Collection}
*/
this.mutualGuilds = new Collection();
@@ -35,6 +36,12 @@ class UserProfile {
}
setup(data) {
+ /**
+ * If the user has Discord Premium
+ * @type {boolean}
+ */
+ this.premium = data.premium;
+
for (const guild of data.mutual_guilds) {
if (this.client.guilds.has(guild.id)) {
this.mutualGuilds.set(guild.id, this.client.guilds.get(guild.id));
diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js
index 162e715f3..848a6d513 100644
--- a/src/structures/VoiceChannel.js
+++ b/src/structures/VoiceChannel.js
@@ -2,7 +2,7 @@ const GuildChannel = require('./GuildChannel');
const Collection = require('../util/Collection');
/**
- * Represents a Server Voice Channel on Discord.
+ * Represents a guild voice channel on Discord.
* @extends {GuildChannel}
*/
class VoiceChannel extends GuildChannel {
@@ -10,7 +10,7 @@ class VoiceChannel extends GuildChannel {
super(guild, data);
/**
- * The members in this Voice Channel.
+ * The members in this voice channel.
* @type {Collection}
*/
this.members = new Collection();
@@ -50,6 +50,7 @@ class VoiceChannel extends GuildChannel {
* @type {boolean}
*/
get joinable() {
+ if (this.client.browser) return false;
return this.permissionsFor(this.client.user).hasPermission('CONNECT');
}
@@ -72,11 +73,25 @@ class VoiceChannel extends GuildChannel {
* .catch(console.error);
*/
setBitrate(bitrate) {
- return this.client.rest.methods.updateChannel(this, { bitrate });
+ return this.edit({ bitrate });
}
/**
- * Attempts to join this Voice Channel
+ * Sets the user limit of the channel
+ * @param {number} userLimit The new user limit
+ * @returns {Promise}
+ * @example
+ * // set the user limit of a voice channel
+ * voiceChannel.setUserLimit(42)
+ * .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`))
+ * .catch(console.error);
+ */
+ setUserLimit(userLimit) {
+ return this.edit({ userLimit });
+ }
+
+ /**
+ * Attempts to join this voice channel
* @returns {Promise}
* @example
* // join a voice channel
@@ -85,6 +100,7 @@ class VoiceChannel extends GuildChannel {
* .catch(console.error);
*/
join() {
+ if (this.client.browser) return Promise.reject(new Error('Voice connections are not available in browsers.'));
return this.client.voice.joinChannel(this);
}
@@ -95,6 +111,7 @@ class VoiceChannel extends GuildChannel {
* voiceChannel.leave();
*/
leave() {
+ if (this.client.browser) return;
const connection = this.client.voice.connections.get(this.guild.id);
if (connection && connection.channel.id === this.id) connection.disconnect();
}
diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js
index 98f44447d..96984ffe0 100644
--- a/src/structures/Webhook.js
+++ b/src/structures/Webhook.js
@@ -2,64 +2,65 @@ const path = require('path');
const escapeMarkdown = require('../util/EscapeMarkdown');
/**
- * Represents a Webhook
+ * Represents a webhook
*/
class Webhook {
constructor(client, dataOrID, token) {
if (client) {
/**
- * The client that instantiated the Channel
+ * The Client that instantiated the Webhook
+ * @name Webhook#client
* @type {Client}
+ * @readonly
*/
- this.client = client;
- Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
+ Object.defineProperty(this, 'client', { value: client });
if (dataOrID) this.setup(dataOrID);
} else {
this.id = dataOrID;
this.token = token;
- this.client = this;
+ Object.defineProperty(this, 'client', { value: this });
}
}
setup(data) {
/**
- * The name of the Webhook
+ * The name of the webhook
* @type {string}
*/
this.name = data.name;
/**
- * The token for the Webhook
+ * The token for the webhook
* @type {string}
*/
this.token = data.token;
/**
- * The avatar for the Webhook
+ * The avatar for the webhook
* @type {string}
*/
this.avatar = data.avatar;
/**
- * The ID of the Webhook
+ * The ID of the webhook
* @type {string}
*/
this.id = data.id;
/**
- * The guild the Webhook belongs to
+ * The guild the webhook belongs to
* @type {string}
*/
this.guildID = data.guild_id;
/**
- * The channel the Webhook belongs to
+ * The channel the webhook belongs to
* @type {string}
*/
this.channelID = data.channel_id;
/**
- * The owner of the Webhook
+ * The owner of the webhook
* @type {User}
*/
if (data.user) this.owner = data.user;
@@ -101,7 +102,7 @@ class Webhook {
* 'color': '#F0F',
* 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png',
* 'footer': 'Powered by sneks',
- * 'ts': new Date().getTime() / 1000
+ * 'ts': Date.now() / 1000
* }]
* }).catch(console.error);
*/
@@ -127,7 +128,7 @@ class Webhook {
/**
* Send a file with this webhook
- * @param {FileResolvable} attachment The file to send
+ * @param {BufferResolvable} attachment The file to send
* @param {string} [fileName="file.jpg"] The name and extension of the file
* @param {StringResolvable} [content] Text message to send with the attachment
* @param {WebhookMessageOptions} [options] The options to provide
@@ -143,14 +144,12 @@ class Webhook {
fileName = 'file.jpg';
}
}
- return new Promise((resolve, reject) => {
- this.client.resolver.resolveFile(attachment).then(file => {
- this.client.rest.methods.sendWebhookMessage(this, content, options, {
- file,
- name: fileName,
- }).then(resolve).catch(reject);
- }).catch(reject);
- });
+ return this.client.resolver.resolveBuffer(attachment).then(file =>
+ this.client.rest.methods.sendWebhookMessage(this, content, options, {
+ file,
+ name: fileName,
+ })
+ );
}
/**
@@ -171,30 +170,26 @@ class Webhook {
}
/**
- * Edit the Webhook.
+ * Edit the webhook.
* @param {string} name The new name for the Webhook
- * @param {FileResolvable} avatar The new avatar for the Webhook.
+ * @param {BufferResolvable} avatar The new avatar for the Webhook.
* @returns {Promise}
*/
edit(name = this.name, avatar) {
- return new Promise((resolve, reject) => {
- if (avatar) {
- this.client.resolver.resolveFile(avatar).then(file => {
- const dataURI = this.client.resolver.resolveBase64(file);
- this.client.rest.methods.editWebhook(this, name, dataURI)
- .then(resolve).catch(reject);
- }).catch(reject);
- } else {
- this.client.rest.methods.editWebhook(this, name)
- .then(data => {
- this.setup(data);
- }).catch(reject);
- }
+ if (avatar) {
+ return this.client.resolver.resolveBuffer(avatar).then(file => {
+ const dataURI = this.client.resolver.resolveBase64(file);
+ return this.client.rest.methods.editWebhook(this, name, dataURI);
+ });
+ }
+ return this.client.rest.methods.editWebhook(this, name).then(data => {
+ this.setup(data);
+ return this;
});
}
/**
- * Delete the Webhook
+ * Delete the webhook
* @returns {Promise}
*/
delete() {
diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js
index 12f31cead..353c0a9cf 100644
--- a/src/structures/interface/TextBasedChannel.js
+++ b/src/structures/interface/TextBasedChannel.js
@@ -2,7 +2,7 @@ const path = require('path');
const Message = require('../Message');
const MessageCollector = require('../MessageCollector');
const Collection = require('../../util/Collection');
-const escapeMarkdown = require('../../util/EscapeMarkdown');
+
/**
* Interface for classes that have text-channel-like features
@@ -11,7 +11,7 @@ const escapeMarkdown = require('../../util/EscapeMarkdown');
class TextBasedChannel {
constructor() {
/**
- * A Collection containing the messages sent to this channel.
+ * A collection containing the messages sent to this channel.
* @type {Collection}
*/
this.messages = new Collection();
@@ -24,16 +24,26 @@ class TextBasedChannel {
}
/**
- * Options that can be passed into sendMessage, sendTTSMessage, sendFile, sendCode, or Message.reply
+ * Options that can be passed into send, sendMessage, sendFile, sendEmbed, sendCode, and Message#reply
* @typedef {Object} MessageOptions
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
* @property {string} [nonce=''] The nonce for the message
+ * @property {Object} [embed] An embed for the message
+ * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details)
* @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here
* should be replaced with plain-text
+ * @property {FileOptions|string} [file] A file to send with the message
+ * @property {string|boolean} [code] Language for optional codeblock formatting to apply
* @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if
* it exceeds the character limit. If an object is provided, these are the options for splitting the message.
*/
+ /**
+ * @typedef {Object} FileOptions
+ * @property {BufferResolvable} attachment
+ * @property {string} [name='file.jpg']
+ */
+
/**
* Options for splitting a message
* @typedef {Object} SplitOptions
@@ -45,8 +55,47 @@ class TextBasedChannel {
/**
* Send a message to this channel
- * @param {StringResolvable} content The content to send
- * @param {MessageOptions} [options={}] The options to provide
+ * @param {StringResolvable} [content] Text for the message
+ * @param {MessageOptions} [options={}] Options for the message
+ * @returns {Promise}
+ * @example
+ * // send a message
+ * channel.send('hello!')
+ * .then(message => console.log(`Sent message: ${message.content}`))
+ * .catch(console.error);
+ */
+ send(content, options) {
+ if (!options && typeof content === 'object' && !(content instanceof Array)) {
+ options = content;
+ content = '';
+ } else if (!options) {
+ options = {};
+ }
+ if (options.file) {
+ if (typeof options.file === 'string') options.file = { attachment: options.file };
+ if (!options.file.name) {
+ if (typeof options.file.attachment === 'string') {
+ options.file.name = path.basename(options.file.attachment);
+ } else if (options.file.attachment && options.file.attachment.path) {
+ options.file.name = path.basename(options.file.attachment.path);
+ } else {
+ options.file.name = 'file.jpg';
+ }
+ }
+ return this.client.resolver.resolveBuffer(options.file.attachment).then(file =>
+ this.client.rest.methods.sendMessage(this, content, options, {
+ file,
+ name: options.file.name,
+ })
+ );
+ }
+ return this.client.rest.methods.sendMessage(this, content, options);
+ }
+
+ /**
+ * Send a message to this channel
+ * @param {StringResolvable} content Text for the message
+ * @param {MessageOptions} [options={}] Options for the message
* @returns {Promise}
* @example
* // send a message
@@ -54,75 +103,54 @@ class TextBasedChannel {
* .then(message => console.log(`Sent message: ${message.content}`))
* .catch(console.error);
*/
- sendMessage(content, options = {}) {
- return this.client.rest.methods.sendMessage(this, content, options);
+ sendMessage(content, options) {
+ return this.send(content, options);
}
/**
- * Send a text-to-speech message to this channel
- * @param {StringResolvable} content The content to send
- * @param {MessageOptions} [options={}] The options to provide
- * @returns {Promise}
- * @example
- * // send a TTS message
- * channel.sendTTSMessage('hello!')
- * .then(message => console.log(`Sent tts message: ${message.content}`))
- * .catch(console.error);
+ * Send an embed to this channel
+ * @param {RichEmbed|Object} embed Embed for the message
+ * @param {string} [content] Text for the message
+ * @param {MessageOptions} [options] Options for the message
+ * @returns {Promise}
*/
- sendTTSMessage(content, options = {}) {
- Object.assign(options, { tts: true });
- return this.client.rest.methods.sendMessage(this, content, options);
+ sendEmbed(embed, content, options) {
+ if (!options && typeof content === 'object') {
+ options = content;
+ content = '';
+ } else if (!options) {
+ options = {};
+ }
+ return this.send(content, Object.assign(options, { embed }));
}
/**
* Send a file to this channel
- * @param {FileResolvable} attachment The file to send
- * @param {string} [fileName="file.jpg"] The name and extension of the file
- * @param {StringResolvable} [content] Text message to send with the attachment
- * @param {MessageOptions} [options] The options to provide
+ * @param {BufferResolvable} attachment File to send
+ * @param {string} [name='file.jpg'] Name and extension of the file
+ * @param {StringResolvable} [content] Text for the message
+ * @param {MessageOptions} [options] Options for the message
* @returns {Promise}
*/
- sendFile(attachment, fileName, content, options = {}) {
- if (!fileName) {
- if (typeof attachment === 'string') {
- fileName = path.basename(attachment);
- } else if (attachment && attachment.path) {
- fileName = path.basename(attachment.path);
- } else {
- fileName = 'file.jpg';
- }
- }
- return new Promise((resolve, reject) => {
- this.client.resolver.resolveFile(attachment).then(file => {
- this.client.rest.methods.sendMessage(this, content, options, {
- file,
- name: fileName,
- }).then(resolve).catch(reject);
- }).catch(reject);
- });
+ sendFile(attachment, name, content, options = {}) {
+ return this.send(content, Object.assign(options, { file: { attachment, name } }));
}
/**
* Send a code block to this channel
* @param {string} lang Language for the code block
* @param {StringResolvable} content Content of the code block
- * @param {MessageOptions} options The options to provide
+ * @param {MessageOptions} [options] Options for the message
* @returns {Promise}
*/
sendCode(lang, content, options = {}) {
- if (options.split) {
- if (typeof options.split !== 'object') options.split = {};
- if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`;
- if (!options.split.append) options.split.append = '\n```';
- }
- content = escapeMarkdown(this.client.resolver.resolveString(content), true);
- return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options);
+ return this.send(content, Object.assign(options, { code: lang }));
}
/**
* Gets a single message from this channel, regardless of it being cached or not.
- * Only OAuth bot accounts can use this method.
- * @param {string} messageID The ID of the message to get
+ * This is only available when using a bot account.
+ * @param {string} messageID ID of the message to get
* @returns {Promise}
* @example
* // get message
@@ -131,14 +159,10 @@ class TextBasedChannel {
* .catch(console.error);
*/
fetchMessage(messageID) {
- return new Promise((resolve, reject) => {
- this.client.rest.methods.getChannelMessage(this, messageID).then(data => {
- let msg = data;
- if (!(msg instanceof Message)) msg = new Message(this, data, this.client);
-
- this._cacheMessage(msg);
- resolve(msg);
- }).catch(reject);
+ return this.client.rest.methods.getChannelMessage(this, messageID).then(data => {
+ const msg = data instanceof Message ? data : new Message(this, data, this.client);
+ this._cacheMessage(msg);
+ return msg;
});
}
@@ -153,8 +177,8 @@ class TextBasedChannel {
*/
/**
- * Gets the past messages sent in this channel. Resolves with a Collection mapping message ID's to Message objects.
- * @param {ChannelLogsQueryOptions} [options={}] The query parameters to pass in
+ * Gets the past messages sent in this channel. Resolves with a collection mapping message ID's to Message objects.
+ * @param {ChannelLogsQueryOptions} [options={}] Query parameters to pass in
* @returns {Promise>}
* @example
* // get messages
@@ -163,34 +187,30 @@ class TextBasedChannel {
* .catch(console.error);
*/
fetchMessages(options = {}) {
- return new Promise((resolve, reject) => {
- this.client.rest.methods.getChannelMessages(this, options).then(data => {
- const messages = new Collection();
- for (const message of data) {
- const msg = new Message(this, message, this.client);
- messages.set(message.id, msg);
- this._cacheMessage(msg);
- }
- resolve(messages);
- }).catch(reject);
+ return this.client.rest.methods.getChannelMessages(this, options).then(data => {
+ const messages = new Collection();
+ for (const message of data) {
+ const msg = new Message(this, message, this.client);
+ messages.set(message.id, msg);
+ this._cacheMessage(msg);
+ }
+ return messages;
});
}
/**
- * Fetches the pinned messages of this Channel and returns a Collection of them.
+ * Fetches the pinned messages of this channel and returns a collection of them.
* @returns {Promise>}
*/
fetchPinnedMessages() {
- return new Promise((resolve, reject) => {
- this.client.rest.methods.getChannelPinnedMessages(this).then(data => {
- const messages = new Collection();
- for (const message of data) {
- const msg = new Message(this, message, this.client);
- messages.set(message.id, msg);
- this._cacheMessage(msg);
- }
- resolve(messages);
- }).catch(reject);
+ return this.client.rest.methods.getChannelPinnedMessages(this).then(data => {
+ const messages = new Collection();
+ for (const message of data) {
+ const msg = new Message(this, message, this.client);
+ messages.set(message.id, msg);
+ this._cacheMessage(msg);
+ }
+ return messages;
});
}
@@ -220,7 +240,7 @@ class TextBasedChannel {
/**
* Stops the typing indicator in the channel.
* The indicator will only stop if this is called as many times as startTyping().
- * It can take a few seconds for the Client User to stop typing.
+ * It can take a few seconds for the client user to stop typing.
* @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop
* @example
* // stop typing in a channel
@@ -284,7 +304,7 @@ class TextBasedChannel {
*/
/**
- * Similar to createCollector but in Promise form. Resolves with a Collection of messages that pass the specified
+ * Similar to createCollector but in promise form. Resolves with a collection of messages that pass the specified
* filter.
* @param {CollectorFilterFunction} filter The filter function to use
* @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector
@@ -312,21 +332,17 @@ class TextBasedChannel {
/**
* Bulk delete given messages.
- * Only OAuth Bot accounts may use this method.
+ * This is only available when using a bot account.
* @param {Collection|Message[]|number} messages Messages to delete, or number of messages to delete
* @returns {Promise>} Deleted messages
*/
bulkDelete(messages) {
- return new Promise((resolve, reject) => {
- if (!isNaN(messages)) {
- this.fetchMessages({ limit: messages }).then(msgs => resolve(this.bulkDelete(msgs)));
- } else if (messages instanceof Array || messages instanceof Collection) {
- const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id);
- resolve(this.client.rest.methods.bulkDeleteMessages(this, messageIDs));
- } else {
- reject(new TypeError('Messages must be an Array, Collection, or number.'));
- }
- });
+ if (!isNaN(messages)) return this.fetchMessages({ limit: messages }).then(msgs => this.bulkDelete(msgs));
+ if (messages instanceof Array || messages instanceof Collection) {
+ const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id);
+ return this.client.rest.methods.bulkDeleteMessages(this, messageIDs);
+ }
+ throw new TypeError('The messages must be an Array, Collection, or number.');
}
_cacheMessage(message) {
@@ -339,23 +355,23 @@ class TextBasedChannel {
}
exports.applyToClass = (structure, full = false) => {
- const props = ['sendMessage', 'sendTTSMessage', 'sendFile', 'sendCode'];
+ const props = ['send', 'sendMessage', 'sendEmbed', 'sendFile', 'sendCode'];
if (full) {
- props.push('_cacheMessage');
- props.push('fetchMessages');
- props.push('fetchMessage');
- props.push('bulkDelete');
- props.push('startTyping');
- props.push('stopTyping');
- props.push('typing');
- props.push('typingCount');
- props.push('fetchPinnedMessages');
- props.push('createCollector');
- props.push('awaitMessages');
+ props.push(
+ '_cacheMessage',
+ 'fetchMessages',
+ 'fetchMessage',
+ 'bulkDelete',
+ 'startTyping',
+ 'stopTyping',
+ 'typing',
+ 'typingCount',
+ 'fetchPinnedMessages',
+ 'createCollector',
+ 'awaitMessages'
+ );
+ }
+ for (const prop of props) {
+ Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop));
}
- for (const prop of props) applyProp(structure, prop);
};
-
-function applyProp(structure, prop) {
- Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop));
-}
diff --git a/src/util/Collection.js b/src/util/Collection.js
index 53299023a..bafe710b6 100644
--- a/src/util/Collection.js
+++ b/src/util/Collection.js
@@ -1,33 +1,44 @@
/**
- * A utility class to help make it easier to access the data stores
+ * A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has
+ * an ID, for significantly improved performance and ease-of-use.
* @extends {Map}
*/
class Collection extends Map {
constructor(iterable) {
super(iterable);
+
+ /**
+ * Cached array for the `array()` method - will be reset to `null` whenever `set()` or `delete()` are called.
+ * @type {?Array}
+ * @private
+ */
this._array = null;
+
+ /**
+ * Cached array for the `keyArray()` method - will be reset to `null` whenever `set()` or `delete()` are called.
+ * @type {?Array}
+ * @private
+ */
this._keyArray = null;
}
set(key, val) {
- super.set(key, val);
this._array = null;
this._keyArray = null;
+ return super.set(key, val);
}
delete(key) {
- super.delete(key);
this._array = null;
this._keyArray = null;
+ return super.delete(key);
}
/**
* Creates an ordered array of the values of this collection, and caches it internally. The array will only be
- * reconstructed if an item is added to or removed from the collection, or if you add/remove elements on the array.
+ * reconstructed if an item is added to or removed from the collection, or if you change the length of the array
+ * itself. If you don't want this caching behaviour, use `Array.from(collection.values())` instead.
* @returns {Array}
- * @example
- * // identical to:
- * Array.from(collection.values());
*/
array() {
if (!this._array || this._array.length !== this.size) this._array = Array.from(this.values());
@@ -36,11 +47,9 @@ class Collection extends Map {
/**
* Creates an ordered array of the keys of this collection, and caches it internally. The array will only be
- * reconstructed if an item is added to or removed from the collection, or if you add/remove elements on the array.
+ * reconstructed if an item is added to or removed from the collection, or if you change the length of the array
+ * itself. If you don't want this caching behaviour, use `Array.from(collection.keys())` instead.
* @returns {Array}
- * @example
- * // identical to:
- * Array.from(collection.keys());
*/
keyArray() {
if (!this._keyArray || this._keyArray.length !== this.size) this._keyArray = Array.from(this.keys());
@@ -48,7 +57,7 @@ class Collection extends Map {
}
/**
- * Returns the first item in this collection.
+ * Obtains the first item in this collection.
* @returns {*}
*/
first() {
@@ -56,7 +65,7 @@ class Collection extends Map {
}
/**
- * Returns the first key in this collection.
+ * Obtains the first key in this collection.
* @returns {*}
*/
firstKey() {
@@ -64,8 +73,8 @@ class Collection extends Map {
}
/**
- * Returns the last item in this collection. This is a relatively slow operation,
- * since an array copy of the values must be made to find the last element.
+ * Obtains the last item in this collection. This relies on the `array()` method, and thus the caching mechanism
+ * applies here as well.
* @returns {*}
*/
last() {
@@ -74,8 +83,8 @@ class Collection extends Map {
}
/**
- * Returns the last key in this collection. This is a relatively slow operation,
- * since an array copy of the keys must be made to find the last element.
+ * Obtains the last key in this collection. This relies on the `keyArray()` method, and thus the caching mechanism
+ * applies here as well.
* @returns {*}
*/
lastKey() {
@@ -84,8 +93,8 @@ class Collection extends Map {
}
/**
- * Returns a random item from this collection. This is a relatively slow operation,
- * since an array copy of the values must be made to find a random element.
+ * Obtains a random item from this collection. This relies on the `array()` method, and thus the caching mechanism
+ * applies here as well.
* @returns {*}
*/
random() {
@@ -94,8 +103,8 @@ class Collection extends Map {
}
/**
- * Returns a random key from this collection. This is a relatively slow operation,
- * since an array copy of the keys must be made to find a random element.
+ * Obtains a random key from this collection. This relies on the `keyArray()` method, and thus the caching mechanism
+ * applies here as well.
* @returns {*}
*/
randomKey() {
@@ -104,10 +113,11 @@ class Collection extends Map {
}
/**
- * Returns an array of items where `item[prop] === value` of the collection
+ * Searches for all items where their specified property's value is identical to the given value
+ * (`item[prop] === value`).
* @param {string} prop The property to test against
* @param {*} value The expected value
- * @returns {array}
+ * @returns {Array}
* @example
* collection.findAll('username', 'Bob');
*/
@@ -122,10 +132,12 @@ class Collection extends Map {
}
/**
- * Returns a single item where `item[prop] === value`, or the given function returns `true`.
- * In the latter case, this is identical to
+ * Searches for a single item where its specified property's value is identical to the given value
+ * (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to
* [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find).
- * @param {string|function} propOrFn The property to test against, or the function to test with
+ * Do not use this to obtain an item by its ID. Instead, use `collection.get(id)`. See
+ * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details.
+ * @param {string|Function} propOrFn The property to test against, or the function to test with
* @param {*} [value] The expected value - only applicable and required if using a property for the first argument
* @returns {*}
* @example
@@ -136,6 +148,7 @@ class Collection extends Map {
find(propOrFn, value) {
if (typeof propOrFn === 'string') {
if (typeof value === 'undefined') throw new Error('Value must be specified.');
+ if (propOrFn === 'id') throw new RangeError('Don\'t use .find() with IDs. Instead, use .get(id).');
for (const item of this.values()) {
if (item[propOrFn] === value) return item;
}
@@ -152,10 +165,10 @@ class Collection extends Map {
/* eslint-disable max-len */
/**
- * Returns the key of the item where `item[prop] === value`, or the given function returns `true`.
- * In the latter case, this is identical to
+ * Searches for the key of a single item where its specified property's value is identical to the given value
+ * (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to
* [Array.findIndex()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex).
- * @param {string|function} propOrFn The property to test against, or the function to test with
+ * @param {string|Function} propOrFn The property to test against, or the function to test with
* @param {*} [value] The expected value - only applicable and required if using a property for the first argument
* @returns {*}
* @example
@@ -182,7 +195,10 @@ class Collection extends Map {
}
/**
- * Returns true if the collection has an item where `item[prop] === value`
+ * Searches for the existence of a single item where its specified property's value is identical to the given value
+ * (`item[prop] === value`).
+ * Do not use this to check for an item by its ID. Instead, use `collection.has(id)`. See
+ * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) for details.
* @param {string} prop The property to test against
* @param {*} value The expected value
* @returns {boolean}
@@ -192,6 +208,7 @@ class Collection extends Map {
* }
*/
exists(prop, value) {
+ if (prop === 'id') throw new RangeError('Don\'t use .exists() with IDs. Instead, use .has(id).');
return Boolean(this.find(prop, value));
}
@@ -199,7 +216,7 @@ class Collection extends Map {
* Identical to
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter),
* but returns a Collection instead of an Array.
- * @param {function} fn Function used to test (should return a boolean)
+ * @param {Function} fn Function used to test (should return a boolean)
* @param {Object} [thisArg] Value to use as `this` when executing function
* @returns {Collection}
*/
@@ -215,9 +232,9 @@ class Collection extends Map {
/**
* Identical to
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).
- * @param {function} fn Function used to test (should return a boolean)
+ * @param {Function} fn Function used to test (should return a boolean)
* @param {Object} [thisArg] Value to use as `this` when executing function
- * @returns {Collection}
+ * @returns {Array}
*/
filterArray(fn, thisArg) {
if (thisArg) fn = fn.bind(thisArg);
@@ -231,9 +248,9 @@ class Collection extends Map {
/**
* Identical to
* [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
- * @param {function} fn Function that produces an element of the new array, taking three arguments
+ * @param {Function} fn Function that produces an element of the new array, taking three arguments
* @param {*} [thisArg] Value to use as `this` when executing function
- * @returns {array}
+ * @returns {Array}
*/
map(fn, thisArg) {
if (thisArg) fn = fn.bind(thisArg);
@@ -246,7 +263,7 @@ class Collection extends Map {
/**
* Identical to
* [Array.some()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some).
- * @param {function} fn Function used to test (should return a boolean)
+ * @param {Function} fn Function used to test (should return a boolean)
* @param {Object} [thisArg] Value to use as `this` when executing function
* @returns {boolean}
*/
@@ -261,7 +278,7 @@ class Collection extends Map {
/**
* Identical to
* [Array.every()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every).
- * @param {function} fn Function used to test (should return a boolean)
+ * @param {Function} fn Function used to test (should return a boolean)
* @param {Object} [thisArg] Value to use as `this` when executing function
* @returns {boolean}
*/
@@ -276,19 +293,33 @@ class Collection extends Map {
/**
* Identical to
* [Array.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce).
- * @param {function} fn Function used to reduce
- * @param {*} [startVal] The starting value
+ * @param {Function} fn Function used to reduce, taking four arguments; `accumulator`, `currentValue`, `currentKey`,
+ * and `collection`
+ * @param {*} [initialValue] Starting value for the accumulator
* @returns {*}
*/
- reduce(fn, startVal) {
- let currentVal = startVal;
- for (const [key, val] of this) currentVal = fn(currentVal, val, key, this);
- return currentVal;
+ reduce(fn, initialValue) {
+ let accumulator;
+ if (typeof initialValue !== 'undefined') {
+ accumulator = initialValue;
+ for (const [key, val] of this) accumulator = fn(accumulator, val, key, this);
+ } else {
+ let first = true;
+ for (const [key, val] of this) {
+ if (first) {
+ accumulator = val;
+ first = false;
+ continue;
+ }
+ accumulator = fn(accumulator, val, key, this);
+ }
+ }
+ return accumulator;
}
/**
* Combines this collection with others into a new collection. None of the source collections are modified.
- * @param {Collection} collections Collections to merge
+ * @param {...Collection} collections Collections to merge
* @returns {Collection}
* @example const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl);
*/
@@ -302,8 +333,7 @@ class Collection extends Map {
}
/**
- * If the items in this collection have a delete method (e.g. messages), invoke
- * the delete method. Returns an array of promises
+ * Calls the `delete()` method on all items that have it.
* @returns {Promise[]}
*/
deleteAll() {
@@ -313,6 +343,23 @@ class Collection extends Map {
}
return returns;
}
+
+ /**
+ * Checks if this collection shares identical key-value pairings with another.
+ * This is different to checking for equality using equal-signs, because
+ * the collections may be different objects, but contain the same data.
+ * @param {Collection} collection Collection to compare with
+ * @returns {boolean} Whether the collections have identical contents
+ */
+ equals(collection) {
+ if (!collection) return false;
+ if (this === collection) return true;
+ if (this.size !== collection.size) return false;
+ return !this.find((value, key) => {
+ const testVal = collection.get(key);
+ return testVal !== value || (testVal === undefined && !collection.has(key));
+ });
+ }
}
module.exports = Collection;
diff --git a/src/util/Constants.js b/src/util/Constants.js
index 60575cd61..cee2192f1 100644
--- a/src/util/Constants.js
+++ b/src/util/Constants.js
@@ -8,8 +8,8 @@ exports.Package = require('../../package.json');
* @property {number} [shardId=0] The ID of this shard
* @property {number} [shardCount=0] The number of shards
* @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel
- * @property {boolean} [sync=false] Whether to periodically sync guilds
- * (-1 for unlimited - don't do this without message sweeping, otherwise memory usage will climb indefinitely)
+ * (-1 or Infinity for unlimited - don't do this without message sweeping, otherwise memory usage will climb
+ * indefinitely)
* @property {number} [messageCacheLifetime=0] How long until a message should be uncached by the message sweeping
* (in seconds, 0 for forever)
* @property {number} [messageSweepInterval=0] How frequently to remove messages from the cache that are older than
@@ -17,11 +17,15 @@ exports.Package = require('../../package.json');
* @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as
* upon joining a guild
* @property {boolean} [disableEveryone=false] Default value for MessageOptions.disableEveryone
+ * @property {boolean} [sync=false] Whether to periodically sync guilds (for userbots)
* @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their
* corresponding websocket events
- * @property {string[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be
- * processed. Disabling useless events such as 'TYPING_START' can result in significant performance increases on
- * large-scale bots.
+ * @property {number} [restTimeOffset=500] The extra time in millseconds to wait before continuing to make REST
+ * requests (higher values will reduce rate-limiting errors on bad connections)
+ * @property {WSEventType[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be
+ * processed, potentially resulting in performance improvements for larger bots. Only disable events you are
+ * 100% certain you don't need, as many are important, but not obviously so. The safest one to disable with the
+ * most impact is typically `TYPING_START`.
* @property {WebsocketOptions} [ws] Options for the websocket
*/
exports.DefaultOptions = {
@@ -33,19 +37,21 @@ exports.DefaultOptions = {
messageSweepInterval: 0,
fetchAllMembers: false,
disableEveryone: false,
+ sync: false,
restWsBridgeTimeout: 5000,
disabledEvents: [],
- sync: false,
+ restTimeOffset: 500,
/**
* Websocket options. These are left as snake_case to match the API.
* @typedef {Object} WebsocketOptions
* @property {number} [large_threshold=250] Number of members in a guild to be considered large
- * @property {boolean} [compress=true] Whether to compress data sent on the connection
+ * @property {boolean} [compress=true] Whether to compress data sent on the connection.
+ * Defaults to `false` for browsers.
*/
ws: {
large_threshold: 250,
- compress: true,
+ compress: typeof window === 'undefined',
properties: {
$os: process ? process.platform : 'discord.js',
$browser: 'discord.js',
@@ -58,17 +64,20 @@ exports.DefaultOptions = {
exports.Errors = {
NO_TOKEN: 'Request to use token, but token was unavailable to the client.',
- NO_BOT_ACCOUNT: 'You ideally should be using a bot account!',
+ NO_BOT_ACCOUNT: 'Only bot accounts are able to make use of this feature.',
+ NO_USER_ACCOUNT: 'Only user accounts are able to make use of this feature.',
BAD_WS_MESSAGE: 'A bad message was received from the websocket; either bad compression, or not JSON.',
TOOK_TOO_LONG: 'Something took too long to do.',
NOT_A_PERMISSION: 'Invalid permission string or number.',
INVALID_RATE_LIMIT_METHOD: 'Unknown rate limiting method.',
BAD_LOGIN: 'Incorrect login details were provided.',
INVALID_SHARD: 'Invalid shard settings were provided.',
+ INVALID_TOKEN: 'An invalid token was provided.',
};
const PROTOCOL_VERSION = exports.PROTOCOL_VERSION = 6;
-const API = exports.API = `https://discordapp.com/api/v${PROTOCOL_VERSION}`;
+const HOST = exports.HOST = `https://discordapp.com`;
+const API = exports.API = `${HOST}/api/v${PROTOCOL_VERSION}`;
const Endpoints = exports.Endpoints = {
// general
login: `${API}/auth/login`,
@@ -77,21 +86,29 @@ const Endpoints = exports.Endpoints = {
botGateway: `${API}/gateway/bot`,
invite: (id) => `${API}/invite/${id}`,
inviteLink: (id) => `https://discord.gg/${id}`,
+ assets: (asset) => `${HOST}/assets/${asset}`,
CDN: 'https://cdn.discordapp.com',
// users
user: (userID) => `${API}/users/${userID}`,
userChannels: (userID) => `${Endpoints.user(userID)}/channels`,
userProfile: (userID) => `${Endpoints.user(userID)}/profile`,
- avatar: (userID, avatar) => userID === '1' ? avatar : `${Endpoints.user(userID)}/avatars/${avatar}.jpg`,
+ avatar: (userID, avatar) => {
+ if (userID === '1') return avatar;
+ return `${Endpoints.CDN}/avatars/${userID}/${avatar}.${avatar.startsWith('a_') ? 'gif' : 'jpg'}?size=1024`;
+ },
me: `${API}/users/@me`,
meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`,
+ meMentions: (limit, roles, everyone, guildID) =>
+ `users/@me/mentions?limit=${limit}&roles=${roles}&everyone=${everyone}${guildID ? `&guild_id=${guildID}` : ''}`,
relationships: (userID) => `${Endpoints.user(userID)}/relationships`,
+ note: (userID) => `${Endpoints.me}/notes/${userID}`,
// guilds
guilds: `${API}/guilds`,
guild: (guildID) => `${Endpoints.guilds}/${guildID}`,
- guildIcon: (guildID, hash) => `${Endpoints.guild(guildID)}/icons/${hash}.jpg`,
+ guildIcon: (guildID, hash) => `${Endpoints.CDN}/icons/${guildID}/${hash}.jpg`,
+ guildSplash: (guildID, hash) => `${Endpoints.CDN}/splashes/${guildID}/${hash}.jpg`,
guildPrune: (guildID) => `${Endpoints.guild(guildID)}/prune`,
guildEmbed: (guildID) => `${Endpoints.guild(guildID)}/embed`,
guildInvites: (guildID) => `${Endpoints.guild(guildID)}/invites`,
@@ -101,7 +118,8 @@ const Endpoints = exports.Endpoints = {
guildIntegrations: (guildID) => `${Endpoints.guild(guildID)}/integrations`,
guildMembers: (guildID) => `${Endpoints.guild(guildID)}/members`,
guildMember: (guildID, memberID) => `${Endpoints.guildMembers(guildID)}/${memberID}`,
- stupidInconsistentGuildEndpoint: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`,
+ guildMemberRole: (guildID, memberID, roleID) => `${Endpoints.guildMember(guildID, memberID)}/roles/${roleID}`,
+ guildMemberNickname: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`,
guildChannels: (guildID) => `${Endpoints.guild(guildID)}/channels`,
guildEmojis: (guildID) => `${Endpoints.guild(guildID)}/emojis`,
@@ -115,8 +133,26 @@ const Endpoints = exports.Endpoints = {
channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`,
channelWebhooks: (channelID) => `${Endpoints.channel(channelID)}/webhooks`,
+ // message reactions
+ messageReactions: (channelID, messageID) => `${Endpoints.channelMessage(channelID, messageID)}/reactions`,
+ messageReaction:
+ (channel, msg, emoji, limit) =>
+ `${Endpoints.messageReactions(channel, msg)}/${emoji}` +
+ `${limit ? `?limit=${limit}` : ''}`,
+ selfMessageReaction: (channel, msg, emoji, limit) =>
+ `${Endpoints.messageReaction(channel, msg, emoji, limit)}/@me`,
+ userMessageReaction: (channel, msg, emoji, limit, id) =>
+ `${Endpoints.messageReaction(channel, msg, emoji, limit)}/${id}`,
+
// webhooks
webhook: (webhookID, token) => `${API}/webhooks/${webhookID}${token ? `/${token}` : ''}`,
+
+ // oauth
+ myApplication: `${API}/oauth2/applications/@me`,
+ getApp: (id) => `${API}/oauth2/authorize?client_id=${id}`,
+
+ // emoji
+ emoji: (emojiID) => `${Endpoints.CDN}/emojis/${emojiID}.png`,
};
exports.Status = {
@@ -125,6 +161,7 @@ exports.Status = {
RECONNECTING: 2,
IDLE: 3,
NEARLY: 4,
+ DISCONNECTED: 5,
};
exports.ChannelTypes = {
@@ -174,9 +211,9 @@ exports.Events = {
GUILD_ROLE_CREATE: 'roleCreate',
GUILD_ROLE_DELETE: 'roleDelete',
GUILD_ROLE_UPDATE: 'roleUpdate',
- GUILD_EMOJI_CREATE: 'guildEmojiCreate',
- GUILD_EMOJI_DELETE: 'guildEmojiDelete',
- GUILD_EMOJI_UPDATE: 'guildEmojiUpdate',
+ GUILD_EMOJI_CREATE: 'emojiCreate',
+ GUILD_EMOJI_DELETE: 'emojiDelete',
+ GUILD_EMOJI_UPDATE: 'emojiUpdate',
GUILD_BAN_ADD: 'guildBanAdd',
GUILD_BAN_REMOVE: 'guildBanRemove',
CHANNEL_CREATE: 'channelCreate',
@@ -187,7 +224,11 @@ exports.Events = {
MESSAGE_DELETE: 'messageDelete',
MESSAGE_UPDATE: 'messageUpdate',
MESSAGE_BULK_DELETE: 'messageDeleteBulk',
+ MESSAGE_REACTION_ADD: 'messageReactionAdd',
+ MESSAGE_REACTION_REMOVE: 'messageReactionRemove',
+ MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll',
USER_UPDATE: 'userUpdate',
+ USER_NOTE_UPDATE: 'userNoteUpdate',
PRESENCE_UPDATE: 'presenceUpdate',
VOICE_STATE_UPDATE: 'voiceStateUpdate',
TYPING_START: 'typingStart',
@@ -199,6 +240,43 @@ exports.Events = {
DEBUG: 'debug',
};
+/**
+ * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events:
+ * - READY
+ * - GUILD_SYNC
+ * - GUILD_CREATE
+ * - GUILD_DELETE
+ * - GUILD_UPDATE
+ * - GUILD_MEMBER_ADD
+ * - GUILD_MEMBER_REMOVE
+ * - GUILD_MEMBER_UPDATE
+ * - GUILD_MEMBERS_CHUNK
+ * - GUILD_ROLE_CREATE
+ * - GUILD_ROLE_DELETE
+ * - GUILD_ROLE_UPDATE
+ * - GUILD_BAN_ADD
+ * - GUILD_BAN_REMOVE
+ * - CHANNEL_CREATE
+ * - CHANNEL_DELETE
+ * - CHANNEL_UPDATE
+ * - CHANNEL_PINS_UPDATE
+ * - MESSAGE_CREATE
+ * - MESSAGE_DELETE
+ * - MESSAGE_UPDATE
+ * - MESSAGE_DELETE_BULK
+ * - MESSAGE_REACTION_ADD
+ * - MESSAGE_REACTION_REMOVE
+ * - MESSAGE_REACTION_REMOVE_ALL
+ * - USER_UPDATE
+ * - USER_NOTE_UPDATE
+ * - PRESENCE_UPDATE
+ * - VOICE_STATE_UPDATE
+ * - TYPING_START
+ * - VOICE_SERVER_UPDATE
+ * - RELATIONSHIP_ADD
+ * - RELATIONSHIP_REMOVE
+ * @typedef {string} WSEventType
+ */
exports.WSEvents = {
READY: 'READY',
GUILD_SYNC: 'GUILD_SYNC',
@@ -214,6 +292,7 @@ exports.WSEvents = {
GUILD_ROLE_UPDATE: 'GUILD_ROLE_UPDATE',
GUILD_BAN_ADD: 'GUILD_BAN_ADD',
GUILD_BAN_REMOVE: 'GUILD_BAN_REMOVE',
+ GUILD_EMOJIS_UPDATE: 'GUILD_EMOJIS_UPDATE',
CHANNEL_CREATE: 'CHANNEL_CREATE',
CHANNEL_DELETE: 'CHANNEL_DELETE',
CHANNEL_UPDATE: 'CHANNEL_UPDATE',
@@ -222,12 +301,14 @@ exports.WSEvents = {
MESSAGE_DELETE: 'MESSAGE_DELETE',
MESSAGE_UPDATE: 'MESSAGE_UPDATE',
MESSAGE_DELETE_BULK: 'MESSAGE_DELETE_BULK',
+ MESSAGE_REACTION_ADD: 'MESSAGE_REACTION_ADD',
+ MESSAGE_REACTION_REMOVE: 'MESSAGE_REACTION_REMOVE',
+ MESSAGE_REACTION_REMOVE_ALL: 'MESSAGE_REACTION_REMOVE_ALL',
USER_UPDATE: 'USER_UPDATE',
+ USER_NOTE_UPDATE: 'USER_NOTE_UPDATE',
PRESENCE_UPDATE: 'PRESENCE_UPDATE',
VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE',
TYPING_START: 'TYPING_START',
- FRIEND_ADD: 'RELATIONSHIP_ADD',
- FRIEND_REMOVE: 'RELATIONSHIP_REMOVE',
VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE',
RELATIONSHIP_ADD: 'RELATIONSHIP_ADD',
RELATIONSHIP_REMOVE: 'RELATIONSHIP_REMOVE',
@@ -243,6 +324,14 @@ exports.MessageTypes = {
6: 'PINS_ADD',
};
+exports.DefaultAvatars = {
+ BLURPLE: '6debd47ed13483642cf09e832ed0bc1b',
+ GREY: '322c936a8c8be1b803cd94861bdfa868',
+ GREEN: 'dd4dbc0016779df1378e7812eabaa04d',
+ ORANGE: '0e291f67c9274a1abdddeb3fd919cbaa',
+ RED: '1cbd08c76f8af6dddce02c5138971129',
+};
+
const PermissionFlags = exports.PermissionFlags = {
CREATE_INSTANT_INVITE: 1 << 0,
KICK_MEMBERS: 1 << 1,
@@ -250,6 +339,7 @@ const PermissionFlags = exports.PermissionFlags = {
ADMINISTRATOR: 1 << 3,
MANAGE_CHANNELS: 1 << 4,
MANAGE_GUILD: 1 << 5,
+ ADD_REACTIONS: 1 << 6,
READ_MESSAGES: 1 << 10,
SEND_MESSAGES: 1 << 11,
diff --git a/src/util/ConvertArrayBuffer.js b/src/util/ConvertArrayBuffer.js
new file mode 100644
index 000000000..26b1cc8b7
--- /dev/null
+++ b/src/util/ConvertArrayBuffer.js
@@ -0,0 +1,18 @@
+function arrayBufferToBuffer(ab) {
+ const buffer = new Buffer(ab.byteLength);
+ const view = new Uint8Array(ab);
+ for (var i = 0; i < buffer.length; ++i) buffer[i] = view[i];
+ return buffer;
+}
+
+function str2ab(str) {
+ const buffer = new ArrayBuffer(str.length * 2);
+ const view = new Uint16Array(buffer);
+ for (var i = 0, strLen = str.length; i < strLen; i++) view[i] = str.charCodeAt(i);
+ return buffer;
+}
+
+module.exports = function convertArrayBuffer(x) {
+ if (typeof x === 'string') x = str2ab(x);
+ return arrayBufferToBuffer(x);
+};
diff --git a/src/util/ParseEmoji.js b/src/util/ParseEmoji.js
new file mode 100644
index 000000000..d9f7b2212
--- /dev/null
+++ b/src/util/ParseEmoji.js
@@ -0,0 +1,14 @@
+module.exports = function parseEmoji(text) {
+ if (text.includes('%')) {
+ text = decodeURIComponent(text);
+ }
+ if (text.includes(':')) {
+ const [name, id] = text.split(':');
+ return { name, id };
+ } else {
+ return {
+ name: text,
+ id: null,
+ };
+ }
+};
diff --git a/test/random.js b/test/random.js
index ee5d95427..cc65fc759 100644
--- a/test/random.js
+++ b/test/random.js
@@ -20,7 +20,11 @@ client.on('userUpdate', (o, n) => {
console.log(o.username, n.username);
});
-client.on('guildMemberAdd', (g, m) => console.log(`${m.user.username} joined ${g.name}`));
+client.on('guildEmojiCreate', e => console.log('create!!', e.name));
+client.on('guildEmojiDelete', e => console.log('delete!!', e.name));
+client.on('guildEmojiUpdate', (o, n) => console.log('update!!', o.name, n.name));
+
+client.on('guildMemberAdd', m => console.log(`${m.user.username} joined ${m.guild.name}`));
client.on('channelCreate', channel => {
console.log(`made ${channel.name}`);
@@ -117,10 +121,15 @@ client.on('message', message => {
if (message.content === 'ratelimittest') {
let i = 1;
+ const start = Date.now();
while (i <= 20) {
message.channel.sendMessage(`Testing my rates, item ${i} of 20`);
i++;
}
+ message.channel.sendMessage('last one...').then(m => {
+ const diff = Date.now() - start;
+ m.reply(`Each message took ${diff / 21}ms to send`);
+ });
}
if (message.content === 'makerole') {
@@ -176,4 +185,30 @@ client.on('message', msg => {
})
.catch(console.error);
}
-})
+});
+
+client.on('messageReactionAdd', (reaction, user) => {
+ if (reaction.message.channel.id !== '222086648706498562') return;
+ reaction.message.channel.sendMessage(`${user.username} added reaction ${reaction.emoji}, count is now ${reaction.count}`);
+});
+
+client.on('messageReactionRemove', (reaction, user) => {
+ if (reaction.message.channel.id !== '222086648706498562') return;
+ reaction.message.channel.sendMessage(`${user.username} removed reaction ${reaction.emoji}, count is now ${reaction.count}`);
+});
+
+client.on('message', m => {
+ if (m.content.startsWith('#reactions')) {
+ const mID = m.content.split(' ')[1];
+ m.channel.fetchMessage(mID).then(rM => {
+ for (const reaction of rM.reactions.values()) {
+ reaction.fetchUsers().then(users => {
+ m.channel.sendMessage(
+ `The following gave that message ${reaction.emoji}:\n` +
+ `${users.map(u => u.username).map(t => `- ${t}`).join('\n')}`
+ );
+ });
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/test/webpack.html b/test/webpack.html
new file mode 100644
index 000000000..967712e33
--- /dev/null
+++ b/test/webpack.html
@@ -0,0 +1,30 @@
+
+
+
+ discord.js Webpack test
+
+
+
+
+
+
+
+
diff --git a/typings b/typings
new file mode 160000
index 000000000..9b503a119
--- /dev/null
+++ b/typings
@@ -0,0 +1 @@
+Subproject commit 9b503a119c10c6873c8fd8cc65576f0992da5967
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 000000000..501279ee6
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,43 @@
+/*
+ ONLY RUN BUILDS WITH `npm run web-dist`!
+ DO NOT USE NORMAL WEBPACK! IT WILL NOT WORK!
+*/
+
+const webpack = require('webpack');
+const createVariants = require('parallel-webpack').createVariants;
+const version = require('./package.json').version;
+
+const createConfig = (options) => {
+ const plugins = [
+ new webpack.DefinePlugin({ 'global.GENTLY': false }),
+ ];
+
+ if (options.minify) plugins.push(new webpack.optimize.UglifyJsPlugin({ minimize: true }));
+
+ const filename = `./webpack/discord${process.env.VERSIONED === 'false' ? '' : '.' + version}${options.minify ? '.min' : ''}.js`; // eslint-disable-line
+
+ return {
+ entry: './src/index.js',
+ output: {
+ path: __dirname,
+ filename,
+ },
+ module: {
+ rules: [
+ { test: /\.md$/, loader: 'ignore-loader' },
+ ],
+ },
+ node: {
+ fs: 'empty',
+ dns: 'mock',
+ tls: 'mock',
+ child_process: 'empty',
+ dgram: 'empty',
+ zlib: 'empty',
+ __dirname: true,
+ },
+ plugins,
+ };
+};
+
+module.exports = createVariants({}, { minify: [false, true] }, createConfig);