Merge branch 'indev'

This commit is contained in:
Amish Shah
2016-12-29 16:37:00 +00:00
122 changed files with 3467 additions and 2437 deletions

View File

@@ -14,17 +14,20 @@
"valid-jsdoc": ["error", { "valid-jsdoc": ["error", {
"requireReturn": false, "requireReturn": false,
"requireReturnDescription": false, "requireReturnDescription": false,
"prefer": {
"return": "returns",
"arg": "param"
},
"preferType": { "preferType": {
"String": "string", "String": "string",
"Number": "number", "Number": "number",
"Boolean": "boolean", "Boolean": "boolean",
"Function": "function",
"object": "Object", "object": "Object",
"function": "Function",
"array": "Array",
"date": "Date", "date": "Date",
"error": "Error" "error": "Error",
}, "null": "void"
"prefer": {
"return": "returns"
} }
}], }],
@@ -80,6 +83,7 @@
"consistent-this": ["error", "$this"], "consistent-this": ["error", "$this"],
"eol-last": "error", "eol-last": "error",
"func-names": "error", "func-names": "error",
"func-name-matching": "error",
"func-style": ["error", "declaration", { "allowArrowFunctions": true }], "func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"indent": ["error", 2, { "SwitchCase": 1 }], "indent": ["error", 2, { "SwitchCase": 1 }],
"key-spacing": "error", "key-spacing": "error",
@@ -122,6 +126,7 @@
"no-useless-computed-key": "error", "no-useless-computed-key": "error",
"no-useless-constructor": "error", "no-useless-constructor": "error",
"prefer-arrow-callback": "error", "prefer-arrow-callback": "error",
"prefer-numeric-literals": "error",
"prefer-rest-params": "error", "prefer-rest-params": "error",
"prefer-spread": "error", "prefer-spread": "error",
"prefer-template": "error", "prefer-template": "error",

4
.gitignore vendored
View File

@@ -8,10 +8,14 @@ logs/
# Authentication # Authentication
test/auth.json test/auth.json
test/auth.js
docs/deploy/deploy_key docs/deploy/deploy_key
docs/deploy/deploy_key.pub docs/deploy/deploy_key.pub
deploy/deploy_key
deploy/deploy_key.pub
# Miscellaneous # Miscellaneous
.tmp/ .tmp/
.vscode/ .vscode/
docs/docs.json docs/docs.json
webpack/

3
.gitmodules vendored Normal file
View File

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

11
.tern-project Normal file
View File

@@ -0,0 +1,11 @@
{
"ecmaVersion": 6,
"libs": [],
"plugins": {
"node": {
"dontLoad": "node_modules/**",
"load": "",
"modules": ""
}
}
}

View File

@@ -5,9 +5,12 @@ cache:
directories: directories:
- node_modules - node_modules
install: npm install install: npm install
script: bash ./docs/deploy/deploy.sh script:
- npm run test
- bash ./deploy/deploy.sh
env: env:
global: global:
- ENCRYPTION_LABEL: "af862fa96d3e" - ENCRYPTION_LABEL: "af862fa96d3e"
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
dist: trusty
sudo: false

View File

@@ -6,7 +6,7 @@ is a great boon to your coding process.
## Setup ## Setup
To get ready to work on the codebase, please do the following: To get ready to work on the codebase, please do the following:
1. Fork & clone the repository 1. Fork & clone the repository, and make sure you're on the **indev** branch
2. Run `npm install` 2. Run `npm install`
3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript` 3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript`
4. Code your heart out! 4. Code your heart out!

View File

@@ -1,7 +1,9 @@
<div align="center"> <div align="center">
<br />
<p> <p>
<a href="https://discord.js.org"><img src="https://i.imgur.com/StEGtEh.png" width="546" alt="discord.js" /></a> <a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
</p> </p>
<br />
<p> <p>
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a> <a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a> <a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
@@ -15,21 +17,31 @@
</div> </div>
## About ## 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. discord.js is a powerful node.js module that allows you to interact with the
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. [Discord API](https://discordapp.com/developers/docs/intro) very easily.
Usability and performance are key focuses of discord.js. It also has nearly 100% coverage of the Discord API.
- Object-oriented
- Predictable abstractions
- Performant
- Nearly 100% coverage of the Discord API
## Installation ## 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` 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 ([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` 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. 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. 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 ## Example Usage
```js ```js
const Discord = require('discord.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). 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 ## Links
* [Website](http://discord.js.org/) * [Website](https://discord.js.org/)
* [Discord.js server](https://discord.gg/bRCvFy9) * [Discord.js server](https://discord.gg/bRCvFy9)
* [Discord API server](https://discord.gg/rV4BwdK) * [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) * [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) * [GitHub](https://github.com/hydrabolt/discord.js)
* [NPM](https://www.npmjs.com/package/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 ## Contributing
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
[documentation](http://discord.js.org/#!/docs). [documentation](https://discord.js.org/#/docs).
See [the contributing guide](CONTRIBUTING.md) if you'd like to submit a PR. See [the contribution guide](CONTRIBUTING.md) if you'd like to submit a PR.
## Help ## Help
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle

View File

@@ -4,7 +4,11 @@
set -e set -e
function build { 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 # Ignore Travis checking PRs
@@ -15,7 +19,9 @@ if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
fi fi
# Ignore travis checking other branches irrelevant to users # 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" echo "deploy.sh: Ignoring push to another branch than master/indev"
build build
exit 0 exit 0
@@ -29,19 +35,27 @@ if [ -n "$TRAVIS_TAG" ]; then
SOURCE=$TRAVIS_TAG SOURCE=$TRAVIS_TAG
fi fi
# Initialise some useful variables
REPO=`git config remote.origin.url` REPO=`git config remote.origin.url`
SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:} SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:}
SHA=`git rev-parse --verify HEAD` 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 # 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 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 # Move the generated JSON file to the newly-checked-out repo, to be committed
# and pushed # and pushed
@@ -49,20 +63,28 @@ mv docs/docs.json out/$SOURCE.json
# Commit and push # Commit and push
cd out cd out
git add .
git config user.name "Travis CI" git config user.name "Travis CI"
git config user.email "$COMMIT_AUTHOR_EMAIL" git config user.email "$COMMIT_AUTHOR_EMAIL"
git commit -m "Docs build: ${SHA}" || true
git add . git push $SSH_REPO $TARGET_BRANCH
git commit -m "Docs build: ${SHA}"
# Clean up...
ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key" cd ..
ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv" rm -rf out
ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR}
ENCRYPTED_IV=${!ENCRYPTED_IV_VAR} # ...then do the same once more for the webpack
openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in ../docs/deploy/deploy_key.enc -out deploy_key -d TARGET_BRANCH="webpack"
chmod 600 deploy_key git clone $REPO out -b $TARGET_BRANCH
eval `ssh-agent -s`
ssh-add deploy_key # Move the generated webpack over
mv webpack/discord.js out/discord.$SOURCE.js
# Now that we're all set up, we can push. 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 git push $SSH_REPO $TARGET_BRANCH

View File

@@ -1,2 +1 @@
# discord.js docs ## [View the documentation here.](https://discord.js.org/#/docs)
[View documentation here](http://discord.js.org/#!/docs)

View File

@@ -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')}
\`\`\``,
};

View File

@@ -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'),
};

View File

@@ -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;

View File

@@ -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')}
\`\`\``,
};

View File

@@ -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'),
};

View File

@@ -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')}
\`\`\``,
};

View File

@@ -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'),
};

View File

@@ -9,13 +9,15 @@ Update to Node.js 6.0.0 or newer.
## How do I get voice working? ## How do I get voice working?
- Install FFMPEG. - Install FFMPEG.
- Install either the `node-opus` package or the `opusscript` package. - 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? ## 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` - **Ubuntu 14.04:** `sudo apt-get install libav-tools`
- **Windows:** See the [FFMPEG section of AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md#download-ffmpeg). - **Windows:** 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? ## How do I set up node-opus?
- **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats! - **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!

View File

@@ -1,128 +1,128 @@
# Version 10 # Version 10
Version 10's non-BC changes focus on cleaning up some inconsistencies that exist in previous versions. 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. Upgrading from v9 should be quick and painless.
## Client options ## Client options
All client options have been converted to camelCase rather than snake_case, and `max_message_cache` was renamed to `messageCacheMaxSize`. All client options have been converted to camelCase rather than snake_case, and `max_message_cache` was renamed to `messageCacheMaxSize`.
v9 code example: v9 code example:
```js ```js
const client = new Discord.Client({ const client = new Discord.Client({
disable_everyone: true, disable_everyone: true,
max_message_cache: 500, max_message_cache: 500,
message_cache_lifetime: 120, message_cache_lifetime: 120,
message_sweep_interval: 60 message_sweep_interval: 60
}); });
``` ```
v10 code example: v10 code example:
```js ```js
const client = new Discord.Client({ const client = new Discord.Client({
disableEveryone: true, disableEveryone: true,
messageCacheMaxSize: 500, messageCacheMaxSize: 500,
messageCacheLifetime: 120, messageCacheLifetime: 120,
messageSweepInterval: 60 messageSweepInterval: 60
}); });
``` ```
## Presences ## Presences
Presences have been completely restructured. 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. 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: v9 discord.js code may look something like this:
```js ```js
User.status; // the status of the user User.status; // the status of the user
User.game; // the game that the user is playing User.game; // the game that the user is playing
ClientUser.setStatus(status, game, url); // set the new status for the user 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. 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. 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. 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. **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.** A user may have an entirely different presence between two different guilds.**
v10 code: v10 code:
```js ```js
MemberOrUser.presence.status; // the status of the member or user MemberOrUser.presence.status; // the status of the member or user
MemberOrUser.presence.game; // the game that the member or user is playing MemberOrUser.presence.game; // the game that the member or user is playing
ClientUser.setStatus(status); // online, idle, dnd, offline ClientUser.setStatus(status); // online, idle, dnd, offline
ClientUser.setGame(game, streamingURL); // a game ClientUser.setGame(game, streamingURL); // a game
ClientUser.setPresence(fullPresence); // status and game combined ClientUser.setPresence(fullPresence); // status and game combined
``` ```
## Voice ## Voice
Voice has been rewritten internally, but in a backwards-compatible manner. 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`. 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. Several more events have been made available to a VoiceConnection, so see the documentation.
## Events ## Events
Many events have been renamed or had their arguments change. Many events have been renamed or had their arguments change.
### Client events ### Client events
| Version 9 | Version 10 | | Version 9 | Version 10 |
|------------------------------------------------------|-----------------------------------------------| |------------------------------------------------------|-----------------------------------------------|
| guildMemberAdd(guild, member) | guildMemberAdd(member) | | guildMemberAdd(guild, member) | guildMemberAdd(member) |
| guildMemberAvailable(guild, member) | guildMemberAvailable(member) | | guildMemberAvailable(guild, member) | guildMemberAvailable(member) |
| guildMemberRemove(guild, member) | guildMemberRemove(member) | | guildMemberRemove(guild, member) | guildMemberRemove(member) |
| guildMembersChunk(guild, members) | guildMembersChunk(members) | | guildMembersChunk(guild, members) | guildMembersChunk(members) |
| guildMemberUpdate(guild, oldMember, newMember) | guildMemberUpdate(oldMember, newMember) | | guildMemberUpdate(guild, oldMember, newMember) | guildMemberUpdate(oldMember, newMember) |
| guildRoleCreate(guild, role) | roleCreate(role) | | guildRoleCreate(guild, role) | roleCreate(role) |
| guildRoleDelete(guild, role) | roleDelete(role) | | guildRoleDelete(guild, role) | roleDelete(role) |
| guildRoleUpdate(guild, oldRole, newRole) | roleUpdate(oldRole, newRole) | | 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`. The guild parameter that has been dropped from the guild-related events can still be derived using `member.guild` or `role.guild`.
### VoiceConnection events ### VoiceConnection events
| Version 9 | Version 10 | | Version 9 | Version 10 |
|--------------|------------| |--------------|------------|
| disconnected | disconnect | | disconnected | disconnect |
## Dates and timestamps ## Dates and timestamps
All dates/timestamps on the structures have been refactored to have a consistent naming scheme and availability. 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: All of them are named similarly to this:
**Date:** `Message.createdAt` **Date:** `Message.createdAt`
**Timestamp:** `Message.createdTimestamp` **Timestamp:** `Message.createdTimestamp`
See the docs for each structure to see which date/timestamps are available on them. See the docs for each structure to see which date/timestamps are available on them.
# Version 9 # Version 9
The version 9 (v9) rewrite takes a much more object-oriented approach than previous versions, 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. 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 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. 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 - 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. 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, 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. 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. 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: Here are a few examples of methods that have changed:
* `Client.sendMessage(channel, message)` ==> `TextChannel.sendMessage(message)` * `Client.sendMessage(channel, message)` ==> `TextChannel.sendMessage(message)`
* `Client.sendMessage(user, message)` ==> `User.sendMessage(message)` * `Client.sendMessage(user, message)` ==> `User.sendMessage(message)`
* `Client.updateMessage(message, "New content")` ==> `Message.edit("New Content")` * `Client.updateMessage(message, "New content")` ==> `Message.edit("New Content")`
* `Client.getChannelLogs(channel, limit)` ==> `TextChannel.fetchMessages({options})` * `Client.getChannelLogs(channel, limit)` ==> `TextChannel.fetchMessages({options})`
* `Server.detailsOfUser(User)` ==> `Server.members.get(User).properties` (retrieving a member gives a GuildMember object) * `Server.detailsOfUser(User)` ==> `Server.members.get(User).properties` (retrieving a member gives a GuildMember object)
* `Client.joinVoiceChannel(voicechannel)` => `VoiceChannel.join()` * `Client.joinVoiceChannel(voicechannel)` => `VoiceChannel.join()`
A couple more important details: A couple more important details:
* `Client.loginWithToken("token")` ==> `client.login("token")` * `Client.loginWithToken("token")` ==> `client.login("token")`
* `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`) * `Client.servers.length` ==> `client.guilds.size` (all instances of `server` are now `guild`)
## No more callbacks! ## No more callbacks!
Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed. Version 9 eschews callbacks in favour of Promises. This means all code relying on callbacks must be changed.
For example, the following code: For example, the following code:
```js ```js
client.getChannelLogs(channel, 100, function(messages) { client.getChannelLogs(channel, 100, function(messages) {
console.log(`${messages.length} messages found`); console.log(`${messages.length} messages found`);
}); });
``` ```
```js ```js
channel.fetchMessages({limit: 100}).then(messages => { channel.fetchMessages({limit: 100}).then(messages => {
console.log(`${messages.size} messages found`); console.log(`${messages.size} messages found`);
}); });
``` ```

View File

@@ -1,54 +1,72 @@
<div align="center"> <div align="center">
<p> <br />
<a href="https://discord.js.org"><img src="https://i.imgur.com/StEGtEh.png" width="546" alt="discord.js" /></a> <p>
</p> <a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
<p> </p>
<a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a> <br />
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a> <p>
<a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a> <a href="https://discord.gg/bRCvFy9"><img src="https://discordapp.com/api/guilds/222078108977594368/embed.png" alt="Discord server" /></a>
<a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a> <a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/v/discord.js.svg?maxAge=3600" alt="NPM version" /></a>
<a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a> <a href="https://www.npmjs.com/package/discord.js"><img src="https://img.shields.io/npm/dt/discord.js.svg?maxAge=3600" alt="NPM downloads" /></a>
</p> <a href="https://travis-ci.org/hydrabolt/discord.js"><img src="https://travis-ci.org/hydrabolt/discord.js.svg" alt="Build status" /></a>
<p> <a href="https://david-dm.org/hydrabolt/discord.js"><img src="https://img.shields.io/david/hydrabolt/discord.js.svg?maxAge=3600" alt="Dependencies" /></a>
<a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a> </p>
</p> <p>
</div> <a href="https://nodei.co/npm/discord.js/"><img src="https://nodei.co/npm/discord.js.png?downloads=true&stars=true" alt="NPM info" /></a>
</p>
# Welcome! </div>
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. # Welcome!
Welcome to the discord.js v10 documentation.
## About v10 is just a more consistent and stable iteration over v9, and contains loads of new and improved features, optimisations, and bug fixes.
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. ## About
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.
## Installation
**Node.js 6.0.0 or newer is required.** - Object-oriented
- Predictable abstractions
Without voice support: `npm install discord.js --save` - Performant
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save` - Nearly 100% coverage of the Discord API
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
## Installation
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. **Node.js 6.0.0 or newer is required.**
Using opusscript is only recommended for development on Windows, since getting node-opus to build there can be a bit of a challenge. Ignore any warnings about unmet peer dependencies - all of them are optional.
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
Without voice support: `npm install discord.js --save`
## Guides With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/) With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
* [York's v9 upgrade guide](https://yorkaargh.wordpress.com/2016/09/03/updating-discord-js-bots/)
### Audio engines
## Links 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.
* [Website](http://discord.js.org/) Using opusscript is only recommended for development environments where node-opus is tough to get working.
* [Discord.js server](https://discord.gg/bRCvFy9) For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
* [Discord API server](https://discord.gg/rV4BwdK)
* [Documentation](http://discord.js.org/#!/docs) ### Optional packages
* [Legacy (v8) documentation](http://discordjs.readthedocs.io/en/8.2.0/docs_client.html) - [uws](https://www.npmjs.com/package/uws) for much a much faster WebSocket connection (`npm install uws --save`)
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples) - [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack --save`)
* [GitHub](https://github.com/hydrabolt/discord.js)
* [NPM](https://www.npmjs.com/package/discord.js) ## Web distributions
* [Related libraries](https://discordapi.com/unofficial/libs.html) 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')`,
## Help the entire `Discord` object is available as a global (on the `window` object).
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle The ShardingManager and any voice-related functionality is unavailable in these builds.
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
## 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).

View File

@@ -1,4 +0,0 @@
{
"GEN_VERSION": 13,
"COMPRESS": false
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.<Role>"
]
},
"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;

View File

@@ -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.<Message>"
]
}
}
],
"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;

View File

@@ -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;

View File

@@ -1,17 +0,0 @@
class DocumentedItem {
constructor(parent, info) {
this.parent = parent;
this.directData = {};
this.registerMetaInfo(info);
}
registerMetaInfo() {
return;
}
serialize() {
return;
}
}
module.exports = DocumentedItem;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

16
docs/index.yml Normal file
View File

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

19
docs/logo.svg Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="100%" width="100%" viewBox="0 0 6111.4378 1102.9827">
<g transform="translate(2539.6 -107.66)">
<g id="logo-discord" fill="#3d3f42" transform="translate(-44.194 1175.6)">
<path d="m-2495.4-1051.4v453.6 453.6l145.75-.37695c127.36-.3288 147.71-.58582 161.25-2.041 45.045-4.8398 76.353-11.233 111.79-22.826 44.217-14.465 83.672-35.567 118.71-63.49 13.615-10.851 40.444-37.567 50.889-50.674 37.186-46.665 61.816-98.191 78.01-163.2 23.57-94.614 23.154-219.66-1.0469-313.5-41.72-161.77-155.27-260-329.35-284.92-38.756-5.5479-34.464-5.4161-190.75-5.8086l-145.25-.3652zm161 130.09 41.75.0156c55.334.0205 78.397 1.6295 108.25 7.5566 105.75 20.995 171.57 87.554 196.39 198.59 12.878 57.6 14.716 139.6 4.5469 202.81-7.3952 45.963-21.469 87.286-40.711 119.53-12.041 20.179-33.82 45.681-51 59.719-38.627 31.563-87.98 50.255-148.73 56.326-9.5463.9541-32.361 1.7291-62.75 2.1328l-47.75.63477v-323.66-323.66z"/>
<path d="m-1631.4-597.85v-453.5h80.5 80.5v453.5 453.5h-80.5-80.5v-453.5z"/>
<path d="m-1008.4-128.41c-96.325-5.9603-189.36-41.918-264.54-102.25-15.565-12.49-33-28.526-33-30.352 0-.7224 20.622-25.63 45.826-55.351l45.826-54.038 3.8214 3.2697c17.83 15.256 22.538 19.151 29.616 24.501 48.673 36.79 103.35 61.169 158.92 70.862 18.387 3.2073 54.666 4.419 74.088 2.4745 41.751-4.1802 74.798-17.199 96.864-38.16 10.213-9.7012 15.896-17.429 21.626-29.408 17.4-36.376 13.152-81.77-10.39-111-16.357-20.31-45.054-37.907-98.696-60.521-41.654-17.56-164.15-71.537-176.19-77.638-85.541-43.335-134.63-104.27-148.9-184.84-2.6851-15.162-3.7276-49.931-1.9989-66.666 7.4631-72.25 48.261-136.63 113.09-178.46 41.81-26.976 88.546-43.103 144.99-50.03 20.52-2.5182 67.722-2.5268 88-.016 74.352 9.2063 141.74 36.296 199 79.999 18.772 14.327 37.632 31.435 36.864 33.44-.2001.52235-18.812 23.693-41.361 51.49l-40.997 50.54-3.503-2.9264c-1.9267-1.6095-9.4625-7.4505-16.746-12.98-44.158-33.522-88.429-52.307-140.26-59.513-17.665-2.4562-54.274-2.4782-70-.042-35.82 5.5488-61.303 16.869-80.113 35.588-17.506 17.422-26.238 37.587-27.528 63.576-1.3118 26.419 6.521 48.306 24.066 67.249 17.834 19.254 45.314 35.115 99.448 57.398 32.211 13.259 137.3 57.517 151.65 63.864 47.003 20.795 80.577 42.726 108.49 70.87 43.959 44.316 64.938 98.562 65.021 168.13.053 44.646-7.8058 78.816-26.734 116.23-12.46 24.632-27.741 45.114-49.45 66.28-51.458 50.172-122.59 79.937-208.86 87.392-17.502 1.5126-51.786 2.0335-67.962 1.0326z"/>
<path d="m-155.84-128.44c-100.7-5.7557-190.26-44.562-257.1-111.4-58.171-58.171-98.098-136.72-116.41-229.01-13.522-68.153-15.549-148.4-5.5195-218.5 13.11-91.624 47.506-173.73 99.29-237 11.342-13.858 35.64-38.591 49.282-50.164 54.726-46.425 120.9-76.546 193.88-88.256 25.873-4.1511 37.999-5.0552 67.977-5.0681 28.858-.013 38.31.6981 60.5 4.5485 70.566 12.245 140.29 49.396 192.89 102.78l6.8911 6.9936-2.8911 3.4607c-1.59 1.9034-21.52 24.408-44.288 50.011l-41.397 46.551-10.103-9.0797c-40.998-36.846-79.308-56.146-125.89-63.421-13.826-2.1591-48.594-2.4422-62.711-.51067-51.945 7.1074-94.856 27.696-131.17 62.933-64.806 62.887-97.854 165.12-92.829 287.16 2.697 65.505 14.091 119.1 35.16 165.38 30.027 65.96 77.365 110.94 138.03 131.16 24.572 8.1885 46.583 11.525 76.026 11.525 45.839 0 83.431-9.665 120.81-31.062 19.559-11.195 45.837-32.314 63.267-50.848 3.7379-3.9745 7.1554-7.0833 7.5942-6.9085 1.3142.5236 88.109 97.158 88.109 98.098 0 2.0843-41.684 42.322-54 52.126-73.043 58.146-157.48 84.1-255.41 78.503z"/>
<path d="m610.07-1067.8c-34.898-.056-47.464.862-75.232 5.4922-188.34 31.405-308.9 182.45-325.21 407.46-2.8044 38.675-2.2536 84.125 1.4941 123.38 9.2582 96.975 39.751 184.31 87.494 250.58 57.015 79.142 139.29 130.29 236.46 147 14.533 2.4988 40.496 5.3373 53.5 5.8496 147.12 5.7956 267.7-55.193 342.98-173.48 10.897-17.122 28.991-52.974 36.758-72.828 27.4-70.046 39.498-139.21 39.617-226.5.062-45.479-1.9339-73.343-7.9121-110.4-31.164-193.18-145.75-321-314.25-350.53-27.838-4.8789-41.445-5.9606-75.699-6.0156zm-1.4395 139.59c2.8062.0114 5.6199.0752 8.4395.19336 49.33 2.0671 91.449 18.361 127.46 49.305 12.954 11.133 20.363 19.102 31.482 33.861 40.99 54.409 62.709 125.93 66.582 219.25 4.5628 109.93-19.826 208.09-67.676 272.39-33.936 45.599-76.643 72.514-130.84 82.459-10.577 1.9408-50.92 2.8029-62 1.3242-74.694-9.9681-131.62-54.014-168.58-130.43-24.356-50.365-36.989-106.85-39.92-178.5-5.9652-145.81 37.791-262.31 118.61-315.79 33.933-22.452 74.357-34.245 116.45-34.074z"/>
<path d="m1187.6-1051.4v453.54 453.54h80.5 80.5v-177.51-177.51l68.717.25585 68.719.25782 97.531 177.22 97.533 177.22 90.285.0273c85.686.0268 90.237-.0599 89.336-1.7207-.5222-.9625-49.147-86.08-108.05-189.15-58.906-103.07-106.98-187.52-106.83-187.67.1497-.14971 5.5455-2.31 11.99-4.8008 92.947-35.923 149.28-103.8 164.7-198.43 3.4973-21.47 4.3763-36.845 3.7539-65.688-.8444-39.124-4.5518-62.293-14.883-93.008-29.696-88.286-106.44-143.03-224.91-160.44-38.597-5.6719-28.81-5.4157-221.14-5.7871l-177.75-.3438zm161 128.95 84.25.37695c91.298.40795 95.375.61732 123.75 6.3809 23.495 4.7723 45.38 13.215 61 23.533 15.167 10.019 29.716 27.182 37.475 44.207 14.573 31.978 16.395 82.735 4.3301 120.62-6.6274 20.814-16.172 36.615-31.18 51.625-27.567 27.57-66.814 42.804-121.93 47.324-7.3903.60617-43.437 1.0508-85.25 1.0508h-72.445v-147.56-147.56z"/>
<path d="m2014.6-1051.4v453.6 453.6l145.75-.37695c156.69-.4046 153.13-.29648 191.25-5.8008 38.321-5.5332 77.017-15.82 109.08-28.998 17.362-7.137 22.208-9.743 21.508-11.566-.3206-.8355-1.452-4.9721-2.5156-9.1914-3.4865-13.831-4.3718-23.482-3.7617-41.053.63-18.145 2.2913-27.3 7.7285-42.617 17.594-49.562 60.836-85.599 112.95-94.131 16.457-2.6941 38.955-1.8474 57.701 2.1719 3.6928.79178 3.1565 1.7476 11.26-20.041 27.066-72.775 38.169-169.68 30.476-265.97-14.239-178.25-95.276-299.81-236.97-355.47-33.122-13.01-69.539-22.404-108.45-27.975-38.756-5.5479-34.464-5.4161-190.75-5.8086l-145.25-.3652zm161 130.09 41.75.0156c55.334.0205 78.397 1.6295 108.25 7.5566 105.75 20.995 171.57 87.554 196.39 198.59 12.878 57.6 14.716 139.6 4.5469 202.81-7.3952 45.963-21.469 87.286-40.711 119.53-12.041 20.179-33.82 45.681-51 59.719-38.627 31.563-87.98 50.255-148.73 56.326-9.5463.9541-32.361 1.7291-62.75 2.1328l-47.75.63477v-323.66-323.66z"/>
</g>
<circle id="logo-dot" cx="2575.3" cy="939.96" r="125.4" fill="#499a6c"/>
<g id="logo-js" fill="#33b5e5" transform="translate(-44.194 1175.6)">
<path d="m2602.1 34.57c-57.094-4.6075-113.49-28.558-158.26-67.213-27.741-23.949-51.228-55.235-63.883-85.094-5.4804-12.93-5.926-15.992-2.3882-16.406 8.1404-.953 38.073-7.05 53.318-10.86 20.337-5.0831 29.827-8.2686 48.112-16.15 12.138-5.2318 12.996-5.46 14-3.7198 14.778 25.613 36.757 46.236 62.906 59.024 21.609 10.567 39.696 14.761 63.664 14.761 23.073 0 41.694-4.1466 61.73-13.746 36.584-17.528 62.542-46.884 75.844-85.772 2.3995-7.0151 7.5664-31.714 9.361-44.747 2.8753-20.881 3.0454-40.134 3.0555-345.75l.01-314.25h78 78v318.25c0 209.58-.3574 323.03-1.0389 332.25-4.4405 60.076-22.061 115.17-51.016 159.5-11.306 17.311-21.135 29.375-35.857 44.012-44.122 43.866-101.51 69.204-169.58 74.876-17.815 1.4842-53.463 2.0433-65.964 1.0344z"/>
<path d="m3256.6 33.535c-103.92-8.2588-202.14-50.771-278.59-120.57l-11.459-10.464 4.7737-5.6963c2.6255-3.133 23.371-27.615 46.101-54.405l41.327-48.709 11.068 9.6086c54.856 47.624 120.13 79.074 185.78 89.508 19.275 3.0634 60.816 3.3389 79 .5237 56.007-8.6707 91.978-30.946 109.48-67.793 5.7814-12.174 8.6772-25.17 9.2639-41.574 1.8511-51.755-20.009-81.836-81.241-111.79-10.45-5.1123-25.75-12.128-34-15.591-32.568-13.67-168.23-73.282-178.56-78.459-84.895-42.577-136.19-105.76-149.34-183.97-24.654-146.62 80.068-271.29 246.91-293.93 39.105-5.3065 82.999-4.2183 122.48 3.0365 76.174 13.996 145.21 48.561 201.87 101.07l7.367 6.8275-39.699 49c-21.834 26.95-40.537 49.863-41.563 50.918-1.8327 1.8856-1.9536 1.8424-7.1685-2.562-25.013-21.126-59.394-41.952-87.804-53.188-33.742-13.345-63.677-18.968-101.5-19.066-28.062-.0727-45.321 2.2-65.5 8.6248-40.117 12.773-65.445 37.309-74.612 72.282-3.4331 13.097-3.8978 33.664-1.0368 45.883 7.6067 32.488 29.949 55.7 75.674 78.622 15.123 7.5809 24.021 11.522 52.974 23.46 125.45 51.728 173.58 73.274 198.67 88.935 70.314 43.888 106.41 97.76 116.97 174.59 2.1563 15.683 2.4444 55.002.5056 69-7.9359 57.297-31.186 104.9-70.626 144.6-53.439 53.792-126.37 84.242-218.91 91.402-14.98 1.1588-53.385 1.0944-68.605-.1152z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -3,9 +3,13 @@
"version": "10.0.1", "version": "10.0.1",
"description": "A powerful library for interacting with the Discord API", "description": "A powerful library for interacting with the Discord API",
"main": "./src/index", "main": "./src/index",
"types": "./typings/index.d.ts",
"scripts": { "scripts": {
"test": "eslint src/ && node docs/generator/generator.js silent", "test": "eslint src && docgen --source src --custom docs/index.yml",
"docs": "node docs/generator/generator.js" "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": { "repository": {
"type": "git", "type": "git",
@@ -25,21 +29,57 @@
"url": "https://github.com/hydrabolt/discord.js/issues" "url": "https://github.com/hydrabolt/discord.js/issues"
}, },
"homepage": "https://github.com/hydrabolt/discord.js#readme", "homepage": "https://github.com/hydrabolt/discord.js#readme",
"runkitExampleFilename": "./docs/examples/ping.js",
"dependencies": { "dependencies": {
"superagent": "^2.3.0", "@types/node": "^6.0.0",
"tweetnacl": "^0.14.3", "pako": "^1.0.0",
"ws": "^1.1.1" "superagent": "^3.3.0",
"tweetnacl": "^0.14.0",
"ws": "^1.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"node-opus": "^0.2.1", "erlpack": "hammerandchisel/erlpack#master",
"opusscript": "^0.0.1" "node-opus": "^0.2.0",
"opusscript": "^0.0.1",
"uws": "^0.12.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^3.8.0", "discord.js-docgen": "hydrabolt/discord.js-docgen#master",
"fs-extra": "^0.30.0", "eslint": "^3.12.0",
"jsdoc-to-markdown": "^2.0.0" "parallel-webpack": "^1.6.0",
"uglify-js": "mishoo/UglifyJS2#harmony",
"webpack": "2.2.0-rc.3"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "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
} }
} }

View File

@@ -77,39 +77,39 @@ class Client extends EventEmitter {
this.actions = new ActionsManager(this); this.actions = new ActionsManager(this);
/** /**
* The Voice Manager of the Client * The Voice Manager of the Client (`null` in browsers)
* @type {ClientVoiceManager} * @type {?ClientVoiceManager}
* @private * @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) * 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; 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<string, User>} * @type {Collection<string, User>}
*/ */
this.users = new Collection(); this.users = new Collection();
/** /**
* A Collection of the Client's stored guilds * A collection of the Client's stored guilds
* @type {Collection<string, Guild>} * @type {Collection<string, Guild>}
*/ */
this.guilds = new Collection(); this.guilds = new Collection();
/** /**
* A Collection of the Client's stored channels * A collection of the Client's stored channels
* @type {Collection<string, Channel>} * @type {Collection<string, Channel>}
*/ */
this.channels = new Collection(); this.channels = new Collection();
/** /**
* A Collection of presences for friends of the logged in user. * A collection of presences for friends of the logged in user.
* <warn>This is only present for user accounts, not bot accounts!</warn> * <warn>This is only filled when using a user account.</warn>
* @type {Collection<string, Presence>} * @type {Collection<string, Presence>}
*/ */
this.presences = new Collection(); this.presences = new Collection();
@@ -124,18 +124,6 @@ class Client extends EventEmitter {
this.token = null; 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 * The ClientUser representing the logged in Client
* @type {?ClientUser} * @type {?ClientUser}
@@ -148,6 +136,13 @@ class Client extends EventEmitter {
*/ */
this.readyAt = null; 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._timeouts = new Set();
this._intervals = 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<string, VoiceConnection>} * @type {Collection<string, VoiceConnection>}
* @readonly * @readonly
*/ */
get voiceConnections() { get voiceConnections() {
if (this.browser) return new Collection();
return this.voice.connections; return this.voice.connections;
} }
@@ -205,14 +210,21 @@ class Client extends EventEmitter {
return this.readyAt ? this.readyAt.getTime() : null; 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. <warn>If you're making a bot, it's * Logs the client in. If successful, resolves with the account's token. <warn>If you're making a bot, it's
* much better to use a bot account rather than a user account. * 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 * 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.</warn> * that are making a lot of API requests can even be banned.</warn>
* @param {string} tokenOrEmail The token or email used for the account. If it is an email, a password _must_ be * @param {string} token The token used for the account.
* provided.
* @param {string} [password] The password for the account, only needed if an email was provided.
* @returns {Promise<string>} * @returns {Promise<string>}
* @example * @example
* // log the client in using a token * // log the client in using a token
@@ -224,9 +236,8 @@ class Client extends EventEmitter {
* const password = 'supersecret123'; * const password = 'supersecret123';
* client.login(email, password); * client.login(email, password);
*/ */
login(tokenOrEmail, password = null) { login(token) {
if (password) return this.rest.methods.loginEmailPassword(tokenOrEmail, password); return this.rest.methods.login(token);
return this.rest.methods.loginToken(tokenOrEmail);
} }
/** /**
@@ -238,29 +249,26 @@ class Client extends EventEmitter {
for (const i of this._intervals) clearInterval(i); for (const i of this._intervals) clearInterval(i);
this._timeouts.clear(); this._timeouts.clear();
this._intervals.clear(); this._intervals.clear();
this.token = null;
this.email = null;
this.password = null;
return this.manager.destroy(); return this.manager.destroy();
} }
/** /**
* This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however * 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.
* <warn>This is only available when using a user account.</warn>
* @param {Guild[]|Collection<string, Guild>} [guilds=this.guilds] An array or collection of guilds to sync * @param {Guild[]|Collection<string, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
*/ */
syncGuilds(guilds = this.guilds) { syncGuilds(guilds = this.guilds) {
if (!this.user.bot) { if (this.user.bot) return;
this.ws.send({ this.ws.send({
op: 12, op: 12,
d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id), 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. * 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. * <warn>This is only available when using a bot account.</warn>
* @param {string} id The ID of the user to obtain * @param {string} id The ID of the user to obtain
* @returns {Promise<User>} * @returns {Promise<User>}
*/ */
@@ -282,10 +290,11 @@ class Client extends EventEmitter {
/** /**
* Fetch a webhook by ID. * Fetch a webhook by ID.
* @param {string} id ID of the webhook * @param {string} id ID of the webhook
* @param {string} [token] Token for the webhook
* @returns {Promise<Webhook>} * @returns {Promise<Webhook>}
*/ */
fetchWebhook(id) { fetchWebhook(id, token) {
return this.rest.methods.getWebhook(id); return this.rest.methods.getWebhook(id, token);
} }
/** /**
@@ -324,31 +333,90 @@ class Client extends EventEmitter {
return messages; return messages;
} }
setTimeout(fn, ...params) { /**
* Gets the bot's OAuth2 application.
* <warn>This is only available when using a bot account.</warn>
* @returns {Promise<ClientOAuth2Application>}
*/
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<string>} 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(() => { const timeout = setTimeout(() => {
fn(); fn();
this._timeouts.delete(timeout); this._timeouts.delete(timeout);
}, ...params); }, delay, ...args);
this._timeouts.add(timeout); this._timeouts.add(timeout);
return timeout; return timeout;
} }
/**
* Clears a timeout
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) { clearTimeout(timeout) {
clearTimeout(timeout); clearTimeout(timeout);
this._timeouts.delete(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); this._intervals.add(interval);
return interval; return interval;
} }
/**
* Clears an interval
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) { clearInterval(interval) {
clearInterval(interval); clearInterval(interval);
this._intervals.delete(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) { _setPresence(id, presence) {
if (this.presences.get(id)) { if (this.presences.get(id)) {
this.presences.get(id).update(presence); this.presences.get(id).update(presence);
@@ -400,11 +468,11 @@ module.exports = Client;
/** /**
* Emitted for general warnings * Emitted for general warnings
* @event Client#warn * @event Client#warn
* @param {string} The warning * @param {string} info The warning
*/ */
/** /**
* Emitted for general debugging information * Emitted for general debugging information
* @event Client#debug * @event Client#debug
* @param {string} The debug information * @param {string} info The debug information
*/ */

View File

@@ -24,7 +24,7 @@ class ClientDataManager {
this.client.guilds.set(guild.id, guild); this.client.guilds.set(guild.id, guild);
if (this.pastReady && !already) { if (this.pastReady && !already) {
/** /**
* Emitted whenever the client joins a Guild. * Emitted whenever the client joins a guild.
* @event Client#guildCreate * @event Client#guildCreate
* @param {Guild} guild The created guild * @param {Guild} guild The created guild
*/ */
@@ -78,7 +78,7 @@ class ClientDataManager {
const already = guild.emojis.has(data.id); const already = guild.emojis.has(data.id);
if (data && !already) { if (data && !already) {
let emoji = new Emoji(guild, data); 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); guild.emojis.set(emoji.id, emoji);
return emoji; return emoji;
} else if (already) { } else if (already) {
@@ -90,7 +90,7 @@ class ClientDataManager {
killEmoji(emoji) { killEmoji(emoji) {
if (!(emoji instanceof Emoji && emoji.guild)) return; 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); emoji.guild.emojis.delete(emoji.id);
} }

View File

@@ -3,11 +3,14 @@ const fs = require('fs');
const request = require('superagent'); const request = require('superagent');
const Constants = require('../util/Constants'); const Constants = require('../util/Constants');
const User = require(`../structures/User`); const convertArrayBuffer = require('../util/ConvertArrayBuffer');
const Message = require(`../structures/Message`); const User = require('../structures/User');
const Guild = require(`../structures/Guild`); const Message = require('../structures/Message');
const Channel = require(`../structures/Channel`); const Guild = require('../structures/Guild');
const GuildMember = require(`../structures/GuildMember`); 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. * 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: * Data that resolves to give a User object. This can be:
* * A User object * * A User object
* * A User ID * * A user ID
* * A Message (resolves to the message author) * * A Message object (resolves to the message author)
* * A Guild (owner of the guild) * * A Guild object (owner of the guild)
* * A Guild Member * * A GuildMember object
* @typedef {User|string|Message|Guild|GuildMember} UserResolvable * @typedef {User|string|Message|Guild|GuildMember} UserResolvable
*/ */
@@ -62,7 +65,8 @@ class ClientDataResolver {
/** /**
* Data that resolves to give a Guild object. This can be: * Data that resolves to give a Guild object. This can be:
* * A Guild object * * A Guild object
* @typedef {Guild} GuildResolvable * * A Guild ID
* @typedef {Guild|string} GuildResolvable
*/ */
/** /**
@@ -91,20 +95,18 @@ class ClientDataResolver {
*/ */
resolveGuildMember(guild, user) { resolveGuildMember(guild, user) {
if (user instanceof GuildMember) return user; if (user instanceof GuildMember) return user;
guild = this.resolveGuild(guild); guild = this.resolveGuild(guild);
user = this.resolveUser(user); user = this.resolveUser(user);
if (!guild || !user) return null; if (!guild || !user) return null;
return guild.members.get(user.id) || null; return guild.members.get(user.id) || null;
} }
/** /**
* Data that can be resolved to give a Channel. This can be: * Data that can be resolved to give a Channel. This can be:
* * An instance of a Channel * * A Channel object
* * An instance of a Message (the channel the message was sent in) * * A Message object (the channel the message was sent in)
* * An instance of a Guild (the #general channel) * * A Guild object (the #general channel)
* * An ID of a Channel * * A channel ID
* @typedef {Channel|Guild|Message|string} ChannelResolvable * @typedef {Channel|Guild|Message|string} ChannelResolvable
*/ */
@@ -136,7 +138,6 @@ class ClientDataResolver {
resolveInviteCode(data) { resolveInviteCode(data) {
const inviteRegex = /discord(?:app)?\.(?:gg|com\/invite)\/([a-z0-9]{5})/i; const inviteRegex = /discord(?:app)?\.(?:gg|com\/invite)\/([a-z0-9]{5})/i;
const match = inviteRegex.exec(data); const match = inviteRegex.exec(data);
if (match && match[1]) return match[1]; if (match && match[1]) return match[1];
return data; return data;
} }
@@ -155,6 +156,7 @@ class ClientDataResolver {
* "ADMINISTRATOR", * "ADMINISTRATOR",
* "MANAGE_CHANNELS", * "MANAGE_CHANNELS",
* "MANAGE_GUILD", * "MANAGE_GUILD",
* "ADD_REACTIONS", // add reactions to messages
* "READ_MESSAGES", * "READ_MESSAGES",
* "SEND_MESSAGES", * "SEND_MESSAGES",
* "SEND_TTS_MESSAGES", * "SEND_TTS_MESSAGES",
@@ -172,7 +174,9 @@ class ClientDataResolver {
* "USE_VAD", // use voice activity detection * "USE_VAD", // use voice activity detection
* "CHANGE_NICKNAME", * "CHANGE_NICKNAME",
* "MANAGE_NICKNAMES", // change nicknames of others * "MANAGE_NICKNAMES", // change nicknames of others
* "MANAGE_ROLES_OR_PERMISSIONS" * "MANAGE_ROLES_OR_PERMISSIONS",
* "MANAGE_WEBHOOKS",
* "MANAGE_EMOJIS"
* ] * ]
* ``` * ```
* @typedef {string|number} PermissionResolvable * @typedef {string|number} PermissionResolvable
@@ -189,10 +193,21 @@ class ClientDataResolver {
return permission; 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: * Data that can be resolved to give a string. This can be:
* * A string * * 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 * * Any value
* @typedef {string|Array|*} StringResolvable * @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: * Data that resolves to give a Base64 string, typically for image uploading. This can be:
* * A Buffer * * A Buffer
* * A Base64 string * * A base64 string
* @typedef {Buffer|string} Base64Resolvable * @typedef {Buffer|string} Base64Resolvable
*/ */
@@ -230,21 +245,29 @@ class ClientDataResolver {
* * A Buffer * * A Buffer
* * The path to a local file * * The path to a local file
* * A URL * * A URL
* @typedef {string|Buffer} FileResolvable * @typedef {string|Buffer} BufferResolvable
*/ */
/** /**
* Resolves a FileResolvable to a Buffer * Resolves a BufferResolvable to a Buffer
* @param {FileResolvable} resource The file resolvable to resolve * @param {BufferResolvable} resource The buffer resolvable to resolve
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
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') { if (typeof resource === 'string') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (/^https?:\/\//.test(resource)) { if (/^https?:\/\//.test(resource)) {
request.get(resource) const req = request.get(resource).set('Content-Type', 'blob');
.set('Content-Type', 'blob') if (this.client.browser) req.responseType('arraybuffer');
.end((err, res) => err ? reject(err) : resolve(res.body)); 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 { } else {
const file = path.resolve(resource); const file = path.resolve(resource);
fs.stat(file, (err, stats) => { fs.stat(file, (err, stats) => {
@@ -258,8 +281,28 @@ class ClientDataResolver {
}); });
} }
if (resource instanceof Buffer) return Promise.resolve(resource); return Promise.reject(new TypeError('The resource must be a string or Buffer.'));
return Promise.reject(new TypeError('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;
} }
} }

View File

@@ -22,8 +22,8 @@ class ClientManager {
/** /**
* Connects the Client to the WebSocket * Connects the Client to the WebSocket
* @param {string} token The authorization token * @param {string} token The authorization token
* @param {function} resolve Function to run when connection is successful * @param {Function} resolve Function to run when connection is successful
* @param {function} reject Function to run when connection fails * @param {Function} reject Function to run when connection fails
*/ */
connectToWebSocket(token, resolve, reject) { connectToWebSocket(token, resolve, reject) {
this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`); this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`);
@@ -40,7 +40,7 @@ class ClientManager {
resolve(token); resolve(token);
this.client.clearTimeout(timeout); 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 * @param {number} time The interval in milliseconds at which heartbeat packets should be sent
*/ */
setupKeepAlive(time) { setupKeepAlive(time) {
this.heartbeatInterval = this.client.setInterval(() => { this.heartbeatInterval = this.client.setInterval(() => this.client.ws.heartbeat(true), time);
this.client.emit('debug', 'Sending heartbeat');
this.client.ws.send({
op: Constants.OPCodes.HEARTBEAT,
d: this.client.ws.sequence,
}, true);
}, time);
} }
destroy() { destroy() {
return new Promise((resolve, reject) => { this.client.ws.destroy();
this.client.ws.destroy(); if (this.client.user.bot) {
if (!this.client.user.bot) { this.client.token = null;
this.client.rest.methods.logout().then(resolve, reject); return Promise.resolve();
} else { } else {
resolve(); return this.client.rest.methods.logout().then(() => {
} this.client.token = null;
}); });
}
} }
} }

View File

@@ -2,33 +2,36 @@ class ActionsManager {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.register('MessageCreate'); this.register(require('./MessageCreate'));
this.register('MessageDelete'); this.register(require('./MessageDelete'));
this.register('MessageDeleteBulk'); this.register(require('./MessageDeleteBulk'));
this.register('MessageUpdate'); this.register(require('./MessageUpdate'));
this.register('ChannelCreate'); this.register(require('./MessageReactionAdd'));
this.register('ChannelDelete'); this.register(require('./MessageReactionRemove'));
this.register('ChannelUpdate'); this.register(require('./MessageReactionRemoveAll'));
this.register('GuildDelete'); this.register(require('./ChannelCreate'));
this.register('GuildUpdate'); this.register(require('./ChannelDelete'));
this.register('GuildMemberGet'); this.register(require('./ChannelUpdate'));
this.register('GuildMemberRemove'); this.register(require('./GuildDelete'));
this.register('GuildBanRemove'); this.register(require('./GuildUpdate'));
this.register('GuildRoleCreate'); this.register(require('./GuildMemberGet'));
this.register('GuildRoleDelete'); this.register(require('./GuildMemberRemove'));
this.register('GuildRoleUpdate'); this.register(require('./GuildBanRemove'));
this.register('UserGet'); this.register(require('./GuildRoleCreate'));
this.register('UserUpdate'); this.register(require('./GuildRoleDelete'));
this.register('GuildSync'); this.register(require('./GuildRoleUpdate'));
this.register('GuildEmojiCreate'); this.register(require('./UserGet'));
this.register('GuildEmojiDelete'); this.register(require('./UserUpdate'));
this.register('GuildEmojiUpdate'); this.register(require('./UserNoteUpdate'));
this.register('GuildRolesPositionUpdate'); this.register(require('./GuildSync'));
this.register(require('./GuildEmojiCreate'));
this.register(require('./GuildEmojiDelete'));
this.register(require('./GuildEmojiUpdate'));
this.register(require('./GuildRolesPositionUpdate'));
} }
register(name) { register(Action) {
const Action = require(`./${name}`); this[Action.name.replace(/Action$/, '')] = new Action(this.client);
this[name] = new Action(this.client);
} }
} }

View File

@@ -1,9 +1,9 @@
const Action = require('./Action'); const Action = require('./Action');
class EmojiCreateAction extends Action { class GuildEmojiCreateAction extends Action {
handle(data, guild) { handle(guild, createdEmoji) {
const client = this.client; const client = this.client;
const emoji = client.dataManager.newEmoji(data, guild); const emoji = client.dataManager.newEmoji(createdEmoji, guild);
return { return {
emoji, emoji,
}; };
@@ -11,8 +11,8 @@ class EmojiCreateAction extends Action {
} }
/** /**
* Emitted whenever an emoji is created * Emitted whenever a custom emoji is created in a guild
* @event Client#guildEmojiCreate * @event Client#emojiCreate
* @param {Emoji} emoji The emoji that was created. * @param {Emoji} emoji The emoji that was created.
*/ */
module.exports = EmojiCreateAction; module.exports = GuildEmojiCreateAction;

View File

@@ -1,18 +1,18 @@
const Action = require('./Action'); const Action = require('./Action');
class EmojiDeleteAction extends Action { class GuildEmojiDeleteAction extends Action {
handle(data) { handle(emoji) {
const client = this.client; const client = this.client;
client.dataManager.killEmoji(data); client.dataManager.killEmoji(emoji);
return { return {
data, emoji,
}; };
} }
} }
/** /**
* Emitted whenever an emoji is deleted * Emitted whenever a custom guild emoji is deleted
* @event Client#guildEmojiDelete * @event Client#emojiDelete
* @param {Emoji} emoji The emoji that was deleted. * @param {Emoji} emoji The emoji that was deleted.
*/ */
module.exports = EmojiDeleteAction; module.exports = GuildEmojiDeleteAction;

View File

@@ -1,28 +1,14 @@
const Action = require('./Action'); const Action = require('./Action');
class GuildEmojiUpdateAction extends Action { class GuildEmojiUpdateAction extends Action {
handle(data, guild) { handle(oldEmoji, newEmoji) {
const client = this.client; this.client.dataManager.updateEmoji(oldEmoji, newEmoji);
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,
};
} }
} }
/** /**
* Emitted whenever an emoji is updated * Emitted whenever a custom guild emoji is updated
* @event Client#guildEmojiUpdate * @event Client#emojiUpdate
* @param {Emoji} oldEmoji The old emoji * @param {Emoji} oldEmoji The old emoji
* @param {Emoji} newEmoji The new emoji * @param {Emoji} newEmoji The new emoji
*/ */

View File

@@ -17,7 +17,7 @@ class GuildSync extends Action {
if (member) { if (member) {
guild._updateMember(member, syncMember); guild._updateMember(member, syncMember);
} else { } else {
guild._addMember(syncMember); guild._addMember(syncMember, false);
} }
} }
} }

View File

@@ -6,17 +6,25 @@ class MessageCreateAction extends Action {
const client = this.client; const client = this.client;
const channel = client.channels.get((data instanceof Array ? data[0] : data).channel_id); 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) { if (channel) {
const member = channel.guild ? channel.guild.member(user) : null;
if (data instanceof Array) { if (data instanceof Array) {
const messages = new Array(data.length); const messages = new Array(data.length);
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
messages[i] = channel._cacheMessage(new Message(channel, data[i], client)); 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 { return {
messages, messages,
}; };
} else { } else {
const message = channel._cacheMessage(new Message(channel, data, client)); 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 { return {
message, message,
}; };

View File

@@ -0,0 +1,43 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
/*
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', 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;

View File

@@ -0,0 +1,43 @@
const Action = require('./Action');
const Constants = require('../../util/Constants');
/*
{ user_id: 'id',
message_id: 'id',
emoji: { name: '<27>', 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -4,7 +4,7 @@ const Constants = require('../../util/Constants');
function getRoute(url) { function getRoute(url) {
let route = url.split('?')[0]; let route = url.split('?')[0];
if (route.includes('/channels/') || route.includes('/guilds/')) { 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]; const majorID = route.substring(startInd).split('/')[2];
route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID); route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID);
} }
@@ -37,15 +37,11 @@ class APIRequest {
if (this.file && this.file.file) { if (this.file && this.file.file) {
apiRequest.attach('file', this.file.file, this.file.name); apiRequest.attach('file', this.file.file, this.file.name);
this.data = this.data || {}; this.data = this.data || {};
for (const key in this.data) { apiRequest.field('payload_json', JSON.stringify(this.data));
if (this.data[key]) {
apiRequest.field(key, this.data[key]);
}
}
} else if (this.data) { } else if (this.data) {
apiRequest.send(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; return apiRequest;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,9 @@ class BurstRequestHandler extends RequestHandler {
this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000; this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.requestRemaining = Number(res.headers['x-ratelimit-remaining']); this.requestRemaining = Number(res.headers['x-ratelimit-remaining']);
this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); 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) {
if (err.status === 429) { if (err.status === 429) {
@@ -38,10 +40,8 @@ class BurstRequestHandler extends RequestHandler {
this.restManager.client.setTimeout(() => { this.restManager.client.setTimeout(() => {
this.globalLimit = false; this.globalLimit = false;
this.handle(); this.handle();
}, Number(res.headers['retry-after']) + 500); }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
if (res.headers['x-ratelimit-global']) { if (res.headers['x-ratelimit-global']) this.globalLimit = true;
this.globalLimit = true;
}
} else { } else {
item.reject(err); item.reject(err);
} }

View File

@@ -60,10 +60,8 @@ class SequentialRequestHandler extends RequestHandler {
this.waiting = false; this.waiting = false;
this.globalLimit = false; this.globalLimit = false;
resolve(); resolve();
}, Number(res.headers['retry-after']) + 500); }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset);
if (res.headers['x-ratelimit-global']) { if (res.headers['x-ratelimit-global']) this.globalLimit = true;
this.globalLimit = true;
}
} else { } else {
this.queue.shift(); this.queue.shift();
this.waiting = false; this.waiting = false;
@@ -76,10 +74,13 @@ class SequentialRequestHandler extends RequestHandler {
const data = res && res.body ? res.body : {}; const data = res && res.body ? res.body : {};
item.resolve(data); item.resolve(data);
if (this.requestRemaining === 0) { if (this.requestRemaining === 0) {
this.restManager.client.setTimeout(() => { this.restManager.client.setTimeout(
this.waiting = false; () => {
resolve(data); this.waiting = false;
}, (this.requestResetTime - Date.now()) + this.timeDifference + 1000); resolve(data);
},
this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset
);
} else { } else {
this.waiting = false; this.waiting = false;
resolve(data); resolve(data);

View File

@@ -23,7 +23,7 @@ class ClientVoiceManager {
this.connections = new Collection(); this.connections = new Collection();
/** /**
* Pending connection attempts, maps Guild ID to VoiceChannel * Pending connection attempts, maps guild ID to VoiceChannel
* @type {Collection<string, VoiceChannel>} * @type {Collection<string, VoiceChannel>}
*/ */
this.pending = new 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?'); 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')) { 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({ options = mergeDefault({
@@ -79,10 +79,7 @@ class ClientVoiceManager {
joinChannel(channel) { joinChannel(channel) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.'); 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); const existingConnection = this.connections.get(channel.guild.id);
if (existingConnection) { if (existingConnection) {
@@ -142,7 +139,7 @@ class PendingVoiceConnection extends EventEmitter {
/** /**
* An object containing data required to connect to the voice servers with * An object containing data required to connect to the voice servers with
* @type {object} * @type {Object}
*/ */
this.data = {}; this.data = {};

View File

@@ -7,7 +7,7 @@ const EventEmitter = require('events').EventEmitter;
const fs = require('fs'); const fs = require('fs');
/** /**
* Represents a connection to a Voice Channel in Discord. * Represents a connection to a voice channel in Discord.
* ```js * ```js
* // obtained using: * // obtained using:
* voiceChannel.join().then(connection => { * voiceChannel.join().then(connection => {
@@ -17,9 +17,9 @@ const fs = require('fs');
* @extends {EventEmitter} * @extends {EventEmitter}
*/ */
class VoiceConnection extends EventEmitter { class VoiceConnection extends EventEmitter {
constructor(pendingConnection) { constructor(pendingConnection) {
super(); super();
/** /**
* The Voice Manager that instantiated this connection * The Voice Manager that instantiated this connection
* @type {ClientVoiceManager} * @type {ClientVoiceManager}
@@ -46,7 +46,7 @@ class VoiceConnection extends EventEmitter {
/** /**
* The authentication data needed to connect to the voice server * The authentication data needed to connect to the voice server
* @type {object} * @type {Object}
* @private * @private
*/ */
this.authentication = pendingConnection.data; this.authentication = pendingConnection.data;
@@ -70,7 +70,7 @@ class VoiceConnection extends EventEmitter {
/** /**
* Warning info from the connection * Warning info from the connection
* @event VoiceConnection#warn * @event VoiceConnection#warn
* @param {string|error} warning the warning * @param {string|Error} warning the warning
*/ */
this.emit('warn', e); this.emit('warn', e);
this.player.cleanup(); this.player.cleanup();
@@ -83,9 +83,16 @@ class VoiceConnection extends EventEmitter {
*/ */
this.ssrcMap = new Map(); 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 * Object that wraps contains the `ws` and `udp` sockets of this voice connection
* @type {object} * @type {Object}
* @private * @private
*/ */
this.sockets = {}; this.sockets = {};
@@ -106,8 +113,7 @@ class VoiceConnection extends EventEmitter {
speaking: true, speaking: true,
delay: 0, delay: 0,
}, },
}) }).catch(e => {
.catch(e => {
this.emit('debug', e); this.emit('debug', e);
}); });
} }
@@ -155,8 +161,7 @@ class VoiceConnection extends EventEmitter {
this.sockets.udp.findEndpointAddress() this.sockets.udp.findEndpointAddress()
.then(address => { .then(address => {
this.sockets.udp.createUDPSocket(address); this.sockets.udp.createUDPSocket(address);
}) }, e => this.emit('error', e));
.catch(e => this.emit('error', e));
}); });
this.sockets.ws.once('sessionDescription', (mode, secret) => { this.sockets.ws.once('sessionDescription', (mode, secret) => {
this.authentication.encryptionMode = mode; this.authentication.encryptionMode = mode;
@@ -167,6 +172,7 @@ class VoiceConnection extends EventEmitter {
* @event VoiceConnection#ready * @event VoiceConnection#ready
*/ */
this.emit('ready'); this.emit('ready');
this.ready = true;
}); });
this.sockets.ws.on('speaking', data => { this.sockets.ws.on('speaking', data => {
const guild = this.channel.guild; const guild = this.channel.guild;
@@ -253,7 +259,7 @@ class VoiceConnection extends EventEmitter {
*/ */
playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
const options = { seek, volume, passes }; const options = { seek, volume, passes };
return this.player.playPCMStream(stream, options); return this.player.playPCMStream(stream, null, options);
} }
/** /**

View File

@@ -3,18 +3,6 @@ const dns = require('dns');
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
const EventEmitter = require('events').EventEmitter; 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 * Represents a UDP Client for a Voice Connection
* @extends {EventEmitter} * @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; module.exports = VoiceConnectionUDPClient;

View File

@@ -1,8 +1,14 @@
const WebSocket = require('ws');
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
const SecretKey = require('./util/SecretKey'); const SecretKey = require('./util/SecretKey');
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
let WebSocket;
try {
WebSocket = require('uws');
} catch (err) {
WebSocket = require('ws');
}
/** /**
* Represents a Voice Connection's WebSocket * Represents a Voice Connection's WebSocket
* @extends {EventEmitter} * @extends {EventEmitter}

View File

@@ -10,9 +10,7 @@ nonce.fill(0);
* // obtained using: * // obtained using:
* voiceChannel.join().then(connection => { * voiceChannel.join().then(connection => {
* // you can play a file or a stream here: * // you can play a file or a stream here:
* connection.playFile('./file.mp3').then(dispatcher => { * const dispatcher = connection.playFile('./file.mp3');
*
* });
* }); * });
* ``` * ```
* @extends {EventEmitter} * @extends {EventEmitter}
@@ -116,9 +114,10 @@ class StreamDispatcher extends EventEmitter {
/** /**
* Stops the current stream permanently and emits an `end` event. * Stops the current stream permanently and emits an `end` event.
* @param {string} [reason='user'] An optional reason for stopping the dispatcher.
*/ */
end() { end(reason = 'user') {
this._triggerTerminalState('end', 'user requested'); this._triggerTerminalState('end', reason);
} }
_setSpeaking(value) { _setSpeaking(value) {
@@ -136,7 +135,7 @@ class StreamDispatcher extends EventEmitter {
const packet = this._createPacket(sequence, timestamp, this.player.opusEncoder.encode(buffer)); const packet = this._createPacket(sequence, timestamp, this.player.opusEncoder.encode(buffer));
while (repeats--) { while (repeats--) {
this.player.voiceConnection.sockets.udp.send(packet) 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); buffer = this._applyVolume(buffer);
data.count++; 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; data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
this._sendBuffer(buffer, data.sequence, data.timestamp); 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. * Emitted once the stream has ended. Attach a `once` listener to this.
* @event StreamDispatcher#end * @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) { _triggerError(err) {
@@ -282,7 +283,7 @@ class StreamDispatcher extends EventEmitter {
return; 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)); this.stream.on('error', err => this._triggerTerminalState('error', err));
const data = this.streamingData; const data = this.streamingData;

View File

@@ -67,7 +67,14 @@ class FfmpegConverterEngine extends ConverterEngine {
} }
function chooseCommand() { 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; if (!ChildProcess.spawnSync(cmd, ['-h']).error) return cmd;
} }
throw new Error( throw new Error(

View File

@@ -1,9 +1,30 @@
const WebSocket = require('ws'); const browser = typeof window !== 'undefined';
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
const convertArrayBuffer = require('../../util/ConvertArrayBuffer');
const pako = require('pako');
const zlib = require('zlib'); const zlib = require('zlib');
const PacketManager = require('./packets/WebSocketPacketManager'); 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 * The WebSocket Manager of the Client
* @private * @private
@@ -64,9 +85,11 @@ class WebSocketManager extends EventEmitter {
* @type {Object} * @type {Object}
*/ */
this.disabledEvents = {}; 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.first = true;
this.lastHeartbeatAck = true;
} }
/** /**
@@ -78,15 +101,21 @@ class WebSocketManager extends EventEmitter {
this.normalReady = false; this.normalReady = false;
if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING; if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING;
this.ws = new WebSocket(gateway); this.ws = new WebSocket(gateway);
this.ws.onopen = () => this.eventOpen(); if (browser) this.ws.binaryType = 'arraybuffer';
this.ws.onclose = (d) => this.eventClose(d); this.ws.onopen = this.eventOpen.bind(this);
this.ws.onmessage = (e) => this.eventMessage(e); this.ws.onmessage = this.eventMessage.bind(this);
this.ws.onerror = (e) => this.eventError(e); this.ws.onclose = this.eventClose.bind(this);
this.ws.onerror = this.eventError.bind(this);
this._queue = []; this._queue = [];
this._remaining = 3; this._remaining = 120;
this.client.setInterval(() => {
this._remaining = 120;
this._remainingReset = Date.now();
}, 60e3);
} }
connect(gateway) { connect(gateway) {
gateway = `${gateway}&encoding=${erlpack ? 'etf' : 'json'}`;
if (this.first) { if (this.first) {
this._connect(gateway); this._connect(gateway);
this.first = false; 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 * Sends a packet to the gateway
* @param {Object} data An object that can be JSON stringified * @param {Object} data An object that can be JSON stringified
@@ -102,10 +147,10 @@ class WebSocketManager extends EventEmitter {
*/ */
send(data, force = false) { send(data, force = false) {
if (force) { if (force) {
this._send(JSON.stringify(data)); this._send(serialize(data));
return; return;
} }
this._queue.push(JSON.stringify(data)); this._queue.push(serialize(data));
this.doQueue(); this.doQueue();
} }
@@ -124,19 +169,15 @@ class WebSocketManager extends EventEmitter {
doQueue() { doQueue() {
const item = this._queue[0]; const item = this._queue[0];
if (this.ws.readyState === WebSocket.OPEN && item) { if (!(this.ws.readyState === WebSocket.OPEN && item)) return;
if (this._remaining === 0) { if (this.remaining === 0) {
this.client.setTimeout(() => { this.client.setTimeout(this.doQueue.bind(this), Date.now() - this.remainingReset);
this.doQueue(); return;
}, 1000);
return;
}
this._remaining--;
this._send(item);
this._queue.shift();
this.doQueue();
this.client.setTimeout(() => this._remaining++, 1000);
} }
this._remaining--;
this._send(item);
this._queue.shift();
this.doQueue();
} }
/** /**
@@ -144,6 +185,7 @@ class WebSocketManager extends EventEmitter {
*/ */
eventOpen() { eventOpen() {
this.client.emit('debug', 'Connection to gateway opened'); this.client.emit('debug', 'Connection to gateway opened');
this.lastHeartbeatAck = true;
if (this.status === Constants.Status.RECONNECTING) this._sendResume(); if (this.status === Constants.Status.RECONNECTING) this._sendResume();
else this._sendNewIdentify(); else this._sendNewIdentify();
} }
@@ -187,18 +229,26 @@ class WebSocketManager extends EventEmitter {
this.sequence = -1; 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. * 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) { eventClose(event) {
this.emit('close', 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 * Emitted whenever the client websocket is disconnected
* @event Client#disconnect * @event Client#disconnect
* @param {CloseEvent} event The WebSocket close event
*/ */
clearInterval(this.client.manager.heartbeatInterval); if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT, event);
if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT);
if (event.code === 4004) return; if (event.code === 4004) return;
if (event.code === 4010) return; if (event.code === 4010) return;
if (!this.reconnecting && event.code !== 1000) this.tryReconnect(); if (!this.reconnecting && event.code !== 1000) this.tryReconnect();
@@ -211,18 +261,45 @@ class WebSocketManager extends EventEmitter {
* @returns {boolean} * @returns {boolean}
*/ */
eventMessage(event) { eventMessage(event) {
let packet; const data = this.tryParseEventData(event.data);
try { if (data === null) {
if (event.binary) event.data = zlib.inflateSync(event.data).toString(); this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE));
packet = JSON.parse(event.data); return false;
} catch (e) {
return this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE));
} }
this.client.emit('raw', packet); this.client.emit('raw', data);
if (packet.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval); if (data.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(data.d.heartbeat_interval);
return this.packetManager.handle(packet); 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; this.status = Constants.Status.NEARLY;
if (this.client.options.fetchAllMembers) { if (this.client.options.fetchAllMembers) {
const promises = this.client.guilds.map(g => g.fetchMembers()); 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.WARN, 'Error in pre-ready guild member fetching');
this.client.emit(Constants.Events.ERROR, e); this.client.emit(Constants.Events.ERROR, e);
this._emitReady(); this._emitReady();
@@ -280,6 +357,7 @@ class WebSocketManager extends EventEmitter {
* Tries to reconnect the client, changing the status to Constants.Status.RECONNECTING. * Tries to reconnect the client, changing the status to Constants.Status.RECONNECTING.
*/ */
tryReconnect() { tryReconnect() {
if (this.status === Constants.Status.RECONNECTING || this.status === Constants.Status.CONNECTING) return;
this.status = Constants.Status.RECONNECTING; this.status = Constants.Status.RECONNECTING;
this.ws.close(); this.ws.close();
this.packetManager.handleQueue(); this.packetManager.handleQueue();

View File

@@ -15,43 +15,47 @@ class WebSocketPacketManager {
this.handlers = {}; this.handlers = {};
this.queue = []; this.queue = [];
this.register(Constants.WSEvents.READY, 'Ready'); this.register(Constants.WSEvents.READY, require('./handlers/Ready'));
this.register(Constants.WSEvents.GUILD_CREATE, 'GuildCreate'); this.register(Constants.WSEvents.GUILD_CREATE, require('./handlers/GuildCreate'));
this.register(Constants.WSEvents.GUILD_DELETE, 'GuildDelete'); this.register(Constants.WSEvents.GUILD_DELETE, require('./handlers/GuildDelete'));
this.register(Constants.WSEvents.GUILD_UPDATE, 'GuildUpdate'); this.register(Constants.WSEvents.GUILD_UPDATE, require('./handlers/GuildUpdate'));
this.register(Constants.WSEvents.GUILD_BAN_ADD, 'GuildBanAdd'); this.register(Constants.WSEvents.GUILD_BAN_ADD, require('./handlers/GuildBanAdd'));
this.register(Constants.WSEvents.GUILD_BAN_REMOVE, 'GuildBanRemove'); this.register(Constants.WSEvents.GUILD_BAN_REMOVE, require('./handlers/GuildBanRemove'));
this.register(Constants.WSEvents.GUILD_MEMBER_ADD, 'GuildMemberAdd'); this.register(Constants.WSEvents.GUILD_MEMBER_ADD, require('./handlers/GuildMemberAdd'));
this.register(Constants.WSEvents.GUILD_MEMBER_REMOVE, 'GuildMemberRemove'); this.register(Constants.WSEvents.GUILD_MEMBER_REMOVE, require('./handlers/GuildMemberRemove'));
this.register(Constants.WSEvents.GUILD_MEMBER_UPDATE, 'GuildMemberUpdate'); this.register(Constants.WSEvents.GUILD_MEMBER_UPDATE, require('./handlers/GuildMemberUpdate'));
this.register(Constants.WSEvents.GUILD_ROLE_CREATE, 'GuildRoleCreate'); this.register(Constants.WSEvents.GUILD_ROLE_CREATE, require('./handlers/GuildRoleCreate'));
this.register(Constants.WSEvents.GUILD_ROLE_DELETE, 'GuildRoleDelete'); this.register(Constants.WSEvents.GUILD_ROLE_DELETE, require('./handlers/GuildRoleDelete'));
this.register(Constants.WSEvents.GUILD_ROLE_UPDATE, 'GuildRoleUpdate'); this.register(Constants.WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate'));
this.register(Constants.WSEvents.GUILD_MEMBERS_CHUNK, 'GuildMembersChunk'); this.register(Constants.WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate'));
this.register(Constants.WSEvents.CHANNEL_CREATE, 'ChannelCreate'); this.register(Constants.WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk'));
this.register(Constants.WSEvents.CHANNEL_DELETE, 'ChannelDelete'); this.register(Constants.WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate'));
this.register(Constants.WSEvents.CHANNEL_UPDATE, 'ChannelUpdate'); this.register(Constants.WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete'));
this.register(Constants.WSEvents.PRESENCE_UPDATE, 'PresenceUpdate'); this.register(Constants.WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate'));
this.register(Constants.WSEvents.USER_UPDATE, 'UserUpdate'); this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate'));
this.register(Constants.WSEvents.VOICE_STATE_UPDATE, 'VoiceStateUpdate'); this.register(Constants.WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate'));
this.register(Constants.WSEvents.TYPING_START, 'TypingStart'); this.register(Constants.WSEvents.USER_UPDATE, require('./handlers/UserUpdate'));
this.register(Constants.WSEvents.MESSAGE_CREATE, 'MessageCreate'); this.register(Constants.WSEvents.USER_NOTE_UPDATE, require('./handlers/UserNoteUpdate'));
this.register(Constants.WSEvents.MESSAGE_DELETE, 'MessageDelete'); this.register(Constants.WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate'));
this.register(Constants.WSEvents.MESSAGE_UPDATE, 'MessageUpdate'); this.register(Constants.WSEvents.TYPING_START, require('./handlers/TypingStart'));
this.register(Constants.WSEvents.VOICE_SERVER_UPDATE, 'VoiceServerUpdate'); this.register(Constants.WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate'));
this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk'); this.register(Constants.WSEvents.MESSAGE_DELETE, require('./handlers/MessageDelete'));
this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate'); this.register(Constants.WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate'));
this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync'); this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk'));
this.register(Constants.WSEvents.RELATIONSHIP_ADD, 'RelationshipAdd'); this.register(Constants.WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate'));
this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, 'RelationshipRemove'); 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() { get client() {
return this.ws.client; return this.ws.client;
} }
register(event, handle) { register(event, Handler) {
const Handler = require(`./handlers/${handle}`);
this.handlers[event] = new Handler(this); this.handlers[event] = new Handler(this);
} }
@@ -74,12 +78,28 @@ class WebSocketPacketManager {
} }
if (packet.op === Constants.OPCodes.INVALID_SESSION) { if (packet.op === Constants.OPCodes.INVALID_SESSION) {
this.ws.sessionID = null; if (packet.d) {
this.ws._sendNewIdentify(); setTimeout(() => {
this.ws._sendResume();
}, 2500);
} else {
this.ws.sessionID = null;
this.ws._sendNewIdentify();
}
return false; 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) { if (this.ws.status === Constants.Status.RECONNECTING) {
this.ws.reconnecting = false; this.ws.reconnecting = false;

View File

@@ -9,7 +9,7 @@ class ChannelCreateHandler extends AbstractHandler {
} }
/** /**
* Emitted whenever a Channel is created. * Emitted whenever a channel is created.
* @event Client#channelCreate * @event Client#channelCreate
* @param {Channel} channel The channel that was created * @param {Channel} channel The channel that was created
*/ */

View File

@@ -12,7 +12,7 @@ class ChannelDeleteHandler extends AbstractHandler {
} }
/** /**
* Emitted whenever a Channel is deleted. * Emitted whenever a channel is deleted.
* @event Client#channelDelete * @event Client#channelDelete
* @param {Channel} channel The channel that was deleted * @param {Channel} channel The channel that was deleted
*/ */

View File

@@ -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. * can be provided easily here - you need to manually check the pins yourself.
* @event Client#channelPinsUpdate * @event Client#channelPinsUpdate
* @param {Channel} channel The channel that the pins update occured in * @param {Channel} channel The channel that the pins update occured in

View File

@@ -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 * @event Client#guildDelete
* @param {Guild} guild The guild that was deleted * @param {Guild} guild The guild that was deleted
*/ */

View File

@@ -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;

View File

@@ -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;

View File

@@ -8,19 +8,19 @@ class GuildMembersChunkHandler extends AbstractHandler {
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d; const data = packet.d;
const guild = client.guilds.get(data.guild_id); const guild = client.guilds.get(data.guild_id);
const members = []; if (!guild) return;
if (guild) { const members = data.members.map(member => guild._addMember(member, false));
for (const member of data.members) members.push(guild._addMember(member, false));
}
guild._checkChunks(); guild._checkChunks();
client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members); 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 * @event Client#guildMembersChunk
* @param {GuildMember[]} members The members in the chunk * @param {GuildMember[]} members The members in the chunk
*/ */

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 * @event Client#guildMemberAvailable
* @param {GuildMember} member The member that became available * @param {GuildMember} member The member that became available
*/ */

View File

@@ -1,13 +1,14 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
const getStructure = name => require(`../../../../structures/${name}`); const ClientUser = require('../../../../structures/ClientUser');
const ClientUser = getStructure('ClientUser');
class ReadyHandler extends AbstractHandler { class ReadyHandler extends AbstractHandler {
handle(packet) { handle(packet) {
const client = this.packetManager.client; const client = this.packetManager.client;
const data = packet.d; const data = packet.d;
client.ws.heartbeat();
const clientUser = new ClientUser(client, data.user); const clientUser = new ClientUser(client, data.user);
client.user = clientUser; client.user = clientUser;
client.readyAt = new Date(); client.readyAt = new Date();
@@ -31,6 +32,15 @@ class ReadyHandler extends AbstractHandler {
client._setPresence(presence.user.id, presence); 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); if (!client.user.bot && client.options.sync) client.setInterval(client.syncGuilds.bind(client), 30000);
client.once('ready', client.syncGuilds.bind(client)); client.once('ready', client.syncGuilds.bind(client));

View File

@@ -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;

View File

@@ -11,6 +11,7 @@ module.exports = {
fetchRecommendedShards: require('./util/FetchRecommendedShards'), fetchRecommendedShards: require('./util/FetchRecommendedShards'),
Channel: require('./structures/Channel'), Channel: require('./structures/Channel'),
ClientOAuth2Application: require('./structures/ClientOAuth2Application'),
ClientUser: require('./structures/ClientUser'), ClientUser: require('./structures/ClientUser'),
DMChannel: require('./structures/DMChannel'), DMChannel: require('./structures/DMChannel'),
Emoji: require('./structures/Emoji'), Emoji: require('./structures/Emoji'),
@@ -25,10 +26,14 @@ module.exports = {
MessageAttachment: require('./structures/MessageAttachment'), MessageAttachment: require('./structures/MessageAttachment'),
MessageCollector: require('./structures/MessageCollector'), MessageCollector: require('./structures/MessageCollector'),
MessageEmbed: require('./structures/MessageEmbed'), MessageEmbed: require('./structures/MessageEmbed'),
MessageReaction: require('./structures/MessageReaction'),
OAuth2Application: require('./structures/OAuth2Application'),
PartialGuild: require('./structures/PartialGuild'), PartialGuild: require('./structures/PartialGuild'),
PartialGuildChannel: require('./structures/PartialGuildChannel'), PartialGuildChannel: require('./structures/PartialGuildChannel'),
PermissionOverwrites: require('./structures/PermissionOverwrites'), PermissionOverwrites: require('./structures/PermissionOverwrites'),
Presence: require('./structures/Presence').Presence, Presence: require('./structures/Presence').Presence,
ReactionEmoji: require('./structures/ReactionEmoji'),
RichEmbed: require('./structures/RichEmbed'),
Role: require('./structures/Role'), Role: require('./structures/Role'),
TextChannel: require('./structures/TextChannel'), TextChannel: require('./structures/TextChannel'),
User: require('./structures/User'), User: require('./structures/User'),
@@ -36,4 +41,7 @@ module.exports = {
Webhook: require('./structures/Webhook'), Webhook: require('./structures/Webhook'),
version: require('../package').version, version: require('../package').version,
Constants: require('./util/Constants'),
}; };
if (typeof window !== 'undefined') window.Discord = module.exports; // eslint-disable-line no-undef

View File

@@ -10,7 +10,7 @@ class Shard {
/** /**
* @param {ShardingManager} manager The sharding manager * @param {ShardingManager} manager The sharding manager
* @param {number} id The ID of this shard * @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 = []) { constructor(manager, id, args = []) {
/** /**
@@ -134,21 +134,29 @@ class Shard {
if (message) { if (message) {
// Shard is requesting a property fetch // Shard is requesting a property fetch
if (message._sFetchProp) { if (message._sFetchProp) {
this.manager.fetchClientValues(message._sFetchProp) this.manager.fetchClientValues(message._sFetchProp).then(
.then(results => this.send({ _sFetchProp: message._sFetchProp, _result: results })) results => this.send({ _sFetchProp: message._sFetchProp, _result: results }),
.catch(err => this.send({ _sFetchProp: message._sFetchProp, _error: makePlainError(err) })); err => this.send({ _sFetchProp: message._sFetchProp, _error: makePlainError(err) })
);
return; return;
} }
// Shard is requesting an eval broadcast // Shard is requesting an eval broadcast
if (message._sEval) { if (message._sEval) {
this.manager.broadcastEval(message._sEval) this.manager.broadcastEval(message._sEval).then(
.then(results => this.send({ _sEval: message._sEval, _result: results })) results => this.send({ _sEval: message._sEval, _result: results }),
.catch(err => this.send({ _sEval: message._sEval, _error: makePlainError(err) })); err => this.send({ _sEval: message._sEval, _error: makePlainError(err) })
);
return; 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); this.manager.emit('message', this, message);
} }
} }

View File

@@ -119,21 +119,22 @@ class ShardClientUtil {
* @private * @private
*/ */
_respond(type, message) { _respond(type, message) {
this.send(message).catch(err => this.send(message).catch(err => {
this.client.emit('error', `Error when sending ${type} response to master process: ${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 * Creates/gets the singleton of this class
* @param {Client} client Client to use * @param {Client} client Client to use
* @returns {ShardUtil} * @returns {ShardClientUtil}
*/ */
static singleton(client) { static singleton(client) {
if (!this._singleton) { if (!this._singleton) {
this._singleton = new this(client); this._singleton = new this(client);
} else { } 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; return this._singleton;
} }

View File

@@ -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 * 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. * 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. * If you do not select an amount of shards, the manager will automatically decide the best amount.
* <warn>The Sharding Manager is still experimental</warn>
* @extends {EventEmitter} * @extends {EventEmitter}
*/ */
class ShardingManager extends EventEmitter { class ShardingManager extends EventEmitter {
@@ -105,19 +104,17 @@ class ShardingManager extends EventEmitter {
* @returns {Promise<Collection<number, Shard>>} * @returns {Promise<Collection<number, Shard>>}
*/ */
spawn(amount = this.totalShards, delay = 5500) { spawn(amount = this.totalShards, delay = 5500) {
return new Promise((resolve, reject) => { if (amount === 'auto') {
if (amount === 'auto') { return fetchRecommendedShards(this.token).then(count => {
fetchRecommendedShards(this.token).then(count => { this.totalShards = count;
this.totalShards = count; return this._spawn(count, delay);
resolve(this._spawn(count, delay)); });
}).catch(reject); } else {
} else { if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
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 < 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.');
if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.'); return this._spawn(amount, delay);
resolve(this._spawn(amount, delay)); }
}
});
} }
/** /**

View File

@@ -1,14 +1,15 @@
/** /**
* Represents any Channel on Discord * Represents any channel on Discord
*/ */
class Channel { class Channel {
constructor(client, data) { constructor(client, data) {
/** /**
* The client that instantiated the Channel * The client that instantiated the Channel
* @name Channel#client
* @type {Client} * @type {Client}
* @readonly
*/ */
this.client = client; Object.defineProperty(this, 'client', { value: client });
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
/** /**
* The type of the channel, either: * The type of the channel, either:

View File

@@ -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;

View File

@@ -2,7 +2,7 @@ const User = require('./User');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
/** /**
* Represents the logged in client's Discord User * Represents the logged in client's Discord user
* @extends {User} * @extends {User}
*/ */
class ClientUser extends User { class ClientUser extends User {
@@ -25,17 +25,24 @@ class ClientUser extends User {
/** /**
* A Collection of friends for the logged in user. * A Collection of friends for the logged in user.
* <warn>This is only filled for user accounts, not bot accounts!</warn> * <warn>This is only filled when using a user account.</warn>
* @type {Collection<string, User>} * @type {Collection<string, User>}
*/ */
this.friends = new Collection(); this.friends = new Collection();
/** /**
* A Collection of blocked users for the logged in user. * A Collection of blocked users for the logged in user.
* <warn>This is only filled for user accounts, not bot accounts!</warn> * <warn>This is only filled when using a user account.</warn>
* @type {Collection<string, User>} * @type {Collection<string, User>}
*/ */
this.blocked = new Collection(); this.blocked = new Collection();
/**
* A Collection of notes for the logged in user.
* <warn>This is only filled when using a user account.</warn>
* @type {Collection<string, string>}
*/
this.notes = new Collection();
} }
edit(data) { edit(data) {
@@ -47,6 +54,7 @@ class ClientUser extends User {
* <info>Changing usernames in Discord is heavily rate limited, with only 2 requests * <info>Changing usernames in Discord is heavily rate limited, with only 2 requests
* every hour. Use this sparingly!</info> * every hour. Use this sparingly!</info>
* @param {string} username The new username * @param {string} username The new username
* @param {string} [password] Current password (only for user accounts)
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
* @example * @example
* // set username * // set username
@@ -54,43 +62,45 @@ class ClientUser extends User {
* .then(user => console.log(`My new username is ${user.username}`)) * .then(user => console.log(`My new username is ${user.username}`))
* .catch(console.error); * .catch(console.error);
*/ */
setUsername(username) { setUsername(username, password) {
return this.client.rest.methods.updateCurrentUser({ username }); 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 * Changes the email for the client user's account.
* email here. * <warn>This is only available when using a user account.</warn>
* @param {string} email The new email * @param {string} email New email to change to
* @param {string} password Current password
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
* @example * @example
* // set email * // 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}`)) * .then(user => console.log(`My new email is ${user.email}`))
* .catch(console.error); * .catch(console.error);
*/ */
setEmail(email) { setEmail(email, password) {
return this.client.rest.methods.updateCurrentUser({ email }); 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 * Changes the password for the client user's account.
* password here. * <warn>This is only available when using a user account.</warn>
* @param {string} password The new password * @param {string} newPassword New password to change to
* @param {string} oldPassword Current password
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
* @example * @example
* // set password * // 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!')) * .then(user => console.log('New password set!'))
* .catch(console.error); * .catch(console.error);
*/ */
setPassword(password) { setPassword(newPassword, oldPassword) {
return this.client.rest.methods.updateCurrentUser({ password }); return this.client.rest.methods.updateCurrentUser({ password: newPassword }, oldPassword);
} }
/** /**
* Set the avatar of the logged in Client. * Set the avatar of the logged in Client.
* @param {FileResolvable|Base64Resolveable} avatar The new avatar * @param {BufferResolvable|Base64Resolvable} avatar The new avatar
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
* @example * @example
* // set avatar * // set avatar
@@ -99,94 +109,28 @@ class ClientUser extends User {
* .catch(console.error); * .catch(console.error);
*/ */
setAvatar(avatar) { setAvatar(avatar) {
return new Promise(resolve => { if (avatar.startsWith('data:')) {
if (avatar.startsWith('data:')) { return this.client.rest.methods.updateCurrentUser({ avatar });
resolve(this.client.rest.methods.updateCurrentUser({ avatar })); } else {
} else { return this.client.resolver.resolveBuffer(avatar).then(data =>
this.client.resolver.resolveFile(avatar).then(data => { this.client.rest.methods.updateCurrentUser({ avatar: data })
resolve(this.client.rest.methods.updateCurrentUser({ avatar: data })); );
}); }
}
});
} }
/** /**
* Set the status of the logged in user. * Data resembling a raw Discord presence
* @param {string} status can be `online`, `idle`, `invisible` or `dnd` (do not disturb) * @typedef {Object} PresenceData
* @returns {Promise<ClientUser>} * @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. * Sets the full presence of the client user.
* @param {string} game the game being played * @param {PresenceData} data Data for the presence
* @param {string} [streamingURL] an optional URL to a twitch stream, if one is available.
* @returns {Promise<ClientUser>}
*/
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<ClientUser>}
*/
setAFK(afk) {
return this.setPresence({ afk });
}
/**
* Send a friend request
* <warn>This is only available for user accounts, not bot accounts!</warn>
* @param {UserResolvable} user The user to send the friend request to.
* @returns {Promise<User>} 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
* <warn>This is only available for user accounts, not bot accounts!</warn>
* @param {UserResolvable} user The user to remove from your friends
* @returns {Promise<User>} The user that was removed
*/
removeFriend(user) {
user = this.client.resolver.resolveUser(user);
return this.client.rest.methods.removeFriend(user);
}
/**
* Creates a guild
* <warn>This is only available for user accounts, not bot accounts!</warn>
* @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<Guild>} 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
* @returns {Promise<ClientUser>} * @returns {Promise<ClientUser>}
*/ */
setPresence(data) { setPresence(data) {
@@ -231,6 +175,100 @@ class ClientUser extends User {
resolve(this); 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<ClientUser>}
*/
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<ClientUser>}
*/
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<ClientUser>}
*/
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<Message[]>}
*/
fetchMentions(options = { limit: 25, roles: true, everyone: true, guild: null }) {
return this.client.rest.methods.fetchMentions(options);
}
/**
* Send a friend request
* <warn>This is only available when using a user account.</warn>
* @param {UserResolvable} user The user to send the friend request to.
* @returns {Promise<User>} The user the friend request was sent to.
*/
addFriend(user) {
user = this.client.resolver.resolveUser(user);
return this.client.rest.methods.addFriend(user);
}
/**
* Remove a friend
* <warn>This is only available when using a user account.</warn>
* @param {UserResolvable} user The user to remove from your friends
* @returns {Promise<User>} The user that was removed
*/
removeFriend(user) {
user = this.client.resolver.resolveUser(user);
return this.client.rest.methods.removeFriend(user);
}
/**
* Creates a guild
* <warn>This is only available when using a user account.</warn>
* @param {string} name The name of the guild
* @param {string} region The region for the server
* @param {BufferResolvable|Base64Resolvable} [icon=null] The icon for the guild
* @returns {Promise<Guild>} 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; module.exports = ClientUser;

View File

@@ -3,7 +3,7 @@ const TextBasedChannel = require('./interface/TextBasedChannel');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
/** /**
* Represents a Direct Message Channel between two users. * Represents a direct message channel between two users.
* @extends {Channel} * @extends {Channel}
* @implements {TextBasedChannel} * @implements {TextBasedChannel}
*/ */
@@ -37,8 +37,9 @@ class DMChannel extends Channel {
} }
// These are here only for documentation purposes - they are implemented by TextBasedChannel // These are here only for documentation purposes - they are implemented by TextBasedChannel
send() { return; }
sendMessage() { return; } sendMessage() { return; }
sendTTSMessage() { return; } sendEmbed() { return; }
sendFile() { return; } sendFile() { return; }
sendCode() { return; } sendCode() { return; }
fetchMessage() { return; } fetchMessage() { return; }

View File

@@ -2,19 +2,20 @@ const Constants = require('../util/Constants');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
/** /**
* Represents a Custom Emoji * Represents a custom emoji
*/ */
class Emoji { class Emoji {
constructor(guild, data) { constructor(guild, data) {
/** /**
* The Client that instantiated this object * The Client that instantiated this object
* @name Emoji#client
* @type {Client} * @type {Client}
* @readonly
*/ */
this.client = guild.client; Object.defineProperty(this, 'client', { value: guild.client });
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
/** /**
* The Guild this emoji is part of * The guild this emoji is part of
* @type {Guild} * @type {Guild}
*/ */
this.guild = guild; this.guild = guild;
@@ -24,13 +25,13 @@ class Emoji {
setup(data) { setup(data) {
/** /**
* The ID of the Emoji * The ID of the emoji
* @type {string} * @type {string}
*/ */
this.id = data.id; this.id = data.id;
/** /**
* The name of the Emoji * The name of the emoji
* @type {string} * @type {string}
*/ */
this.name = data.name; this.name = data.name;
@@ -87,7 +88,7 @@ class Emoji {
* @readonly * @readonly
*/ */
get url() { get url() {
return `${Constants.Endpoints.CDN}/emojis/${this.id}.png`; return Constants.Endpoints.emoji(this.id);
} }
/** /**
@@ -101,6 +102,39 @@ class Emoji {
toString() { toString() {
return this.requiresColons ? `<:${this.name}:${this.id}>` : this.name; 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; module.exports = Emoji;

View File

@@ -57,7 +57,7 @@ class EvaluatedPermissions {
* Checks whether the user has all specified permissions, and lists any missing permissions. * Checks whether the user has all specified permissions, and lists any missing permissions.
* @param {PermissionResolvable[]} permissions The permissions to check for * @param {PermissionResolvable[]} permissions The permissions to check for
* @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions
* @returns {array} * @returns {PermissionResolvable[]}
*/ */
missingPermissions(permissions, explicit = false) { missingPermissions(permissions, explicit = false) {
return permissions.filter(p => !this.hasPermission(p, explicit)); return permissions.filter(p => !this.hasPermission(p, explicit));

View File

@@ -1,7 +1,6 @@
const Channel = require('./Channel'); const Channel = require('./Channel');
const TextBasedChannel = require('./interface/TextBasedChannel'); const TextBasedChannel = require('./interface/TextBasedChannel');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const arraysEqual = require('../util/ArraysEqual');
/* /*
{ type: 3, { type: 3,
@@ -90,7 +89,7 @@ class GroupDMChannel extends Channel {
* Whether this channel equals another channel. It compares all properties, so for most operations * 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 * it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often
* what most users need. * what most users need.
* @param {GroupDMChannel} channel The channel to compare to * @param {GroupDMChannel} channel Channel to compare with
* @returns {boolean} * @returns {boolean}
*/ */
equals(channel) { equals(channel) {
@@ -101,16 +100,14 @@ class GroupDMChannel extends Channel {
this.ownerID === channel.ownerID; this.ownerID === channel.ownerID;
if (equal) { if (equal) {
const thisIDs = this.recipients.keyArray(); return this.recipients.equals(channel.recipients);
const otherIDs = channel.recipients.keyArray();
return arraysEqual(thisIDs, otherIDs);
} }
return equal; 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} * @returns {string}
* @example * @example
* // logs: Hello from My Group DM! * // 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 // These are here only for documentation purposes - they are implemented by TextBasedChannel
send() { return; }
sendMessage() { return; } sendMessage() { return; }
sendTTSMessage() { return; } sendEmbed() { return; }
sendFile() { return; } sendFile() { return; }
sendCode() { return; } sendCode() { return; }
fetchMessage() { return; } fetchMessage() { return; }

View File

@@ -9,7 +9,7 @@ const cloneObject = require('../util/CloneObject');
const arraysEqual = require('../util/ArraysEqual'); const arraysEqual = require('../util/ArraysEqual');
/** /**
* Represents a Guild (or a Server) on Discord. * Represents a guild (or a server) on Discord.
* <info>It's recommended to see if a guild is available before performing operations or reading data from it. You can * <info>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`.</info> * check this with `guild.available`.</info>
*/ */
@@ -17,33 +17,40 @@ class Guild {
constructor(client, data) { constructor(client, data) {
/** /**
* The Client that created the instance of the the Guild. * The Client that created the instance of the the Guild.
* @name Guild#client
* @type {Client} * @type {Client}
* @readonly
*/ */
this.client = client; Object.defineProperty(this, 'client', { value: client });
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
/** /**
* 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<string, GuildMember>} * @type {Collection<string, GuildMember>}
*/ */
this.members = new 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<string, GuildChannel>} * @type {Collection<string, GuildChannel>}
*/ */
this.channels = new 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<string, Role>} * @type {Collection<string, Role>}
*/ */
this.roles = new Collection(); this.roles = new Collection();
/**
* A collection of presences in this guild
* @type {Collection<string, Presence>}
*/
this.presences = new Collection();
if (!data) return; if (!data) return;
if (data.unavailable) { 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} * @type {boolean}
*/ */
this.available = false; this.available = false;
@@ -90,7 +97,7 @@ class Guild {
this.region = data.region; 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} * @type {number}
*/ */
this.memberCount = data.member_count || this.memberCount; this.memberCount = data.member_count || this.memberCount;
@@ -101,12 +108,6 @@ class Guild {
*/ */
this.large = data.large || this.large; this.large = data.large || this.large;
/**
* A collection of presences in this Guild
* @type {Collection<string, Presence>}
*/
this.presences = new Collection();
/** /**
* An array of guild features. * An array of guild features.
* @type {Object[]} * @type {Object[]}
@@ -114,7 +115,13 @@ class Guild {
this.features = data.features; 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<string, Emoji>} * @type {Collection<string, Emoji>}
*/ */
this.emojis = new 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} * @type {GuildMember}
* @readonly * @readonly
*/ */
@@ -256,6 +273,7 @@ class Guild {
* @readonly * @readonly
*/ */
get voiceConnection() { get voiceConnection() {
if (this.client.browser) return null;
return this.client.voice.connections.get(this.id) || 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 * @param {UserResolvable} user The user that you want to obtain the GuildMember of
* @returns {?GuildMember} * @returns {?GuildMember}
* @example * @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<Collection<string, User>>} * @returns {Promise<Collection<string, User>>}
*/ */
fetchBans() { 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<Collection<string, Invite>>} * @returns {Promise<Collection<string, Invite>>}
*/ */
fetchInvites() { 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. * this should not be necessary.
* @param {string} [query=''] An optional query to provide when fetching members * @param {string} [query=''] An optional query to provide when fetching members
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
@@ -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. * Updates the Guild with new information - e.g. a new name.
* @param {GuildEditData} data The data to update the guild with * @param {GuildEditData} data The data to update the guild with
@@ -362,8 +393,8 @@ class Guild {
} }
/** /**
* Edit the name of the Guild. * Edit the name of the guild.
* @param {string} name The new name of the Guild * @param {string} name The new name of the guild
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild name * // edit the guild name
@@ -376,8 +407,8 @@ class Guild {
} }
/** /**
* Edit the region of the Guild. * Edit the region of the guild.
* @param {Region} region The new region of the guild. * @param {string} region The new region of the guild.
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild region * // edit the guild region
@@ -390,8 +421,8 @@ class Guild {
} }
/** /**
* Edit the verification level of the Guild. * Edit the verification level of the guild.
* @param {VerificationLevel} verificationLevel The new verification level of the guild * @param {number} verificationLevel The new verification level of the guild
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild verification level * // edit the guild verification level
@@ -404,8 +435,8 @@ class Guild {
} }
/** /**
* Edit the AFK channel of the Guild. * Edit the AFK channel of the guild.
* @param {GuildChannelResolvable} afkChannel The new AFK channel * @param {ChannelResolvable} afkChannel The new AFK channel
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild AFK channel * // 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 * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @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 * @param {Base64Resolvable} icon The new icon of the guild
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
@@ -446,8 +477,8 @@ class Guild {
} }
/** /**
* Sets a new owner of the Guild. * Sets a new owner of the guild.
* @param {GuildMemberResolvable} owner The new owner of the Guild * @param {GuildMemberResolvable} owner The new owner of the guild
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @example
* // edit the guild owner * // 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 * @param {Base64Resolvable} splash The new splash screen of the guild
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
* @example * @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 * @param {UserResolvable} user The user to unban
* @returns {Promise<User>} * @returns {Promise<User>}
* @example * @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).
* <warn>This is only available when using a user account.</warn>
*/ */
sync() { sync() {
if (!this.client.user.bot) this.client.syncGuilds([this]); 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} name The name of the new channel
* @param {string} type The type of the new channel, either `text` or `voice` * @param {string} type The type of the new channel, either `text` or `voice`
* @param {Array<PermissionOverwrites|Object>} overwrites Permission overwrites to apply to the new channel
* @returns {Promise<TextChannel|VoiceChannel>} * @returns {Promise<TextChannel|VoiceChannel>}
* @example * @example
* // create a new text channel * // create a new text channel
@@ -542,8 +575,8 @@ class Guild {
* .then(channel => console.log(`Created new channel ${channel}`)) * .then(channel => console.log(`Created new channel ${channel}`))
* .catch(console.error); * .catch(console.error);
*/ */
createChannel(name, type) { createChannel(name, type, overwrites) {
return this.client.rest.methods.createChannel(this, name, type); return this.client.rest.methods.createChannel(this, name, type, overwrites);
} }
/** /**
@@ -569,7 +602,7 @@ class Guild {
/** /**
* Creates a new custom emoji in the 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. * @param {string} name The name for the emoji.
* @returns {Promise<Emoji>} The created emoji. * @returns {Promise<Emoji>} The created emoji.
* @example * @example
@@ -584,13 +617,14 @@ class Guild {
* .catch(console.error); * .catch(console.error);
*/ */
createEmoji(attachment, name) { createEmoji(attachment, name) {
return new Promise((resolve, reject) => { return new Promise(resolve => {
this.client.resolver.resolveFile(attachment).then(file => { if (attachment.startsWith('data:')) {
let base64 = new Buffer(file, 'binary').toString('base64'); resolve(this.client.rest.methods.createEmoji(this, attachment, name));
let dataURI = `data:;base64,${base64}`; } else {
this.client.rest.methods.createEmoji(this, dataURI, name) this.client.resolver.resolveBuffer(attachment).then(data =>
.then(resolve).catch(reject); resolve(this.client.rest.methods.createEmoji(this, data, name))
}).catch(reject); );
}
}); });
} }
@@ -637,21 +671,34 @@ class Guild {
* @returns {Promise<Guild>} * @returns {Promise<Guild>}
*/ */
setRolePosition(role, position) { setRolePosition(role, position) {
if (role instanceof Role) { if (typeof role === 'string') {
role = role.id; role = this.roles.get(role);
} else if (typeof role !== 'string') { if (!role) return Promise.reject(new Error('Supplied role is not a role or string.'));
return Promise.reject(new Error('Supplied role is not a role or string'));
} }
position = Number(position); position = Number(position);
if (isNaN(position)) { if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.'));
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 => ({ const updatedRoles = [];
id: r.id,
position: r.id === role ? position : r.position < position ? r.position : r.position + 1, 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); 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 * 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 * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often
* what most users need. * what most users need.
* @param {Guild} guild The guild to compare * @param {Guild} guild Guild to compare with
* @returns {boolean} * @returns {boolean}
*/ */
equals(guild) { 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} * @returns {string}
* @example * @example
* // logs: Hello from My Guild! * // logs: Hello from My Guild!
@@ -712,7 +759,7 @@ class Guild {
const member = new GuildMember(this, guildUser); const member = new GuildMember(this, guildUser);
this.members.set(member.id, member); 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); const voiceState = this._rawVoiceStates.get(member.user.id);
member.serverMute = voiceState.mute; member.serverMute = voiceState.mute;
member.serverDeaf = voiceState.deaf; member.serverDeaf = voiceState.deaf;
@@ -720,7 +767,11 @@ class Guild {
member.selfDeaf = voiceState.self_deaf; member.selfDeaf = voiceState.self_deaf;
member.voiceSessionID = voiceState.session_id; member.voiceSessionID = voiceState.session_id;
member.voiceChannelID = voiceState.channel_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) { 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 * @event Client#guildMemberUpdate
* @param {GuildMember} oldMember The member before the update * @param {GuildMember} oldMember The member before the update
* @param {GuildMember} newMember The member after the update * @param {GuildMember} newMember The member after the update
@@ -770,7 +821,7 @@ class Guild {
if (member && member.speaking !== speaking) { if (member && member.speaking !== speaking) {
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 * @event Client#guildMemberSpeaking
* @param {GuildMember} member The member that started/stopped speaking * @param {GuildMember} member The member that started/stopped speaking
* @param {boolean} speaking Whether or not the member is speaking * @param {boolean} speaking Whether or not the member is speaking

View File

@@ -4,10 +4,9 @@ const PermissionOverwrites = require('./PermissionOverwrites');
const EvaluatedPermissions = require('./EvaluatedPermissions'); const EvaluatedPermissions = require('./EvaluatedPermissions');
const Constants = require('../util/Constants'); const Constants = require('../util/Constants');
const Collection = require('../util/Collection'); 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} * @extends {Channel}
*/ */
class GuildChannel extends Channel { class GuildChannel extends Channel {
@@ -25,7 +24,7 @@ class GuildChannel extends Channel {
super.setup(data); super.setup(data);
/** /**
* The name of the Guild Channel * The name of the guild channel
* @type {string} * @type {string}
*/ */
this.name = data.name; this.name = data.name;
@@ -66,11 +65,11 @@ class GuildChannel extends Channel {
const overwrites = this.overwritesFor(member, true, roles); const overwrites = this.overwritesFor(member, true, roles);
for (const overwrite of overwrites.role.concat(overwrites.member)) { for (const overwrite of overwrites.role.concat(overwrites.member)) {
permissions &= ~overwrite.denyData; permissions &= ~overwrite.deny;
permissions |= overwrite.allowData; permissions |= overwrite.allow;
} }
const admin = Boolean(permissions & (Constants.PermissionFlags.ADMINISTRATOR)); const admin = Boolean(permissions & Constants.PermissionFlags.ADMINISTRATOR);
if (admin) permissions = Constants.ALL_PERMISSIONS; if (admin) permissions = Constants.ALL_PERMISSIONS;
return new EvaluatedPermissions(member, permissions); return new EvaluatedPermissions(member, permissions);
@@ -144,8 +143,8 @@ class GuildChannel extends Channel {
const prevOverwrite = this.permissionOverwrites.get(userOrRole.id); const prevOverwrite = this.permissionOverwrites.get(userOrRole.id);
if (prevOverwrite) { if (prevOverwrite) {
payload.allow = prevOverwrite.allowData; payload.allow = prevOverwrite.allow;
payload.deny = prevOverwrite.denyData; payload.deny = prevOverwrite.deny;
} }
for (const perm in options) { for (const perm in options) {
@@ -155,18 +154,41 @@ class GuildChannel extends Channel {
} else if (options[perm] === false) { } else if (options[perm] === false) {
payload.allow &= ~(Constants.PermissionFlags[perm] || 0); payload.allow &= ~(Constants.PermissionFlags[perm] || 0);
payload.deny |= 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); 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<GuildChannel>}
* @example
* // edit a channel
* channel.edit({name: 'new-channel'})
* .then(c => console.log(`Edited channel ${c}`))
* .catch(console.error);
*/
edit(data) { edit(data) {
return this.client.rest.methods.updateChannel(this, 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 * @param {string} name The new name for the guild channel
* @returns {Promise<GuildChannel>} * @returns {Promise<GuildChannel>}
* @example * @example
@@ -176,11 +198,11 @@ class GuildChannel extends Channel {
* .catch(console.error); * .catch(console.error);
*/ */
setName(name) { 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 * @param {number} position The new position for the guild channel
* @returns {Promise<GuildChannel>} * @returns {Promise<GuildChannel>}
* @example * @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 * @param {string} topic The new topic for the guild channel
* @returns {Promise<GuildChannel>} * @returns {Promise<GuildChannel>}
* @example * @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 * @typedef {Object} InviteOptions
* @property {boolean} [temporary=false] Whether the invite should kick users after 24hrs if they are not given a role * @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 {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 * @param {InviteOptions} [options={}] The options for the invite
* @returns {Promise<Invite>} * @returns {Promise<Invite>}
*/ */
@@ -224,10 +246,20 @@ class GuildChannel extends Channel {
return this.client.rest.methods.createChannelInvite(this, options); 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<GuildChannel>}
*/
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. * 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. * 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} * @returns {boolean}
*/ */
equals(channel) { equals(channel) {
@@ -240,9 +272,7 @@ class GuildChannel extends Channel {
if (equal) { if (equal) {
if (this.permissionOverwrites && channel.permissionOverwrites) { if (this.permissionOverwrites && channel.permissionOverwrites) {
const thisIDSet = this.permissionOverwrites.keyArray(); equal = this.permissionOverwrites.equals(channel.permissionOverwrites);
const otherIDSet = channel.permissionOverwrites.keyArray();
equal = arraysEqual(thisIDSet, otherIDSet);
} else { } else {
equal = !this.permissionOverwrites && !channel.permissionOverwrites; 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} * @returns {string}
* @example * @example
* // Outputs: Hello from #general * // Outputs: Hello from #general

View File

@@ -6,17 +6,18 @@ const Collection = require('../util/Collection');
const Presence = require('./Presence').Presence; const Presence = require('./Presence').Presence;
/** /**
* Represents a Member of a Guild on Discord * Represents a member of a guild on Discord
* @implements {TextBasedChannel} * @implements {TextBasedChannel}
*/ */
class GuildMember { class GuildMember {
constructor(guild, data) { constructor(guild, data) {
/** /**
* The client that instantiated this GuildMember * The Client that instantiated this GuildMember
* @name GuildMember#client
* @type {Client} * @type {Client}
* @readonly
*/ */
this.client = guild.client; Object.defineProperty(this, 'client', { value: guild.client });
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
/** /**
* The guild that this member is part of * The guild that this member is part of
@@ -32,6 +33,12 @@ class GuildMember {
this._roles = []; this._roles = [];
if (data) this.setup(data); 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) { setup(data) {
@@ -78,7 +85,7 @@ class GuildMember {
this.speaking = false; 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} * @type {?string}
*/ */
this.nickname = data.nick || null; 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} * @type {Presence}
* @readonly * @readonly
*/ */
@@ -167,7 +174,7 @@ class GuildMember {
} }
/** /**
* The ID of this User * The ID of this user
* @type {string} * @type {string}
* @readonly * @readonly
*/ */
@@ -175,6 +182,15 @@ class GuildMember {
return this.user.id; 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 * The overall set of permissions for the guild member, taking only roles into account
* @type {EvaluatedPermissions} * @type {EvaluatedPermissions}
@@ -187,7 +203,7 @@ class GuildMember {
const roles = this.roles; const roles = this.roles;
for (const role of roles.values()) permissions |= role.permissions; 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; if (admin) permissions = Constants.ALL_PERMISSIONS;
return new EvaluatedPermissions(this, 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. * 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 {PermissionResolvable[]} permissions The permissions to check for
* @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions * @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions
* @returns {array} * @returns {PermissionResolvable[]}
*/ */
missingPermissions(permissions, explicit = false) { missingPermissions(permissions, explicit = false) {
return permissions.filter(p => !this.hasPermission(p, explicit)); 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 * @param {GuildmemberEditData} data The data to edit the member with
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
@@ -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 * @param {ChannelResolvable} channel The channel to move the member to
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
@@ -299,7 +315,7 @@ class GuildMember {
} }
/** /**
* Sets the Roles applied to the member. * Sets the roles applied to the member.
* @param {Collection<string, Role>|Role[]|string[]} roles The roles or role IDs to apply * @param {Collection<string, Role>|Role[]|string[]} roles The roles or role IDs to apply
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
@@ -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 * @param {Role|string} role The role or ID of the role to add
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
addRole(role) { 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 * @param {Role|string} role The role or ID of the role to remove
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
removeRole(role) { 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 * Set the nickname for the guild member
* @param {string} nick The nickname for the Guild Member * @param {string} nick The nickname for the guild member
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
setNickname(nick) { setNickname(nick) {
@@ -372,7 +390,7 @@ class GuildMember {
} }
/** /**
* Deletes any DMs with this Guild Member * Deletes any DMs with this guild member
* @returns {Promise<DMChannel>} * @returns {Promise<DMChannel>}
*/ */
deleteDM() { deleteDM() {
@@ -380,7 +398,7 @@ class GuildMember {
} }
/** /**
* Kick this member from the Guild * Kick this member from the guild
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
*/ */
kick() { 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 * @param {number} [deleteDays=0] The amount of days worth of messages from this member that should
* also be deleted. Between `0` and `7`. * also be deleted. Between `0` and `7`.
* @returns {Promise<GuildMember>} * @returns {Promise<GuildMember>}
@@ -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} * @returns {string}
* @example * @example
* // logs: Hello from <@123456789>! * // logs: Hello from <@123456789>!
@@ -412,8 +430,9 @@ class GuildMember {
} }
// These are here only for documentation purposes - they are implemented by TextBasedChannel // These are here only for documentation purposes - they are implemented by TextBasedChannel
send() { return; }
sendMessage() { return; } sendMessage() { return; }
sendTTSMessage() { return; } sendEmbed() { return; }
sendFile() { return; } sendFile() { return; }
sendCode() { return; } sendCode() { return; }
} }

View File

@@ -24,25 +24,26 @@ const Constants = require('../util/Constants');
*/ */
/** /**
* Represents an Invitation to a Guild Channel. * Represents an invitation to a guild channel.
* <warn>The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing.</warn> * <warn>The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing.</warn>
*/ */
class Invite { class Invite {
constructor(client, data) { constructor(client, data) {
/** /**
* The client that instantiated the invite * The client that instantiated the invite
* @name Invite#client
* @type {Client} * @type {Client}
* @readonly
*/ */
this.client = client; Object.defineProperty(this, 'client', { value: client });
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
this.setup(data); this.setup(data);
} }
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 * 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. * unknown, this will be a PartialGuild object.
* @type {Guild|PartialGuild} * @type {Guild|PartialGuild}
*/ */
this.guild = this.client.guilds.get(data.guild.id) || new PartialGuild(this.client, data.guild); 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. * 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. * If the channel is unknown, this will be a PartialGuildChannel object.
* @type {GuildChannel|PartialGuildChannel} * @type {GuildChannel|PartialGuildChannel}
*/ */
this.channel = this.client.channels.get(data.channel.id) || new PartialGuildChannel(this.client, data.channel); 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} * @returns {string}
* @example * @example
* // logs: Invite: https://discord.gg/A1b2C3 * // logs: Invite: https://discord.gg/A1b2C3

View File

@@ -1,20 +1,25 @@
const Attachment = require('./MessageAttachment'); const Attachment = require('./MessageAttachment');
const Embed = require('./MessageEmbed'); const Embed = require('./MessageEmbed');
const MessageReaction = require('./MessageReaction');
const Collection = require('../util/Collection'); const Collection = require('../util/Collection');
const Constants = require('../util/Constants'); const Constants = require('../util/Constants');
const escapeMarkdown = require('../util/EscapeMarkdown'); 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 { class Message {
constructor(channel, data, client) { constructor(channel, data, client) {
/** /**
* The client that instantiated the Message * The Client that instantiated the Message
* @name Message#client
* @type {Client} * @type {Client}
* @readonly
*/ */
this.client = client; Object.defineProperty(this, 'client', { value: client });
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
/** /**
* The channel that the message was sent in * The channel that the message was sent in
@@ -25,7 +30,7 @@ class Message {
if (data) this.setup(data); 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) * The ID of the message (unique in the channel it was sent)
* @type {string} * @type {string}
@@ -51,7 +56,7 @@ class Message {
this.author = this.client.dataManager.newUser(data.author); 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. * where the author is still a member.
* @type {GuildMember} * @type {GuildMember}
*/ */
@@ -83,7 +88,7 @@ class Message {
/** /**
* A list of embeds in the message - e.g. YouTube Player * 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)); this.embeds = data.embeds.map(e => new Embed(this, e));
@@ -148,6 +153,25 @@ class Message {
} }
this._edits = []; this._edits = [];
/**
* A collection of reactions to this message, mapped by the reaction "id".
* @type {Collection<string, MessageReaction>}
*/
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 patch(data) { // eslint-disable-line complexity
@@ -199,6 +223,15 @@ class Message {
if (chan) this.mentions.channels.set(chan.id, chan); 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. * An array of cached versions of the message, including the current version.
* Sorted from latest (first) to oldest (last). * Sorted from latest (first) to oldest (last).
* @type {Message[]} * @type {Message[]}
* @readonly * @readonly
*/ */
get edits() { 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); 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 * 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<Message>} * @returns {Promise<Message>}
* @example * @example
* // update the content of a message * // 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}`)) * .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
* .catch(console.error); * .catch(console.error);
*/ */
edit(content) { edit(content, options) {
return this.client.rest.methods.updateMessage(this, content); 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); return this.client.rest.methods.unpinMessage(this);
} }
/**
* Add a reaction to the message
* @param {string|Emoji|ReactionEmoji} emoji Emoji to react with
* @returns {Promise<MessageReaction>}
*/
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<Message>}
*/
clearReactions() {
return this.client.rest.methods.removeMessageReactions(this);
}
/** /**
* Deletes the message * Deletes the message
* @param {number} [timeout=0] How long to wait to delete the message in milliseconds * @param {number} [timeout=0] How long to wait to delete the message in milliseconds
@@ -369,13 +451,15 @@ class Message {
* .catch(console.error); * .catch(console.error);
*/ */
delete(timeout = 0) { delete(timeout = 0) {
return new Promise((resolve, reject) => { if (timeout <= 0) {
this.client.setTimeout(() => { return this.client.rest.methods.deleteMessage(this);
this.client.rest.methods.deleteMessage(this) } else {
.then(resolve) return new Promise(resolve => {
.catch(reject); this.client.setTimeout(() => {
}, timeout); resolve(this.delete());
}); }, timeout);
});
}
} }
/** /**
@@ -385,21 +469,22 @@ class Message {
* @returns {Promise<Message|Message[]>} * @returns {Promise<Message|Message[]>}
* @example * @example
* // reply to a message * // 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}`)) * .then(msg => console.log(`Sent a reply to ${msg.author}`))
* .catch(console.error); * .catch(console.error);
*/ */
reply(content, options = {}) { reply(content, options = {}) {
content = this.client.resolver.resolveString(content); content = `${this.guild || this.channel.type === 'group' ? `${this.author}, ` : ''}${content}`;
const prepend = this.guild ? `${this.author}, ` : ''; return this.channel.send(content, options);
content = `${prepend}${content}`; }
if (options.split) { /**
if (typeof options.split !== 'object') options.split = {}; * Fetches the webhook used to create this message.
if (!options.split.prepend) options.split.prepend = prepend; * @returns {Promise<?Webhook>}
} */
fetchWebhook() {
return this.client.rest.methods.sendMessage(this.channel, content, options); 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} * @returns {string}
* @example * @example
* // logs: Message: This is a message! * // logs: Message: This is a message!
@@ -442,6 +527,42 @@ class Message {
toString() { toString() {
return this.content; 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; module.exports = Message;

View File

@@ -1,14 +1,15 @@
/** /**
* Represents an Attachment in a Message * Represents an attachment in a message
*/ */
class MessageAttachment { class MessageAttachment {
constructor(message, data) { constructor(message, data) {
/** /**
* The Client that instantiated this Message. * The Client that instantiated this MessageAttachment.
* @name MessageAttachment#client
* @type {Client} * @type {Client}
* @readonly
*/ */
this.client = message.client; Object.defineProperty(this, 'client', { value: message.client });
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
/** /**
* The message this attachment is part of. * The message this attachment is part of.

View File

@@ -16,7 +16,7 @@ class MessageCollector extends EventEmitter {
* return false; // failed the filter test * return false; // failed the filter test
* } * }
* ``` * ```
* @typedef {function} CollectorFilterFunction * @typedef {Function} CollectorFilterFunction
*/ */
/** /**
@@ -54,7 +54,7 @@ class MessageCollector extends EventEmitter {
this.options = options; this.options = options;
/** /**
* Whether this collector has stopped collecting Messages. * Whether this collector has stopped collecting messages.
* @type {boolean} * @type {boolean}
*/ */
this.ended = false; this.ended = false;
@@ -81,7 +81,7 @@ class MessageCollector extends EventEmitter {
if (this.filter(message, this)) { if (this.filter(message, this)) {
this.collected.set(message.id, message); 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 {Message} message The received message
* @param {MessageCollector} collector The collector the message passed through * @param {MessageCollector} collector The collector the message passed through
* @event MessageCollector#message * @event MessageCollector#message
@@ -138,7 +138,7 @@ class MessageCollector extends EventEmitter {
/** /**
* Emitted when the Collector stops collecting. * Emitted when the Collector stops collecting.
* @param {Collection<string, Message>} collection A collection of messages collected * @param {Collection<string, Message>} 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 * @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 * 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`. * ended because it reached its message limit, it will be `limit`.

View File

@@ -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 { class MessageEmbed {
constructor(message, data) { constructor(message, data) {
/** /**
* The client that instantiated this embed * The client that instantiated this embed
* @name MessageEmbed#client
* @type {Client} * @type {Client}
* @readonly
*/ */
this.client = message.client; Object.defineProperty(this, 'client', { value: message.client });
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
/** /**
* The message this embed is part of * The message this embed is part of
@@ -20,18 +21,18 @@ class MessageEmbed {
} }
setup(data) { setup(data) {
/**
* The title of this embed, if there is one
* @type {?string}
*/
this.title = data.title;
/** /**
* The type of this embed * The type of this embed
* @type {string} * @type {string}
*/ */
this.type = data.type; 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 * The description of this embed, if there is one
* @type {?string} * @type {?string}
@@ -44,6 +45,25 @@ class MessageEmbed {
*/ */
this.url = data.url; 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 * The thumbnail of this embed, if there is one
* @type {MessageEmbedThumbnail} * @type {MessageEmbedThumbnail}
@@ -61,11 +81,36 @@ class MessageEmbed {
* @type {MessageEmbedProvider} * @type {MessageEmbedProvider}
*/ */
this.provider = data.provider ? new MessageEmbedProvider(this, data.provider) : null; 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 { class MessageEmbedThumbnail {
constructor(embed, data) { 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 { class MessageEmbedProvider {
constructor(embed, data) { 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 { class MessageEmbedAuthor {
constructor(embed, data) { constructor(embed, data) {
@@ -160,11 +205,89 @@ class MessageEmbedAuthor {
* @type {string} * @type {string}
*/ */
this.url = data.url; 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.Thumbnail = MessageEmbedThumbnail;
MessageEmbed.Provider = MessageEmbedProvider; MessageEmbed.Provider = MessageEmbedProvider;
MessageEmbed.Author = MessageEmbedAuthor; MessageEmbed.Author = MessageEmbedAuthor;
MessageEmbed.Field = MessageEmbedField;
MessageEmbed.Footer = MessageEmbedFooter;
module.exports = MessageEmbed; module.exports = MessageEmbed;

View File

@@ -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<string, User>}
*/
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<MessageReaction>}
*/
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<Collection<string, User>>}
*/
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;

View File

@@ -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<string>}
*/
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;

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