mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-18 20:43:30 +01:00
v10 prep
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -35,4 +35,8 @@ build/Release
|
|||||||
node_modules
|
node_modules
|
||||||
test/auth.json
|
test/auth.json
|
||||||
examples/auth.json
|
examples/auth.json
|
||||||
docs/_build
|
docs/_build
|
||||||
|
|
||||||
|
# Secret keys
|
||||||
|
docs/deploy/deploy_key
|
||||||
|
docs/deploy/deploy_key.pub
|
||||||
|
|||||||
@@ -5,3 +5,9 @@ cache:
|
|||||||
directories:
|
directories:
|
||||||
- node_modules
|
- node_modules
|
||||||
install: npm install
|
install: npm install
|
||||||
|
script: bash ./docs/deploy/deploy.sh
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- ENCRYPTION_LABEL: "af862fa96d3e"
|
||||||
|
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ is a great boon to your coding process.
|
|||||||
To get ready to work on the codebase, please do the following:
|
To get ready to work on the codebase, please do the following:
|
||||||
|
|
||||||
1. Fork & clone the repository
|
1. Fork & clone the repository
|
||||||
2. Run `npm install`, or `npm install --no-optional` if you're not working on voice
|
2. Run `npm install`
|
||||||
3. Code your heart out!
|
3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript`
|
||||||
4. Run `npm test` to run ESLint
|
4. Code your heart out!
|
||||||
5. Run `npm run docs` to build any documentation changes
|
5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid
|
||||||
6. [Submit a pull request](https://github.com/hydrabolt/discord.js/compare)
|
6. [Submit a pull request](https://github.com/hydrabolt/discord.js/compare)
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://hydrabolt.github.io/discord.js">
|
<a href="https://hydrabolt.github.io/discord.js">
|
||||||
<img alt="discord.js" src="http://i.imgur.com/sPOLh9y.png" width="546"><br />
|
<img alt="discord.js" src="http://i.imgur.com/0af7LDs.png" width="546"><br />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -16,12 +16,14 @@ discord.js is a powerful node.js module that allows you to interact with the [Di
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
**Node.js 6.0.0 or newer is required.**
|
**Node.js 6.0.0 or newer is required.**
|
||||||
With voice support: `npm install --save discord.js --production`
|
Without voice support: `npm install discord.js --save`
|
||||||
Without voice support: `npm install --save discord.js --production --no-optional`
|
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`
|
||||||
|
If both audio packages are installed, discord.js will automatically choose node-opus.
|
||||||
|
|
||||||
By default, discord.js uses [opusscript](https://www.npmjs.com/package/opusscript) when playing audio over voice connections.
|
The preferred audio engine is node-opus, as it performs significantly better than opusscript.
|
||||||
If you're looking to play over multiple voice connections, it might be better to install [node-opus](https://www.npmjs.com/package/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.
|
||||||
discord.js will automatically prefer node-opus over opusscript.
|
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
```js
|
```js
|
||||||
@@ -41,17 +43,24 @@ client.on('message', message => {
|
|||||||
client.login('your token');
|
client.login('your token');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A bot template using discord.js can be generated using [generator-discordbot](https://www.npmjs.com/package/generator-discordbot).
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
* [Website](http://hydrabolt.github.io/discord.js/)
|
* [Website](http://hydrabolt.github.io/discord.js/)
|
||||||
* [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://hydrabolt.github.io/discord.js/#!/docs/tag/master)
|
* [Documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master)
|
||||||
* [Legacy Documentation](http://discordjs.readthedocs.io/en/8.1.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)
|
||||||
* [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)
|
||||||
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
|
* [Related libraries](https://discordapi.com/unofficial/libs.html)
|
||||||
* [Related Libraries](https://discordapi.com/unofficial/libs.html)
|
|
||||||
|
|
||||||
## Contact
|
## Contributing
|
||||||
Before reporting an issue, please read the [documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master).
|
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||||
If you can't find help there, you can ask in the official [Discord.js Server](https://discord.gg/bRCvFy9).
|
[documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master).
|
||||||
|
See [the contributing guide](CONTRIBUTING.md) if you'd like to submit a PR.
|
||||||
|
|
||||||
|
## Help
|
||||||
|
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||||
|
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||||
|
|||||||
2
docs/README.md
Normal file
2
docs/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# discord.js docs
|
||||||
|
[View documentation here](http://hydrabolt.github.io/discord.js/#!/docs/)
|
||||||
@@ -20,12 +20,14 @@ stable and performant than previous versions.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
**Node.js 6.0.0 or newer is required.**
|
**Node.js 6.0.0 or newer is required.**
|
||||||
With voice support: `npm install --save discord.js --production`
|
Without voice support: `npm install discord.js --save`
|
||||||
Without voice support: `npm install --save discord.js --production --no-optional`
|
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`
|
||||||
|
If both audio packages are installed, discord.js will automatically prefer node-opus.
|
||||||
|
|
||||||
By default, discord.js uses [opusscript](https://www.npmjs.com/package/opusscript) when playing audio over voice connections.
|
The preferred audio engine is node-opus, as it performs significantly better than opusscript.
|
||||||
If you're looking to play over multiple voice connections, it might be better to install [node-opus](https://www.npmjs.com/package/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.
|
||||||
discord.js will automatically prefer node-opus over opusscript.
|
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||||
|
|
||||||
## Guides
|
## Guides
|
||||||
* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/)
|
* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/)
|
||||||
@@ -33,15 +35,15 @@ discord.js will automatically prefer node-opus over opusscript.
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
* [Website](http://hydrabolt.github.io/discord.js/)
|
* [Website](http://hydrabolt.github.io/discord.js/)
|
||||||
* [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://hydrabolt.github.io/discord.js/#!/docs/tag/master)
|
* [Documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master)
|
||||||
* [Legacy Documentation](http://discordjs.readthedocs.io/en/8.1.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)
|
||||||
* [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)
|
||||||
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
|
* [Related libraries](https://discordapi.com/unofficial/libs.html)
|
||||||
* [Related Libraries](https://discordapi.com/unofficial/libs.html)
|
|
||||||
|
|
||||||
## Help
|
## Help
|
||||||
If you don't understand something in this 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
|
||||||
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||||
|
|||||||
12
docs/custom/examples/webhook.js
Normal file
12
docs/custom/examples/webhook.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Send a message using a webhook
|
||||||
|
*/
|
||||||
|
|
||||||
|
// import the discord.js module
|
||||||
|
const Discord = require('discord.js');
|
||||||
|
|
||||||
|
// create a new webhook
|
||||||
|
const hook = new Discord.WebhookClient('webhook id', 'webhook token');
|
||||||
|
|
||||||
|
// send a message using the webhook
|
||||||
|
hook.sendMessage('I am now alive!');
|
||||||
10
docs/custom/webhook.js
Normal file
10
docs/custom/webhook.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
category: 'Examples',
|
||||||
|
name: 'Webhooks',
|
||||||
|
data:
|
||||||
|
`\`\`\`js
|
||||||
|
${fs.readFileSync('./docs/custom/examples/webhook.js').toString('utf-8')}
|
||||||
|
\`\`\``,
|
||||||
|
};
|
||||||
68
docs/deploy/deploy.sh
Normal file
68
docs/deploy/deploy.sh
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Adapted from https://gist.github.com/domenic/ec8b0fc8ab45f39403dd.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function build {
|
||||||
|
node docs/generator/generator.js
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ignore Travis checking PRs
|
||||||
|
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||||
|
echo "deploy.sh: Ignoring PR build"
|
||||||
|
build
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ignore travis checking other branches irrelevant to users
|
||||||
|
if [ "$TRAVIS_BRANCH" != "master" -a "$TRAVIS_BRANCH" != "indev" ]; then
|
||||||
|
echo "deploy.sh: Ignoring push to another branch than master/indev"
|
||||||
|
build
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SOURCE=$TRAVIS_BRANCH
|
||||||
|
|
||||||
|
# Make sure tag pushes are handled
|
||||||
|
if [ -n "$TRAVIS_TAG" ]; then
|
||||||
|
echo "deploy.sh: This is a tag build, proceeding accordingly"
|
||||||
|
SOURCE=$TRAVIS_TAG
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO=`git config remote.origin.url`
|
||||||
|
SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:}
|
||||||
|
SHA=`git rev-parse --verify HEAD`
|
||||||
|
|
||||||
|
TARGET_BRANCH="docs"
|
||||||
|
|
||||||
|
# Checkout the repo in the target branch so we can build docs and push to it
|
||||||
|
git clone $REPO out -b $TARGET_BRANCH
|
||||||
|
cd out
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Build the docs
|
||||||
|
build
|
||||||
|
|
||||||
|
# Move the generated JSON file to the newly-checked-out repo, to be committed
|
||||||
|
# and pushed
|
||||||
|
mv docs/docs.json out/$SOURCE.json
|
||||||
|
|
||||||
|
# Commit and push
|
||||||
|
cd out
|
||||||
|
git config user.name "Travis CI"
|
||||||
|
git config user.email "$COMMIT_AUTHOR_EMAIL"
|
||||||
|
|
||||||
|
git add .
|
||||||
|
git commit -m "Docs build: ${SHA}"
|
||||||
|
|
||||||
|
ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key"
|
||||||
|
ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv"
|
||||||
|
ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR}
|
||||||
|
ENCRYPTED_IV=${!ENCRYPTED_IV_VAR}
|
||||||
|
openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in ../docs/deploy/deploy_key.enc -out deploy_key -d
|
||||||
|
chmod 600 deploy_key
|
||||||
|
eval `ssh-agent -s`
|
||||||
|
ssh-add deploy_key
|
||||||
|
|
||||||
|
# Now that we're all set up, we can push.
|
||||||
|
git push $SSH_REPO $TARGET_BRANCH
|
||||||
BIN
docs/deploy/deploy_key.enc
Normal file
BIN
docs/deploy/deploy_key.enc
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "discord.js",
|
"name": "discord.js",
|
||||||
"version": "9.3.1",
|
"version": "10.0.0",
|
||||||
"description": "A powerful library for interacting with the Discord API",
|
"description": "A powerful library for interacting with the Discord API",
|
||||||
"main": "./src/index",
|
"main": "./src/index",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -28,7 +28,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"superagent": "^2.2.0",
|
"superagent": "^2.2.0",
|
||||||
"tweetnacl": "^0.14.3",
|
"tweetnacl": "^0.14.3",
|
||||||
"ws": "^1.1.1",
|
"ws": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"node-opus": "^0.2.1",
|
||||||
"opusscript": "^0.0.1"
|
"opusscript": "^0.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -36,9 +39,6 @@
|
|||||||
"jsdoc-parse": "^1.2.0",
|
"jsdoc-parse": "^1.2.0",
|
||||||
"eslint": "^3.4.0"
|
"eslint": "^3.4.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
|
||||||
"node-opus": "^0.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const ClientVoiceManager = require('./voice/ClientVoiceManager');
|
|||||||
const WebSocketManager = require('./websocket/WebSocketManager');
|
const WebSocketManager = require('./websocket/WebSocketManager');
|
||||||
const ActionsManager = require('./actions/ActionsManager');
|
const ActionsManager = require('./actions/ActionsManager');
|
||||||
const Collection = require('../util/Collection');
|
const Collection = require('../util/Collection');
|
||||||
|
const Presence = require('../structures/Presence').Presence;
|
||||||
|
const ShardClientUtil = require('../sharding/ShardClientUtil');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The starting point for making a Discord Bot.
|
* The starting point for making a Discord Bot.
|
||||||
@@ -18,22 +20,19 @@ class Client extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @param {ClientOptions} [options] Options for the client
|
* @param {ClientOptions} [options] Options for the client
|
||||||
*/
|
*/
|
||||||
constructor(options) {
|
constructor(options = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
// Obtain shard details from environment
|
||||||
|
if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID);
|
||||||
|
if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options the client was instantiated with
|
* The options the client was instantiated with
|
||||||
* @type {ClientOptions}
|
* @type {ClientOptions}
|
||||||
*/
|
*/
|
||||||
this.options = mergeDefault(Constants.DefaultOptions, options);
|
this.options = mergeDefault(Constants.DefaultOptions, options);
|
||||||
|
this._validateOptions();
|
||||||
if (!this.options.shard_id && 'SHARD_ID' in process.env) {
|
|
||||||
this.options.shard_id = process.env.SHARD_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.options.shard_count && 'SHARD_COUNT' in process.env) {
|
|
||||||
this.options.shard_count = process.env.SHARD_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The REST manager of the client
|
* The REST manager of the client
|
||||||
@@ -84,6 +83,12 @@ class Client extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
this.voice = new ClientVoiceManager(this);
|
this.voice = new ClientVoiceManager(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The shard helpers for the client (only if the process was spawned as a child, such as from a ShardingManager)
|
||||||
|
* @type {?ShardUtil}
|
||||||
|
*/
|
||||||
|
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>}
|
||||||
@@ -103,10 +108,21 @@ class Client extends EventEmitter {
|
|||||||
this.channels = new Collection();
|
this.channels = new Collection();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The authorization token for the logged in user/bot.
|
* A Collection of presences for friends of the logged in user.
|
||||||
* @type {?string}
|
* <warn>This is only present for user accounts, not bot accounts!</warn>
|
||||||
|
* @type {Collection<string, Presence>}
|
||||||
*/
|
*/
|
||||||
this.token = null;
|
this.presences = new Collection();
|
||||||
|
|
||||||
|
if (!this.token && 'CLIENT_TOKEN' in process.env) {
|
||||||
|
/**
|
||||||
|
* The authorization token for the logged in user/bot.
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
this.token = process.env.CLIENT_TOKEN;
|
||||||
|
} else {
|
||||||
|
this.token = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The email, if there is one, for the logged in Client
|
* The email, if there is one, for the logged in Client
|
||||||
@@ -130,38 +146,20 @@ class Client extends EventEmitter {
|
|||||||
* The date at which the Client was regarded as being in the `READY` state.
|
* The date at which the Client was regarded as being in the `READY` state.
|
||||||
* @type {?Date}
|
* @type {?Date}
|
||||||
*/
|
*/
|
||||||
this.readyTime = null;
|
this.readyAt = null;
|
||||||
|
|
||||||
this._timeouts = new Set();
|
this._timeouts = new Set();
|
||||||
this._intervals = new Set();
|
this._intervals = new Set();
|
||||||
|
|
||||||
if (this.options.message_sweep_interval > 0) {
|
if (this.options.messageSweepInterval > 0) {
|
||||||
this.setInterval(this.sweepMessages.bind(this), this.options.message_sweep_interval * 1000);
|
this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000);
|
||||||
}
|
|
||||||
|
|
||||||
if (process.send) {
|
|
||||||
process.on('message', message => {
|
|
||||||
if (!message) return;
|
|
||||||
if (message._eval) {
|
|
||||||
try {
|
|
||||||
process.send({ _evalResult: eval(message._eval) });
|
|
||||||
} catch (err) {
|
|
||||||
process.send({ _evalError: err });
|
|
||||||
}
|
|
||||||
} else if (message._fetchProp) {
|
|
||||||
const props = message._fetchProp.split('.');
|
|
||||||
let value = this; // eslint-disable-line consistent-this
|
|
||||||
for (const prop of props) value = value[prop];
|
|
||||||
process.send({ _fetchProp: message._fetchProp, _fetchPropValue: value });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The status for the logged in Client.
|
* The status for the logged in Client.
|
||||||
* @readonly
|
|
||||||
* @type {?number}
|
* @type {?number}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get status() {
|
get status() {
|
||||||
return this.ws.status;
|
return this.ws.status;
|
||||||
@@ -169,17 +167,17 @@ class Client extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The uptime for the logged in Client.
|
* The uptime for the logged in Client.
|
||||||
* @readonly
|
|
||||||
* @type {?number}
|
* @type {?number}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get uptime() {
|
get uptime() {
|
||||||
return this.readyTime ? Date.now() - this.readyTime : null;
|
return this.readyAt ? Date.now() - this.readyAt : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Collection, mapping Guild ID to Voice Connections.
|
* Returns a Collection, mapping Guild ID to Voice Connections.
|
||||||
* @readonly
|
|
||||||
* @type {Collection<string, VoiceConnection>}
|
* @type {Collection<string, VoiceConnection>}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get voiceConnections() {
|
get voiceConnections() {
|
||||||
return this.voice.connections;
|
return this.voice.connections;
|
||||||
@@ -192,10 +190,21 @@ class Client extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
get emojis() {
|
get emojis() {
|
||||||
const emojis = new Collection();
|
const emojis = new Collection();
|
||||||
this.guilds.map(g => g.emojis.map(e => emojis.set(e.id, e)));
|
for (const guild of this.guilds.values()) {
|
||||||
|
for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji);
|
||||||
|
}
|
||||||
return emojis;
|
return emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp that the client was last ready at
|
||||||
|
* @type {?number}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get readyTimestamp() {
|
||||||
|
return this.readyAt ? this.readyAt.getTime() : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -225,30 +234,26 @@ class Client extends EventEmitter {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
return new Promise((resolve, reject) => {
|
for (const t of this._timeouts) clearTimeout(t);
|
||||||
this.manager.destroy().then(() => {
|
for (const i of this._intervals) clearInterval(i);
|
||||||
for (const t of this._timeouts) clearTimeout(t);
|
this._timeouts = [];
|
||||||
for (const i of this._intervals) clearInterval(i);
|
this._intervals = [];
|
||||||
this._timeouts = [];
|
this.token = null;
|
||||||
this._intervals = [];
|
this.email = null;
|
||||||
this.token = null;
|
this.password = null;
|
||||||
this.email = null;
|
return this.manager.destroy();
|
||||||
this.password = null;
|
|
||||||
resolve();
|
|
||||||
}).catch(reject);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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. Only applicable to user accounts.
|
||||||
* @param {Guild[]} [guilds=this.guilds.array()] An array of guilds to sync
|
* @param {Guild[]|Collection<string, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
|
||||||
*/
|
*/
|
||||||
syncGuilds(guilds = this.guilds.array()) {
|
syncGuilds(guilds = this.guilds) {
|
||||||
if (!this.user.bot) {
|
if (!this.user.bot) {
|
||||||
this.ws.send({
|
this.ws.send({
|
||||||
op: 12,
|
op: 12,
|
||||||
d: guilds.map(g => g.id),
|
d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,23 +271,33 @@ class Client extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches an invite object from an invite code.
|
* Fetches an invite object from an invite code.
|
||||||
* @param {string} code the invite code.
|
* @param {InviteResolvable} invite An invite code or URL
|
||||||
* @returns {Promise<Invite>}
|
* @returns {Promise<Invite>}
|
||||||
*/
|
*/
|
||||||
fetchInvite(code) {
|
fetchInvite(invite) {
|
||||||
|
const code = this.resolver.resolveInviteCode(invite);
|
||||||
return this.rest.methods.getInvite(code);
|
return this.rest.methods.getInvite(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a webhook by ID.
|
||||||
|
* @param {string} id ID of the webhook
|
||||||
|
* @returns {Promise<Webhook>}
|
||||||
|
*/
|
||||||
|
fetchWebhook(id) {
|
||||||
|
return this.rest.methods.getWebhook(id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sweeps all channels' messages and removes the ones older than the max message lifetime.
|
* Sweeps all channels' messages and removes the ones older than the max message lifetime.
|
||||||
* If the message has been edited, the time of the edit is used rather than the time of the original message.
|
* If the message has been edited, the time of the edit is used rather than the time of the original message.
|
||||||
* @param {number} [lifetime=this.options.message_cache_lifetime] Messages that are older than this (in seconds)
|
* @param {number} [lifetime=this.options.messageCacheLifetime] Messages that are older than this (in seconds)
|
||||||
* will be removed from the caches. The default is based on the client's `message_cache_lifetime` option.
|
* will be removed from the caches. The default is based on the client's `messageCacheLifetime` option.
|
||||||
* @returns {number} Amount of messages that were removed from the caches,
|
* @returns {number} Amount of messages that were removed from the caches,
|
||||||
* or -1 if the message cache lifetime is unlimited
|
* or -1 if the message cache lifetime is unlimited
|
||||||
*/
|
*/
|
||||||
sweepMessages(lifetime = this.options.message_cache_lifetime) {
|
sweepMessages(lifetime = this.options.messageCacheLifetime) {
|
||||||
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('Lifetime must be a number.');
|
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.');
|
||||||
if (lifetime <= 0) {
|
if (lifetime <= 0) {
|
||||||
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
|
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
|
||||||
return -1;
|
return -1;
|
||||||
@@ -298,7 +313,7 @@ class Client extends EventEmitter {
|
|||||||
channels++;
|
channels++;
|
||||||
|
|
||||||
for (const message of channel.messages.values()) {
|
for (const message of channel.messages.values()) {
|
||||||
if (now - (message._editedTimestamp || message._timestamp) > lifetimeMs) {
|
if (now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs) {
|
||||||
channel.messages.delete(message.id);
|
channel.messages.delete(message.id);
|
||||||
messages++;
|
messages++;
|
||||||
}
|
}
|
||||||
@@ -333,6 +348,51 @@ class Client extends EventEmitter {
|
|||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
this._intervals.delete(interval);
|
this._intervals.delete(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setPresence(id, presence) {
|
||||||
|
if (this.presences.get(id)) {
|
||||||
|
this.presences.get(id).update(presence);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.presences.set(id, new Presence(presence));
|
||||||
|
}
|
||||||
|
|
||||||
|
_eval(script) {
|
||||||
|
return eval(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
_validateOptions(options = this.options) {
|
||||||
|
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) {
|
||||||
|
throw new TypeError('The shardCount option must be a number.');
|
||||||
|
}
|
||||||
|
if (typeof options.shardId !== 'number' || isNaN(options.shardId)) {
|
||||||
|
throw new TypeError('The shardId option must be a number.');
|
||||||
|
}
|
||||||
|
if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.');
|
||||||
|
if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.');
|
||||||
|
if (options.shardId !== 0 && options.shardId >= options.shardCount) {
|
||||||
|
throw new RangeError('The shardId option must be less than shardCount.');
|
||||||
|
}
|
||||||
|
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
|
||||||
|
throw new TypeError('The messageCacheMaxSize option must be a number.');
|
||||||
|
}
|
||||||
|
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) {
|
||||||
|
throw new TypeError('The messageCacheLifetime option must be a number.');
|
||||||
|
}
|
||||||
|
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
|
||||||
|
throw new TypeError('The messageSweepInterval option must be a number.');
|
||||||
|
}
|
||||||
|
if (typeof options.fetchAllMembers !== 'boolean') {
|
||||||
|
throw new TypeError('The fetchAllMembers option must be a boolean.');
|
||||||
|
}
|
||||||
|
if (typeof options.disableEveryone !== 'boolean') {
|
||||||
|
throw new TypeError('The disableEveryone option must be a boolean.');
|
||||||
|
}
|
||||||
|
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) {
|
||||||
|
throw new TypeError('The restWsBridgeTimeout option must be a number.');
|
||||||
|
}
|
||||||
|
if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Client;
|
module.exports = Client;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const cloneObject = require('../util/CloneObject');
|
|||||||
const Guild = require('../structures/Guild');
|
const Guild = require('../structures/Guild');
|
||||||
const User = require('../structures/User');
|
const User = require('../structures/User');
|
||||||
const DMChannel = require('../structures/DMChannel');
|
const DMChannel = require('../structures/DMChannel');
|
||||||
|
const Emoji = require('../structures/Emoji');
|
||||||
const TextChannel = require('../structures/TextChannel');
|
const TextChannel = require('../structures/TextChannel');
|
||||||
const VoiceChannel = require('../structures/VoiceChannel');
|
const VoiceChannel = require('../structures/VoiceChannel');
|
||||||
const GuildChannel = require('../structures/GuildChannel');
|
const GuildChannel = require('../structures/GuildChannel');
|
||||||
@@ -27,7 +28,7 @@ class ClientDataManager {
|
|||||||
* @event Client#guildCreate
|
* @event Client#guildCreate
|
||||||
* @param {Guild} guild The created guild
|
* @param {Guild} guild The created guild
|
||||||
*/
|
*/
|
||||||
if (this.client.options.fetch_all_members) {
|
if (this.client.options.fetchAllMembers) {
|
||||||
guild.fetchMembers().then(() => { this.client.emit(Constants.Events.GUILD_CREATE, guild); });
|
guild.fetchMembers().then(() => { this.client.emit(Constants.Events.GUILD_CREATE, guild); });
|
||||||
} else {
|
} else {
|
||||||
this.client.emit(Constants.Events.GUILD_CREATE, guild);
|
this.client.emit(Constants.Events.GUILD_CREATE, guild);
|
||||||
@@ -73,6 +74,26 @@ class ClientDataManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newEmoji(data, guild) {
|
||||||
|
const already = guild.emojis.has(data.id);
|
||||||
|
if (data && !already) {
|
||||||
|
let emoji = new Emoji(guild, data);
|
||||||
|
this.client.emit(Constants.Events.EMOJI_CREATE, emoji);
|
||||||
|
guild.emojis.set(emoji.id, emoji);
|
||||||
|
return emoji;
|
||||||
|
} else if (already) {
|
||||||
|
return guild.emojis.get(data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
killEmoji(emoji) {
|
||||||
|
if (!(emoji instanceof Emoji && emoji.guild)) return;
|
||||||
|
this.client.emit(Constants.Events.EMOJI_DELETE, emoji);
|
||||||
|
emoji.guild.emojis.delete(emoji.id);
|
||||||
|
}
|
||||||
|
|
||||||
killGuild(guild) {
|
killGuild(guild) {
|
||||||
const already = this.client.guilds.has(guild.id);
|
const already = this.client.guilds.has(guild.id);
|
||||||
this.client.guilds.delete(guild.id);
|
this.client.guilds.delete(guild.id);
|
||||||
@@ -97,6 +118,12 @@ class ClientDataManager {
|
|||||||
updateChannel(currentChannel, newData) {
|
updateChannel(currentChannel, newData) {
|
||||||
currentChannel.setup(newData);
|
currentChannel.setup(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateEmoji(currentEmoji, newData) {
|
||||||
|
const oldEmoji = cloneObject(currentEmoji);
|
||||||
|
currentEmoji.setup(newData);
|
||||||
|
this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ClientDataManager;
|
module.exports = ClientDataManager;
|
||||||
|
|||||||
@@ -99,23 +99,6 @@ class ClientDataResolver {
|
|||||||
return guild.members.get(user.id) || null;
|
return guild.members.get(user.id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
|
|
||||||
* * A Buffer
|
|
||||||
* * A Base64 string
|
|
||||||
* @typedef {Buffer|string} Base64Resolvable
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a Base64Resolvable to a Base 64 image
|
|
||||||
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
|
|
||||||
* @returns {?string}
|
|
||||||
*/
|
|
||||||
resolveBase64(data) {
|
|
||||||
if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* * An instance of a Channel
|
||||||
@@ -138,6 +121,26 @@ class ClientDataResolver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data that can be resolved to give an invite code. This can be:
|
||||||
|
* * An invite code
|
||||||
|
* * An invite URL
|
||||||
|
* @typedef {string} InviteResolvable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves InviteResolvable to an invite code
|
||||||
|
* @param {InviteResolvable} data The invite resolvable to resolve
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
resolveInviteCode(data) {
|
||||||
|
const inviteRegex = /discord(?:app)?\.(?:gg|com\/invite)\/([a-z0-9]{5})/i;
|
||||||
|
const match = inviteRegex.exec(data);
|
||||||
|
|
||||||
|
if (match && match[1]) return match[1];
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data that can be resolved to give a permission number. This can be:
|
* Data that can be resolved to give a permission number. This can be:
|
||||||
* * A string
|
* * A string
|
||||||
@@ -205,6 +208,23 @@ class ClientDataResolver {
|
|||||||
return String(data);
|
return String(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
|
||||||
|
* * A Buffer
|
||||||
|
* * A Base64 string
|
||||||
|
* @typedef {Buffer|string} Base64Resolvable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a Base64Resolvable to a Base 64 image
|
||||||
|
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
|
||||||
|
* @returns {?string}
|
||||||
|
*/
|
||||||
|
resolveBase64(data) {
|
||||||
|
if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data that can be resolved to give a Buffer. This can be:
|
* Data that can be resolved to give a Buffer. This can be:
|
||||||
* * A Buffer
|
* * A Buffer
|
||||||
|
|||||||
@@ -49,19 +49,20 @@ class ClientManager {
|
|||||||
*/
|
*/
|
||||||
setupKeepAlive(time) {
|
setupKeepAlive(time) {
|
||||||
this.heartbeatInterval = this.client.setInterval(() => {
|
this.heartbeatInterval = this.client.setInterval(() => {
|
||||||
|
this.client.emit('debug', 'Sending heartbeat');
|
||||||
this.client.ws.send({
|
this.client.ws.send({
|
||||||
op: Constants.OPCodes.HEARTBEAT,
|
op: Constants.OPCodes.HEARTBEAT,
|
||||||
d: Date.now(),
|
d: this.client.ws.sequence,
|
||||||
}, true);
|
}, true);
|
||||||
}, time);
|
}, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
this.client.ws.destroy();
|
||||||
if (!this.client.user.bot) {
|
if (!this.client.user.bot) {
|
||||||
this.client.rest.methods.logout().then(resolve);
|
this.client.rest.methods.logout().then(resolve);
|
||||||
} else {
|
} else {
|
||||||
this.client.ws.destroy();
|
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
46
src/client/WebhookClient.js
Normal file
46
src/client/WebhookClient.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const Webhook = require('../structures/Webhook');
|
||||||
|
const RESTManager = require('./rest/RESTManager');
|
||||||
|
const ClientDataResolver = require('./ClientDataResolver');
|
||||||
|
const mergeDefault = require('../util/MergeDefault');
|
||||||
|
const Constants = require('../util/Constants');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Webhook Client
|
||||||
|
* @extends {Webhook}
|
||||||
|
*/
|
||||||
|
class WebhookClient extends Webhook {
|
||||||
|
/**
|
||||||
|
* @param {string} id The id of the webhook.
|
||||||
|
* @param {string} token the token of the webhook.
|
||||||
|
* @param {ClientOptions} [options] Options for the client
|
||||||
|
* @example
|
||||||
|
* // create a new webhook and send a message
|
||||||
|
* let hook = new Discord.WebhookClient('1234', 'abcdef')
|
||||||
|
* hook.sendMessage('This will send a message').catch(console.log)
|
||||||
|
*/
|
||||||
|
constructor(id, token, options) {
|
||||||
|
super(null, id, token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options the client was instantiated with
|
||||||
|
* @type {ClientOptions}
|
||||||
|
*/
|
||||||
|
this.options = mergeDefault(Constants.DefaultOptions, options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The REST manager of the client
|
||||||
|
* @type {RESTManager}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.rest = new RESTManager(this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Data Resolver of the Client
|
||||||
|
* @type {ClientDataResolver}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.resolver = new ClientDataResolver(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebhookClient;
|
||||||
@@ -20,6 +20,10 @@ class ActionsManager {
|
|||||||
this.register('UserGet');
|
this.register('UserGet');
|
||||||
this.register('UserUpdate');
|
this.register('UserUpdate');
|
||||||
this.register('GuildSync');
|
this.register('GuildSync');
|
||||||
|
this.register('GuildEmojiCreate');
|
||||||
|
this.register('GuildEmojiDelete');
|
||||||
|
this.register('GuildEmojiUpdate');
|
||||||
|
this.register('GuildRolesPositionUpdate');
|
||||||
}
|
}
|
||||||
|
|
||||||
register(name) {
|
register(name) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ChannelDeleteAction extends Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scheduleForDeletion(id) {
|
scheduleForDeletion(id) {
|
||||||
this.client.setTimeout(() => this.deleted.delete(id), this.client.options.rest_ws_bridge_timeout);
|
this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class ChannelUpdateAction extends Action {
|
|||||||
if (channel) {
|
if (channel) {
|
||||||
const oldChannel = cloneObject(channel);
|
const oldChannel = cloneObject(channel);
|
||||||
channel.setup(data);
|
channel.setup(data);
|
||||||
if (!oldChannel.equals(data)) client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel);
|
client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel);
|
||||||
return {
|
return {
|
||||||
old: oldChannel,
|
old: oldChannel,
|
||||||
updated: channel,
|
updated: channel,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class GuildDeleteAction extends Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scheduleForDeletion(id) {
|
scheduleForDeletion(id) {
|
||||||
this.client.setTimeout(() => this.deleted.delete(id), this.client.options.rest_ws_bridge_timeout);
|
this.client.setTimeout(() => this.deleted.delete(id), this.client.options.restWsBridgeTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
src/client/actions/GuildEmojiCreate.js
Normal file
18
src/client/actions/GuildEmojiCreate.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const Action = require('./Action');
|
||||||
|
|
||||||
|
class EmojiCreateAction extends Action {
|
||||||
|
handle(data, guild) {
|
||||||
|
const client = this.client;
|
||||||
|
const emoji = client.dataManager.newEmoji(data, guild);
|
||||||
|
return {
|
||||||
|
emoji,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted whenever an emoji is created
|
||||||
|
* @event Client#guildEmojiCreate
|
||||||
|
* @param {Emoji} emoji The emoji that was created.
|
||||||
|
*/
|
||||||
|
module.exports = EmojiCreateAction;
|
||||||
18
src/client/actions/GuildEmojiDelete.js
Normal file
18
src/client/actions/GuildEmojiDelete.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const Action = require('./Action');
|
||||||
|
|
||||||
|
class EmojiDeleteAction extends Action {
|
||||||
|
handle(data) {
|
||||||
|
const client = this.client;
|
||||||
|
client.dataManager.killEmoji(data);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted whenever an emoji is deleted
|
||||||
|
* @event Client#guildEmojiDelete
|
||||||
|
* @param {Emoji} emoji The emoji that was deleted.
|
||||||
|
*/
|
||||||
|
module.exports = EmojiDeleteAction;
|
||||||
29
src/client/actions/GuildEmojiUpdate.js
Normal file
29
src/client/actions/GuildEmojiUpdate.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const Action = require('./Action');
|
||||||
|
|
||||||
|
class GuildEmojiUpdateAction extends Action {
|
||||||
|
handle(data, guild) {
|
||||||
|
const client = this.client;
|
||||||
|
for (let emoji of data.emojis) {
|
||||||
|
const already = guild.emojis.has(emoji.id);
|
||||||
|
if (already) {
|
||||||
|
client.dataManager.updateEmoji(guild.emojis.get(emoji.id), emoji);
|
||||||
|
} else {
|
||||||
|
emoji = client.dataManager.newEmoji(emoji, guild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let emoji of guild.emojis) {
|
||||||
|
if (!data.emoijs.has(emoji.id)) client.dataManager.killEmoji(emoji);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
emojis: data.emojis,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted whenever an emoji is updated
|
||||||
|
* @event Client#guildEmojiUpdate
|
||||||
|
* @param {Emoji} oldEmoji The old emoji
|
||||||
|
* @param {Emoji} newEmoji The new emoji
|
||||||
|
*/
|
||||||
|
module.exports = GuildEmojiUpdateAction;
|
||||||
@@ -17,7 +17,7 @@ class GuildMemberRemoveAction extends Action {
|
|||||||
guild.memberCount--;
|
guild.memberCount--;
|
||||||
guild._removeMember(member);
|
guild._removeMember(member);
|
||||||
this.deleted.set(guild.id + data.user.id, member);
|
this.deleted.set(guild.id + data.user.id, member);
|
||||||
if (client.status === Constants.Status.READY) client.emit(Constants.Events.GUILD_MEMBER_REMOVE, guild, member);
|
if (client.status === Constants.Status.READY) client.emit(Constants.Events.GUILD_MEMBER_REMOVE, member);
|
||||||
this.scheduleForDeletion(guild.id, data.user.id);
|
this.scheduleForDeletion(guild.id, data.user.id);
|
||||||
} else {
|
} else {
|
||||||
member = this.deleted.get(guild.id + data.user.id) || null;
|
member = this.deleted.get(guild.id + data.user.id) || null;
|
||||||
@@ -36,15 +36,14 @@ class GuildMemberRemoveAction extends Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scheduleForDeletion(guildID, userID) {
|
scheduleForDeletion(guildID, userID) {
|
||||||
this.client.setTimeout(() => this.deleted.delete(guildID + userID), this.client.options.rest_ws_bridge_timeout);
|
this.client.setTimeout(() => this.deleted.delete(guildID + userID), this.client.options.restWsBridgeTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever a member leaves a guild, or is kicked.
|
* Emitted whenever a member leaves a guild, or is kicked.
|
||||||
* @event Client#guildMemberRemove
|
* @event Client#guildMemberRemove
|
||||||
* @param {Guild} guild The guild that the member has left.
|
* @param {GuildMember} member The member that has left/been kicked from the guild.
|
||||||
* @param {GuildMember} member The member that has left the guild.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = GuildMemberRemoveAction;
|
module.exports = GuildMemberRemoveAction;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class GuildRoleCreate extends Action {
|
|||||||
const already = guild.roles.has(data.role.id);
|
const already = guild.roles.has(data.role.id);
|
||||||
const role = new Role(guild, data.role);
|
const role = new Role(guild, data.role);
|
||||||
guild.roles.set(role.id, role);
|
guild.roles.set(role.id, role);
|
||||||
if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, guild, role);
|
if (!already) client.emit(Constants.Events.GUILD_ROLE_CREATE, role);
|
||||||
return {
|
return {
|
||||||
role,
|
role,
|
||||||
};
|
};
|
||||||
@@ -24,9 +24,8 @@ class GuildRoleCreate extends Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever a guild role is created.
|
* Emitted whenever a role is created.
|
||||||
* @event Client#guildRoleCreate
|
* @event Client#roleCreate
|
||||||
* @param {Guild} guild The guild that the role was created in.
|
|
||||||
* @param {Role} role The role that was created.
|
* @param {Role} role The role that was created.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class GuildRoleDeleteAction extends Action {
|
|||||||
guild.roles.delete(data.role_id);
|
guild.roles.delete(data.role_id);
|
||||||
this.deleted.set(guild.id + data.role_id, role);
|
this.deleted.set(guild.id + data.role_id, role);
|
||||||
this.scheduleForDeletion(guild.id, data.role_id);
|
this.scheduleForDeletion(guild.id, data.role_id);
|
||||||
client.emit(Constants.Events.GUILD_ROLE_DELETE, guild, role);
|
client.emit(Constants.Events.GUILD_ROLE_DELETE, role);
|
||||||
} else {
|
} else {
|
||||||
role = this.deleted.get(guild.id + data.role_id) || null;
|
role = this.deleted.get(guild.id + data.role_id) || null;
|
||||||
}
|
}
|
||||||
@@ -33,14 +33,13 @@ class GuildRoleDeleteAction extends Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scheduleForDeletion(guildID, roleID) {
|
scheduleForDeletion(guildID, roleID) {
|
||||||
this.client.setTimeout(() => this.deleted.delete(guildID + roleID), this.client.options.rest_ws_bridge_timeout);
|
this.client.setTimeout(() => this.deleted.delete(guildID + roleID), this.client.options.restWsBridgeTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever a guild role is deleted.
|
* Emitted whenever a guild role is deleted.
|
||||||
* @event Client#guildRoleDelete
|
* @event Client#roleDelete
|
||||||
* @param {Guild} guild The guild that the role was deleted in.
|
|
||||||
* @param {Role} role The role that was deleted.
|
* @param {Role} role The role that was deleted.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ class GuildRoleUpdateAction extends Action {
|
|||||||
let oldRole = null;
|
let oldRole = null;
|
||||||
|
|
||||||
const role = guild.roles.get(roleData.id);
|
const role = guild.roles.get(roleData.id);
|
||||||
if (role && !role.equals(roleData)) {
|
if (role) {
|
||||||
oldRole = cloneObject(role);
|
oldRole = cloneObject(role);
|
||||||
role.setup(data.role);
|
role.setup(data.role);
|
||||||
client.emit(Constants.Events.GUILD_ROLE_UPDATE, guild, oldRole, role);
|
client.emit(Constants.Events.GUILD_ROLE_UPDATE, oldRole, role);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -33,8 +33,7 @@ class GuildRoleUpdateAction extends Action {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever a guild role is updated.
|
* Emitted whenever a guild role is updated.
|
||||||
* @event Client#guildRoleUpdate
|
* @event Client#roleUpdate
|
||||||
* @param {Guild} guild The guild that the role was updated in.
|
|
||||||
* @param {Role} oldRole The role before the update.
|
* @param {Role} oldRole The role before the update.
|
||||||
* @param {Role} newRole The role after the update.
|
* @param {Role} newRole The role after the update.
|
||||||
*/
|
*/
|
||||||
|
|||||||
23
src/client/actions/GuildRolesPositionUpdate.js
Normal file
23
src/client/actions/GuildRolesPositionUpdate.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const Action = require('./Action');
|
||||||
|
|
||||||
|
class GuildRolesPositionUpdate extends Action {
|
||||||
|
handle(data) {
|
||||||
|
const client = this.client;
|
||||||
|
|
||||||
|
const guild = client.guilds.get(data.guild_id);
|
||||||
|
if (guild) {
|
||||||
|
for (const partialRole of data.roles) {
|
||||||
|
const role = guild.roles.get(partialRole.id);
|
||||||
|
if (role) {
|
||||||
|
role.position = partialRole.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
guild,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GuildRolesPositionUpdate;
|
||||||
@@ -8,11 +8,7 @@ class GuildSync extends Action {
|
|||||||
if (guild) {
|
if (guild) {
|
||||||
data.presences = data.presences || [];
|
data.presences = data.presences || [];
|
||||||
for (const presence of data.presences) {
|
for (const presence of data.presences) {
|
||||||
const user = client.users.get(presence.user.id);
|
guild._setPresence(presence.user.id, presence);
|
||||||
if (user) {
|
|
||||||
user.status = presence.status;
|
|
||||||
user.game = presence.game;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.members = data.members || [];
|
data.members = data.members || [];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class GuildUpdateAction extends Action {
|
|||||||
if (guild) {
|
if (guild) {
|
||||||
const oldGuild = cloneObject(guild);
|
const oldGuild = cloneObject(guild);
|
||||||
guild.setup(data);
|
guild.setup(data);
|
||||||
if (!oldGuild.equals(data)) client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild);
|
client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild);
|
||||||
return {
|
return {
|
||||||
old: oldGuild,
|
old: oldGuild,
|
||||||
updated: guild,
|
updated: guild,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class MessageDeleteAction extends Action {
|
|||||||
|
|
||||||
scheduleForDeletion(channelID, messageID) {
|
scheduleForDeletion(channelID, messageID) {
|
||||||
this.client.setTimeout(() => this.deleted.delete(channelID + messageID),
|
this.client.setTimeout(() => this.deleted.delete(channelID + messageID),
|
||||||
this.client.options.rest_ws_bridge_timeout);
|
this.client.options.restWsBridgeTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class MessageUpdateAction extends Action {
|
|||||||
const channel = client.channels.get(data.channel_id);
|
const channel = client.channels.get(data.channel_id);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
const message = channel.messages.get(data.id);
|
const message = channel.messages.get(data.id);
|
||||||
if (message && !message.equals(data, true)) {
|
if (message) {
|
||||||
const oldMessage = cloneObject(message);
|
const oldMessage = cloneObject(message);
|
||||||
message.patch(data);
|
message.patch(data);
|
||||||
message._edits.unshift(oldMessage);
|
message._edits.unshift(oldMessage);
|
||||||
|
|||||||
@@ -30,11 +30,4 @@ class UserUpdateAction extends Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted whenever a detail of the logged in User changes - e.g. username.
|
|
||||||
* @event Client#userUpdate
|
|
||||||
* @param {ClientUser} oldClientUser The client user before the update.
|
|
||||||
* @param {ClientUser} newClientUser The client user after the update.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = UserUpdateAction;
|
module.exports = UserUpdateAction;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class RESTManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRequestHandler() {
|
getRequestHandler() {
|
||||||
switch (this.client.options.api_request_method) {
|
switch (this.client.options.apiRequestMethod) {
|
||||||
case 'sequential':
|
case 'sequential':
|
||||||
return SequentialRequestHandler;
|
return SequentialRequestHandler;
|
||||||
case 'burst':
|
case 'burst':
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ const User = requireStructure('User');
|
|||||||
const GuildMember = requireStructure('GuildMember');
|
const GuildMember = requireStructure('GuildMember');
|
||||||
const Role = requireStructure('Role');
|
const Role = requireStructure('Role');
|
||||||
const Invite = requireStructure('Invite');
|
const Invite = requireStructure('Invite');
|
||||||
|
const Webhook = requireStructure('Webhook');
|
||||||
|
const UserProfile = requireStructure('UserProfile');
|
||||||
|
|
||||||
class RESTMethods {
|
class RESTMethods {
|
||||||
constructor(restManager) {
|
constructor(restManager) {
|
||||||
this.rest = restManager;
|
this.rest = restManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
loginToken(token) {
|
loginToken(token = this.rest.client.token) {
|
||||||
|
token = token.replace(/^Bot\s*/i, '');
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.rest.client.manager.connectToWebSocket(token, resolve, reject);
|
this.rest.client.manager.connectToWebSocket(token, resolve, reject);
|
||||||
});
|
});
|
||||||
@@ -26,42 +29,47 @@ class RESTMethods {
|
|||||||
this.rest.client.password = password;
|
this.rest.client.password = password;
|
||||||
this.rest.makeRequest('post', Constants.Endpoints.login, false, { email, password })
|
this.rest.makeRequest('post', Constants.Endpoints.login, false, { email, password })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.rest.client.manager.connectToWebSocket(data.token, resolve, reject);
|
resolve(this.loginToken(data.token));
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
return this.rest.makeRequest('post', Constants.Endpoints.logout, true);
|
return this.rest.makeRequest('post', Constants.Endpoints.logout, true, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
getGateway() {
|
getGateway() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.rest.makeRequest('get', Constants.Endpoints.gateway, true)
|
this.rest.makeRequest('get', Constants.Endpoints.gateway, true)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.rest.client.ws.gateway = `${res.url}/?encoding=json&v=${this.rest.client.options.protocol_version}`;
|
this.rest.client.ws.gateway = `${res.url}/?encoding=json&v=${Constants.PROTOCOL_VERSION}`;
|
||||||
resolve(this.rest.client.ws.gateway);
|
resolve(this.rest.client.ws.gateway);
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(channel, content, { tts, nonce, disable_everyone, split } = {}, file = null) {
|
getBotGateway() {
|
||||||
|
return this.rest.makeRequest('get', Constants.Endpoints.botGateway, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(channel, content, { tts, nonce, disableEveryone, split } = {}, file = null) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
|
if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
|
||||||
|
|
||||||
if (disable_everyone || (typeof disable_everyone === 'undefined' && this.rest.client.options.disable_everyone)) {
|
if (content) {
|
||||||
content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
|
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
|
||||||
}
|
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
|
||||||
|
}
|
||||||
|
|
||||||
if (split) content = splitMessage(content, typeof split === 'object' ? split : {});
|
if (split) content = splitMessage(content, typeof split === 'object' ? split : {});
|
||||||
|
}
|
||||||
|
|
||||||
if (channel instanceof User || channel instanceof GuildMember) {
|
if (channel instanceof User || channel instanceof GuildMember) {
|
||||||
this.createDM(channel).then(chan => {
|
this.createDM(channel).then(chan => {
|
||||||
this._sendMessageRequest(chan, content, file, tts, nonce, resolve, reject);
|
this._sendMessageRequest(chan, content, file, tts, nonce, resolve, reject);
|
||||||
})
|
}).catch(reject);
|
||||||
.catch(reject);
|
|
||||||
} else {
|
} else {
|
||||||
this._sendMessageRequest(channel, content, file, tts, nonce, resolve, reject);
|
this._sendMessageRequest(channel, content, file, tts, nonce, resolve, reject);
|
||||||
}
|
}
|
||||||
@@ -71,22 +79,24 @@ class RESTMethods {
|
|||||||
_sendMessageRequest(channel, content, file, tts, nonce, resolve, reject) {
|
_sendMessageRequest(channel, content, file, tts, nonce, resolve, reject) {
|
||||||
if (content instanceof Array) {
|
if (content instanceof Array) {
|
||||||
const datas = [];
|
const datas = [];
|
||||||
const promise = this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
let promise = this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
||||||
content: content[0], tts, nonce,
|
content: content[0], tts, nonce,
|
||||||
}, file).catch(reject);
|
}, file).catch(reject);
|
||||||
|
|
||||||
for (let i = 1; i <= content.length; i++) {
|
for (let i = 1; i <= content.length; i++) {
|
||||||
if (i < content.length) {
|
if (i < content.length) {
|
||||||
promise.then(data => {
|
const i2 = i;
|
||||||
|
promise = promise.then(data => {
|
||||||
datas.push(data);
|
datas.push(data);
|
||||||
return this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
return this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
||||||
content: content[i], tts, nonce,
|
content: content[i2], tts, nonce,
|
||||||
}, file);
|
}, file);
|
||||||
});
|
}).catch(reject);
|
||||||
} else {
|
} else {
|
||||||
promise.then(data => {
|
promise.then(data => {
|
||||||
datas.push(data);
|
datas.push(data);
|
||||||
resolve(this.rest.client.actions.MessageCreate.handle(datas).messages);
|
resolve(this.rest.client.actions.MessageCreate.handle(datas).messages);
|
||||||
});
|
}).catch(reject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -196,6 +206,26 @@ class RESTMethods {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createGuild(options) {
|
||||||
|
options.icon = this.rest.client.resolver.resolveBase64(options.icon) || null;
|
||||||
|
options.region = options.region || 'us-central';
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('post', Constants.Endpoints.guilds, true, options)
|
||||||
|
.then(data => {
|
||||||
|
if (this.rest.client.guilds.has(data.id)) resolve(this.rest.client.guilds.get(data.id));
|
||||||
|
const handleGuild = guild => {
|
||||||
|
if (guild.id === data.id) resolve(guild);
|
||||||
|
this.rest.client.removeListener('guildCreate', handleGuild);
|
||||||
|
};
|
||||||
|
this.rest.client.on('guildCreate', handleGuild);
|
||||||
|
this.rest.client.setTimeout(() => {
|
||||||
|
this.rest.client.removeListener('guildCreate', handleGuild);
|
||||||
|
reject(new Error('Took too long to receive guild data'));
|
||||||
|
}, 10000);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// untested but probably will work
|
// untested but probably will work
|
||||||
deleteGuild(guild) {
|
deleteGuild(guild) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -363,26 +393,27 @@ class RESTMethods {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
banGuildMember(guild, member, deleteDays) {
|
banGuildMember(guild, member, deleteDays = 0) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = this.rest.client.resolver.resolveUserID(member);
|
const id = this.rest.client.resolver.resolveUserID(member);
|
||||||
if (!id) throw new Error('Couldn\'t resolve the user ID to ban.');
|
if (!id) throw new Error('Couldn\'t resolve the user ID to ban.');
|
||||||
|
|
||||||
this.rest.makeRequest('put', `${Constants.Endpoints.guildBans(guild.id)}/${id}`, true, {
|
this.rest.makeRequest('put',
|
||||||
'delete-message-days': deleteDays,
|
`${Constants.Endpoints.guildBans(guild.id)}/${id}?delete-message-days=${deleteDays}`, true, {
|
||||||
}).then(() => {
|
'delete-message-days': deleteDays,
|
||||||
if (member instanceof GuildMember) {
|
}).then(() => {
|
||||||
resolve(member);
|
if (member instanceof GuildMember) {
|
||||||
return;
|
resolve(member);
|
||||||
}
|
return;
|
||||||
const user = this.rest.client.resolver.resolveUser(id);
|
}
|
||||||
if (user) {
|
const user = this.rest.client.resolver.resolveUser(id);
|
||||||
member = this.rest.client.resolver.resolveGuildMember(guild, user);
|
if (user) {
|
||||||
resolve(member || user);
|
member = this.rest.client.resolver.resolveGuildMember(guild, user);
|
||||||
return;
|
resolve(member || user);
|
||||||
}
|
return;
|
||||||
resolve(id);
|
}
|
||||||
}).catch(reject);
|
resolve(id);
|
||||||
|
}).catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,12 +450,13 @@ class RESTMethods {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const data = {};
|
const data = {};
|
||||||
data.name = _data.name || role.name;
|
data.name = _data.name || role.name;
|
||||||
data.position = _data.position || role.position;
|
data.position = typeof _data.position !== 'undefined' ? _data.position : role.position;
|
||||||
data.color = _data.color || role.color;
|
data.color = _data.color || role.color;
|
||||||
if (typeof data.color === 'string' && data.color.startsWith('#')) {
|
if (typeof data.color === 'string' && data.color.startsWith('#')) {
|
||||||
data.color = parseInt(data.color.replace('#', ''), 16);
|
data.color = parseInt(data.color.replace('#', ''), 16);
|
||||||
}
|
}
|
||||||
data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
|
data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
|
||||||
|
data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable;
|
||||||
|
|
||||||
if (_data.permissions) {
|
if (_data.permissions) {
|
||||||
let perms = 0;
|
let perms = 0;
|
||||||
@@ -516,6 +548,178 @@ class RESTMethods {
|
|||||||
}).catch(reject);
|
}).catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEmoji(guild, image, name) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('post', `${Constants.Endpoints.guildEmojis(guild.id)}`, true, { name: name, image: image })
|
||||||
|
.then(data => {
|
||||||
|
resolve(this.rest.client.actions.EmojiCreate.handle(data, guild).emoji);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEmoji(emoji) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('delete', `${Constants.Endpoints.guildEmojis(emoji.guild.id)}/${emoji.id}`, true)
|
||||||
|
.then(() => {
|
||||||
|
resolve(this.rest.client.actions.EmojiDelete.handle(emoji).data);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getWebhook(id, token) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('get', Constants.Endpoints.webhook(id, token), require('util').isUndefined(token))
|
||||||
|
.then(data => {
|
||||||
|
resolve(new Webhook(this.rest.client, data));
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getGuildWebhooks(guild) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('get', Constants.Endpoints.guildWebhooks(guild.id), true)
|
||||||
|
.then(data => {
|
||||||
|
const hooks = new Collection();
|
||||||
|
for (const hook of data) {
|
||||||
|
hooks.set(hook.id, new Webhook(this.rest.client, hook));
|
||||||
|
}
|
||||||
|
resolve(hooks);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getChannelWebhooks(channel) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('get', Constants.Endpoints.channelWebhooks(channel.id), true)
|
||||||
|
.then(data => {
|
||||||
|
const hooks = new Collection();
|
||||||
|
for (const hook of data) {
|
||||||
|
hooks.set(hook.id, new Webhook(this.rest.client, hook));
|
||||||
|
}
|
||||||
|
resolve(hooks);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createWebhook(channel, name, avatar) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('post', Constants.Endpoints.channelWebhooks(channel.id), true, {
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
resolve(new Webhook(this.rest.client, data));
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editWebhook(webhook, name, avatar) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('patch', Constants.Endpoints.webhook(webhook.id, webhook.token), false, {
|
||||||
|
name,
|
||||||
|
avatar,
|
||||||
|
}).then(data => {
|
||||||
|
webhook.name = data.name;
|
||||||
|
webhook.avatar = data.avatar;
|
||||||
|
resolve(webhook);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteWebhook(webhook) {
|
||||||
|
return this.rest.makeRequest('delete', Constants.Endpoints.webhook(webhook.id, webhook.token), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendWebhookMessage(webhook, content, { avatarURL, tts, disableEveryone, embeds } = {}, file = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
|
||||||
|
|
||||||
|
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
|
||||||
|
content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rest.makeRequest('post', `${Constants.Endpoints.webhook(webhook.id, webhook.token)}?wait=true`, false, {
|
||||||
|
content: content, username: webhook.name, avatar_url: avatarURL, tts: tts, file: file, embeds: embeds,
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
resolve(data);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSlackWebhookMessage(webhook, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest(
|
||||||
|
'post',
|
||||||
|
`${Constants.Endpoints.webhook(webhook.id, webhook.token)}/slack?wait=true`,
|
||||||
|
false,
|
||||||
|
body
|
||||||
|
).then(data => {
|
||||||
|
resolve(data);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addFriend(user) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('post', Constants.Endpoints.relationships('@me'), true, {
|
||||||
|
discriminator: user.discriminator,
|
||||||
|
username: user.username,
|
||||||
|
}).then(() => {
|
||||||
|
resolve(user);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFriend(user) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true)
|
||||||
|
.then(() => {
|
||||||
|
resolve(user);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUserProfile(user) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('get', Constants.Endpoints.userProfile(user.id), true)
|
||||||
|
.then(data => {
|
||||||
|
resolve(new UserProfile(user, data));
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
blockUser(user) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('put', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true, { type: 2 })
|
||||||
|
.then(() => {
|
||||||
|
resolve(user);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unblockUser(user) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('delete', `${Constants.Endpoints.relationships('@me')}/${user.id}`, true)
|
||||||
|
.then(() => {
|
||||||
|
resolve(user);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setRolePositions(guildID, roles) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.rest.makeRequest('patch', Constants.Endpoints.guildRoles(guildID), true, roles)
|
||||||
|
.then(() => {
|
||||||
|
resolve(this.rest.client.actions.GuildRolesPositionUpdate.handle({
|
||||||
|
guild_id: guildID,
|
||||||
|
roles,
|
||||||
|
}).guild);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = RESTMethods;
|
module.exports = RESTMethods;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const Collection = require('../../util/Collection');
|
|||||||
const mergeDefault = require('../../util/MergeDefault');
|
const mergeDefault = require('../../util/MergeDefault');
|
||||||
const Constants = require('../../util/Constants');
|
const Constants = require('../../util/Constants');
|
||||||
const VoiceConnection = require('./VoiceConnection');
|
const VoiceConnection = require('./VoiceConnection');
|
||||||
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages all the voice stuff for the Client
|
* Manages all the voice stuff for the Client
|
||||||
@@ -26,52 +27,17 @@ class ClientVoiceManager {
|
|||||||
* @type {Collection<string, VoiceChannel>}
|
* @type {Collection<string, VoiceChannel>}
|
||||||
*/
|
*/
|
||||||
this.pending = new Collection();
|
this.pending = new Collection();
|
||||||
|
|
||||||
|
this.client.on('self.voiceServer', this.onVoiceServer.bind(this));
|
||||||
|
this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
onVoiceServer(data) {
|
||||||
* Checks whether a pending request can be processed
|
if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint);
|
||||||
* @private
|
|
||||||
* @param {string} guildID The ID of the Guild
|
|
||||||
*/
|
|
||||||
_checkPendingReady(guildID) {
|
|
||||||
const pendingRequest = this.pending.get(guildID);
|
|
||||||
if (!pendingRequest) throw new Error('Guild not pending.');
|
|
||||||
if (pendingRequest.token && pendingRequest.sessionID && pendingRequest.endpoint) {
|
|
||||||
const { channel, token, sessionID, endpoint, resolve, reject } = pendingRequest;
|
|
||||||
const voiceConnection = new VoiceConnection(this, channel, token, sessionID, endpoint, resolve, reject);
|
|
||||||
this.pending.delete(guildID);
|
|
||||||
this.connections.set(guildID, voiceConnection);
|
|
||||||
voiceConnection.once('disconnected', () => {
|
|
||||||
this.connections.delete(guildID);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
onVoiceStateUpdate(data) {
|
||||||
* Called when the Client receives information about this voice server update.
|
if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setSessionID(data.session_id);
|
||||||
* @param {string} guildID The ID of the Guild
|
|
||||||
* @param {string} token The token to authorise with
|
|
||||||
* @param {string} endpoint The endpoint to connect to
|
|
||||||
*/
|
|
||||||
_receivedVoiceServer(guildID, token, endpoint) {
|
|
||||||
const pendingRequest = this.pending.get(guildID);
|
|
||||||
if (!pendingRequest) throw new Error('Guild not pending.');
|
|
||||||
pendingRequest.token = token;
|
|
||||||
// remove the port otherwise it errors ¯\_(ツ)_/¯
|
|
||||||
pendingRequest.endpoint = endpoint.match(/([^:]*)/)[0];
|
|
||||||
this._checkPendingReady(guildID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the Client receives information about the voice state update.
|
|
||||||
* @param {string} guildID The ID of the Guild
|
|
||||||
* @param {string} sessionID The session id to authorise with
|
|
||||||
*/
|
|
||||||
_receivedVoiceStateUpdate(guildID, sessionID) {
|
|
||||||
const pendingRequest = this.pending.get(guildID);
|
|
||||||
if (!pendingRequest) throw new Error('Guild not pending.');
|
|
||||||
pendingRequest.sessionID = sessionID;
|
|
||||||
this._checkPendingReady(guildID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,13 +45,26 @@ class ClientVoiceManager {
|
|||||||
* @param {VoiceChannel} channel The channel to join
|
* @param {VoiceChannel} channel The channel to join
|
||||||
* @param {Object} [options] The options to provide
|
* @param {Object} [options] The options to provide
|
||||||
*/
|
*/
|
||||||
_sendWSJoin(channel, options = {}) {
|
sendVoiceStateUpdate(channel, options = {}) {
|
||||||
|
if (!this.client.user) throw new Error('Unable to join because there is no client user.');
|
||||||
|
if (!channel.permissionsFor) {
|
||||||
|
throw new Error('Channel does not support permissionsFor; is it really a voice channel?');
|
||||||
|
}
|
||||||
|
const permissions = channel.permissionsFor(this.client.user);
|
||||||
|
if (!permissions) {
|
||||||
|
throw new Error('There is no permission set for the client user in this channel - are they part of the guild?');
|
||||||
|
}
|
||||||
|
if (!permissions.hasPermission('CONNECT')) {
|
||||||
|
throw new Error('You do not have permission to connect to this voice channel.');
|
||||||
|
}
|
||||||
|
|
||||||
options = mergeDefault({
|
options = mergeDefault({
|
||||||
guild_id: channel.guild.id,
|
guild_id: channel.guild.id,
|
||||||
channel_id: channel.id,
|
channel_id: channel.id,
|
||||||
self_mute: false,
|
self_mute: false,
|
||||||
self_deaf: false,
|
self_deaf: false,
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
this.client.ws.send({
|
this.client.ws.send({
|
||||||
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
||||||
d: options,
|
d: options,
|
||||||
@@ -99,28 +78,171 @@ 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 a channel in guild.`);
|
if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.');
|
||||||
const existingConn = this.connections.get(channel.guild.id);
|
|
||||||
if (existingConn) {
|
if (!channel.permissionsFor(this.client.user).hasPermission('CONNECT')) {
|
||||||
if (existingConn.channel.id !== channel.id) {
|
throw new Error('You do not have permission to join this voice channel');
|
||||||
this._sendWSJoin(channel);
|
}
|
||||||
|
|
||||||
|
const existingConnection = this.connections.get(channel.guild.id);
|
||||||
|
if (existingConnection) {
|
||||||
|
if (existingConnection.channel.id !== channel.id) {
|
||||||
|
this.sendVoiceStateUpdate(channel);
|
||||||
this.connections.get(channel.guild.id).channel = channel;
|
this.connections.get(channel.guild.id).channel = channel;
|
||||||
}
|
}
|
||||||
resolve(existingConn);
|
resolve(existingConnection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.pending.set(channel.guild.id, {
|
|
||||||
channel,
|
const pendingConnection = new PendingVoiceConnection(this, channel);
|
||||||
sessionID: null,
|
this.pending.set(channel.guild.id, pendingConnection);
|
||||||
token: null,
|
|
||||||
endpoint: null,
|
pendingConnection.on('fail', reason => {
|
||||||
resolve,
|
this.pending.delete(channel.guild.id);
|
||||||
reject,
|
reject(reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
pendingConnection.on('pass', voiceConnection => {
|
||||||
|
this.pending.delete(channel.guild.id);
|
||||||
|
this.connections.set(channel.guild.id, voiceConnection);
|
||||||
|
voiceConnection.once('ready', () => resolve(voiceConnection));
|
||||||
|
voiceConnection.once('error', reject);
|
||||||
|
voiceConnection.once('disconnect', () => this.connections.delete(channel.guild.id));
|
||||||
});
|
});
|
||||||
this._sendWSJoin(channel);
|
|
||||||
this.client.setTimeout(() => reject(new Error('Connection not established within 15 seconds.')), 15000);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Pending Voice Connection
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class PendingVoiceConnection extends EventEmitter {
|
||||||
|
constructor(voiceManager, channel) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ClientVoiceManager that instantiated this pending connection
|
||||||
|
* @type {ClientVoiceManager}
|
||||||
|
*/
|
||||||
|
this.voiceManager = voiceManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel that this pending voice connection will attempt to join
|
||||||
|
* @type {VoiceChannel}
|
||||||
|
*/
|
||||||
|
this.channel = channel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timeout that will be invoked after 15 seconds signifying a failure to connect
|
||||||
|
* @type {Timeout}
|
||||||
|
*/
|
||||||
|
this.deathTimer = this.voiceManager.client.setTimeout(
|
||||||
|
() => this.fail(new Error('Connection not established within 15 seconds.')), 15000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing data required to connect to the voice servers with
|
||||||
|
* @type {object}
|
||||||
|
*/
|
||||||
|
this.data = {};
|
||||||
|
|
||||||
|
this.sendVoiceStateUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkReady() {
|
||||||
|
if (this.data.token && this.data.endpoint && this.data.session_id) {
|
||||||
|
this.pass();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the token and endpoint required to connect to the the voice servers
|
||||||
|
* @param {string} token the token
|
||||||
|
* @param {string} endpoint the endpoint
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
setTokenAndEndpoint(token, endpoint) {
|
||||||
|
if (!token) {
|
||||||
|
this.fail(new Error('Token not provided from voice server packet.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!endpoint) {
|
||||||
|
this.fail(new Error('Endpoint not provided from voice server packet.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.data.token) {
|
||||||
|
this.fail(new Error('There is already a registered token for this connection.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.data.endpoint) {
|
||||||
|
this.fail(new Error('There is already a registered endpoint for this connection.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint = endpoint.match(/([^:]*)/)[0];
|
||||||
|
|
||||||
|
if (!endpoint) {
|
||||||
|
this.fail(new Error('Failed to find an endpoint.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data.token = token;
|
||||||
|
this.data.endpoint = endpoint;
|
||||||
|
|
||||||
|
this.checkReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Session ID for the connection
|
||||||
|
* @param {string} sessionID the session ID
|
||||||
|
*/
|
||||||
|
setSessionID(sessionID) {
|
||||||
|
if (!sessionID) {
|
||||||
|
this.fail(new Error('Session ID not supplied.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.data.session_id) {
|
||||||
|
this.fail(new Error('There is already a registered session ID for this connection.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.data.session_id = sessionID;
|
||||||
|
|
||||||
|
this.checkReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
|
clearInterval(this.deathTimer);
|
||||||
|
this.emit('fail', new Error('Clean-up triggered :fourTriggered:'));
|
||||||
|
}
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
clearInterval(this.deathTimer);
|
||||||
|
this.emit('pass', this.upgrade());
|
||||||
|
}
|
||||||
|
|
||||||
|
fail(reason) {
|
||||||
|
this.emit('fail', reason);
|
||||||
|
this.clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendVoiceStateUpdate() {
|
||||||
|
try {
|
||||||
|
this.voiceManager.sendVoiceStateUpdate(this.channel);
|
||||||
|
} catch (error) {
|
||||||
|
this.fail(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrades this Pending Connection to a full Voice Connection
|
||||||
|
* @returns {VoiceConnection}
|
||||||
|
*/
|
||||||
|
upgrade() {
|
||||||
|
return new VoiceConnection(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = ClientVoiceManager;
|
module.exports = ClientVoiceManager;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
const VoiceConnectionWebSocket = require('./VoiceConnectionWebSocket');
|
const VoiceWebSocket = require('./VoiceWebSocket');
|
||||||
const VoiceConnectionUDPClient = require('./VoiceConnectionUDPClient');
|
const VoiceUDP = require('./VoiceUDPClient');
|
||||||
const VoiceReceiver = require('./receiver/VoiceReceiver');
|
|
||||||
const Constants = require('../../util/Constants');
|
const Constants = require('../../util/Constants');
|
||||||
|
const AudioPlayer = require('./player/AudioPlayer');
|
||||||
|
const VoiceReceiver = require('./receiver/VoiceReceiver');
|
||||||
const EventEmitter = require('events').EventEmitter;
|
const EventEmitter = require('events').EventEmitter;
|
||||||
const DefaultPlayer = require('./player/DefaultPlayer');
|
const fs = require('fs');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a connection to a Voice Channel in Discord.
|
* Represents a connection to a Voice Channel in Discord.
|
||||||
@@ -16,89 +17,101 @@ const DefaultPlayer = require('./player/DefaultPlayer');
|
|||||||
* @extends {EventEmitter}
|
* @extends {EventEmitter}
|
||||||
*/
|
*/
|
||||||
class VoiceConnection extends EventEmitter {
|
class VoiceConnection extends EventEmitter {
|
||||||
constructor(manager, channel, token, sessionID, endpoint, resolve, reject) {
|
|
||||||
|
constructor(pendingConnection) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The voice manager of this connection
|
* The Voice Manager that instantiated this connection
|
||||||
* @type {ClientVoiceManager}
|
* @type {ClientVoiceManager}
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
this.manager = manager;
|
this.voiceManager = pendingConnection.voiceManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The player
|
* The voice channel this connection is currently serving
|
||||||
* @type {BasePlayer}
|
|
||||||
*/
|
|
||||||
this.player = new DefaultPlayer(this);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The endpoint of the connection
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.endpoint = endpoint;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The VoiceChannel for this connection
|
|
||||||
* @type {VoiceChannel}
|
* @type {VoiceChannel}
|
||||||
*/
|
*/
|
||||||
this.channel = channel;
|
this.channel = pendingConnection.channel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The WebSocket connection for this voice connection
|
* An array of Voice Receivers that have been created for this connection
|
||||||
* @type {VoiceConnectionWebSocket}
|
* @type {VoiceReceiver[]}
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
this.websocket = new VoiceConnectionWebSocket(this, channel.guild.id, token, sessionID, endpoint);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the connection is ready
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
this.ready = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The resolve function for the promise associated with creating this connection
|
|
||||||
* @type {function}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._resolve = resolve;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The reject function for the promise associated with creating this connection
|
|
||||||
* @type {function}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._reject = reject;
|
|
||||||
|
|
||||||
this.ssrcMap = new Map();
|
|
||||||
this.queue = [];
|
|
||||||
this.receivers = [];
|
this.receivers = [];
|
||||||
this.bindListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executed whenever an error occurs with the UDP/WebSocket sub-client.
|
|
||||||
* @private
|
|
||||||
* @param {Error} err The encountered error
|
|
||||||
*/
|
|
||||||
_onError(err) {
|
|
||||||
this._reject(err);
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever the connection encounters a fatal error.
|
* The authentication data needed to connect to the voice server
|
||||||
* @event VoiceConnection#error
|
* @type {object}
|
||||||
* @param {Error} error The encountered error
|
* @private
|
||||||
*/
|
*/
|
||||||
this.emit('error', err);
|
this.authentication = pendingConnection.data;
|
||||||
this._shutdown(err);
|
|
||||||
|
/**
|
||||||
|
* The audio player for this voice connection
|
||||||
|
* @type {AudioPlayer}
|
||||||
|
*/
|
||||||
|
this.player = new AudioPlayer(this);
|
||||||
|
|
||||||
|
this.player.on('debug', m => {
|
||||||
|
/**
|
||||||
|
* Debug info from the connection
|
||||||
|
* @event VoiceConnection#debug
|
||||||
|
* @param {string} message the debug message
|
||||||
|
*/
|
||||||
|
this.emit('debug', `audio player - ${m}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.player.on('error', e => {
|
||||||
|
/**
|
||||||
|
* Warning info from the connection
|
||||||
|
* @event VoiceConnection#warn
|
||||||
|
* @param {string|error} warning the warning
|
||||||
|
*/
|
||||||
|
this.emit('warn', e);
|
||||||
|
this.player.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map SSRC to speaking values
|
||||||
|
* @type {Map<number, boolean>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.ssrcMap = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object that wraps contains the `ws` and `udp` sockets of this voice connection
|
||||||
|
* @type {object}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.sockets = {};
|
||||||
|
this.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnects the Client from the Voice Channel.
|
* Sets whether the voice connection should display as "speaking" or not
|
||||||
* @param {string} [reason='user requested'] The reason of the disconnection
|
* @param {boolean} value whether or not to speak
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
disconnect(reason = 'user requested') {
|
setSpeaking(value) {
|
||||||
this.manager.client.ws.send({
|
if (this.speaking === value) return;
|
||||||
|
this.speaking = value;
|
||||||
|
this.sockets.ws.sendPacket({
|
||||||
|
op: Constants.VoiceOPCodes.SPEAKING,
|
||||||
|
d: {
|
||||||
|
speaking: true,
|
||||||
|
delay: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.emit('debug', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the voice connection, causing a disconnect and closing event to be emitted.
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
this.emit('closing');
|
||||||
|
this.voiceManager.client.ws.send({
|
||||||
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
||||||
d: {
|
d: {
|
||||||
guild_id: this.channel.guild.id,
|
guild_id: this.channel.guild.id,
|
||||||
@@ -107,81 +120,51 @@ class VoiceConnection extends EventEmitter {
|
|||||||
self_deaf: false,
|
self_deaf: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this._shutdown(reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onClose(e) {
|
|
||||||
e = e && e.code === 1000 ? null : e;
|
|
||||||
return this._shutdown(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
_shutdown(e) {
|
|
||||||
if (!this.ready) return;
|
|
||||||
this.ready = false;
|
|
||||||
this.websocket._shutdown();
|
|
||||||
this.player._shutdown();
|
|
||||||
if (this.udp) this.udp._shutdown();
|
|
||||||
if (this._vsUpdateListener) this.manager.client.removeListener('voiceStateUpdate', this._vsUpdateListener);
|
|
||||||
/**
|
/**
|
||||||
* Emit once the voice connection has disconnected.
|
* Emitted when the voice connection disconnects
|
||||||
* @event VoiceConnection#disconnected
|
* @event VoiceConnection#disconnect
|
||||||
* @param {Error} error The encountered error, if any
|
|
||||||
*/
|
*/
|
||||||
this.emit('disconnected', e);
|
this.emit('disconnect');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds listeners to the WebSocket and UDP sub-clients.
|
* Connect the voice connection
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
bindListeners() {
|
connect() {
|
||||||
this.websocket.on('error', err => this._onError(err));
|
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
|
||||||
this.websocket.on('close', err => this._onClose(err));
|
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
|
||||||
this.websocket.on('ready-for-udp', data => {
|
this.sockets.ws = new VoiceWebSocket(this);
|
||||||
this.udp = new VoiceConnectionUDPClient(this, data);
|
this.sockets.udp = new VoiceUDP(this);
|
||||||
this.data = data;
|
this.sockets.ws.on('error', e => this.emit('error', e));
|
||||||
this.udp.on('error', err => this._onError(err));
|
this.sockets.udp.on('error', e => this.emit('error', e));
|
||||||
this.udp.on('close', err => this._onClose(err));
|
this.sockets.ws.once('ready', d => {
|
||||||
});
|
this.authentication.port = d.port;
|
||||||
this.websocket.on('ready', secretKey => {
|
this.authentication.ssrc = d.ssrc;
|
||||||
this.data.secret = secretKey;
|
|
||||||
this.ready = true;
|
|
||||||
/**
|
/**
|
||||||
* Emitted once the connection is ready (joining voice channels resolves when the connection is ready anyway)
|
* Emitted whenever the connection encounters an error.
|
||||||
|
* @event VoiceConnection#error
|
||||||
|
* @param {Error} error the encountered error
|
||||||
|
*/
|
||||||
|
this.sockets.udp.findEndpointAddress()
|
||||||
|
.then(address => {
|
||||||
|
this.sockets.udp.createUDPSocket(address);
|
||||||
|
})
|
||||||
|
.catch(e => this.emit('error', e));
|
||||||
|
});
|
||||||
|
this.sockets.ws.once('sessionDescription', (mode, secret) => {
|
||||||
|
this.authentication.encryptionMode = mode;
|
||||||
|
this.authentication.secretKey = secret;
|
||||||
|
/**
|
||||||
|
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
|
||||||
|
* the connection will already be ready.
|
||||||
* @event VoiceConnection#ready
|
* @event VoiceConnection#ready
|
||||||
*/
|
*/
|
||||||
this._resolve(this);
|
|
||||||
this.emit('ready');
|
this.emit('ready');
|
||||||
});
|
});
|
||||||
this.once('ready', () => {
|
this.sockets.ws.on('speaking', data => {
|
||||||
setImmediate(() => {
|
|
||||||
for (const item of this.queue) this.emit(...item);
|
|
||||||
this.queue = [];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this._vsUpdateListener = (oldM, newM) => {
|
|
||||||
if (oldM.voiceChannel && oldM.voiceChannel.guild.id === this.channel.guild.id && !newM.voiceChannel) {
|
|
||||||
const user = newM.user;
|
|
||||||
for (const receiver of this.receivers) {
|
|
||||||
const opusStream = receiver.opusStreams.get(user.id);
|
|
||||||
const pcmStream = receiver.pcmStreams.get(user.id);
|
|
||||||
if (opusStream) {
|
|
||||||
opusStream.push(null);
|
|
||||||
opusStream.open = false;
|
|
||||||
receiver.opusStreams.delete(user.id);
|
|
||||||
}
|
|
||||||
if (pcmStream) {
|
|
||||||
pcmStream.push(null);
|
|
||||||
pcmStream.open = false;
|
|
||||||
receiver.pcmStreams.delete(user.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.manager.client.on(Constants.Events.VOICE_STATE_UPDATE, this._vsUpdateListener);
|
|
||||||
this.websocket.on('speaking', data => {
|
|
||||||
const guild = this.channel.guild;
|
const guild = this.channel.guild;
|
||||||
const user = this.manager.client.users.get(data.user_id);
|
const user = this.voiceManager.client.users.get(data.user_id);
|
||||||
this.ssrcMap.set(+data.ssrc, user);
|
this.ssrcMap.set(+data.ssrc, user);
|
||||||
if (!data.speaking) {
|
if (!data.speaking) {
|
||||||
for (const receiver of this.receivers) {
|
for (const receiver of this.receivers) {
|
||||||
@@ -206,7 +189,6 @@ class VoiceConnection extends EventEmitter {
|
|||||||
* @param {boolean} speaking Whether or not the user is speaking
|
* @param {boolean} speaking Whether or not the user is speaking
|
||||||
*/
|
*/
|
||||||
if (this.ready) this.emit('speaking', user, data.speaking);
|
if (this.ready) this.emit('speaking', user, data.speaking);
|
||||||
else this.queue.push(['speaking', user, data.speaking]);
|
|
||||||
guild._memberSpeakUpdate(data.user_id, data.speaking);
|
guild._memberSpeakUpdate(data.user_id, data.speaking);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -232,9 +214,8 @@ class VoiceConnection extends EventEmitter {
|
|||||||
* })
|
* })
|
||||||
* .catch(console.log);
|
* .catch(console.log);
|
||||||
*/
|
*/
|
||||||
playFile(file, { seek = 0, volume = 1, passes = 1 } = {}) {
|
playFile(file, options) {
|
||||||
const options = { seek, volume, passes };
|
return this.playStream(fs.createReadStream(file), options);
|
||||||
return this.player.playFile(file, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -255,7 +236,7 @@ class VoiceConnection extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||||
const options = { seek, volume, passes };
|
const options = { seek, volume, passes };
|
||||||
return this.player.playStream(stream, options);
|
return this.player.playUnknownStream(stream, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,7 +247,6 @@ 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 };
|
||||||
this.player._shutdown();
|
|
||||||
return this.player.playPCMStream(stream, options);
|
return this.player.playPCMStream(stream, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,9 +255,9 @@ class VoiceConnection extends EventEmitter {
|
|||||||
* @returns {VoiceReceiver}
|
* @returns {VoiceReceiver}
|
||||||
*/
|
*/
|
||||||
createReceiver() {
|
createReceiver() {
|
||||||
const rcv = new VoiceReceiver(this);
|
const receiver = new VoiceReceiver(this);
|
||||||
this.receivers.push(rcv);
|
this.receivers.push(receiver);
|
||||||
return rcv;
|
return receiver;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
const udp = require('dgram');
|
|
||||||
const dns = require('dns');
|
|
||||||
const Constants = require('../../util/Constants');
|
|
||||||
const EventEmitter = require('events').EventEmitter;
|
|
||||||
|
|
||||||
class VoiceConnectionUDPClient extends EventEmitter {
|
|
||||||
constructor(voiceConnection, data) {
|
|
||||||
super();
|
|
||||||
this.voiceConnection = voiceConnection;
|
|
||||||
this.count = 0;
|
|
||||||
this.data = data;
|
|
||||||
this.dnsLookup();
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsLookup() {
|
|
||||||
dns.lookup(this.voiceConnection.endpoint, (err, address) => {
|
|
||||||
if (err) {
|
|
||||||
this.emit('error', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.connectUDP(address);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
send(packet) {
|
|
||||||
if (this.udpSocket) {
|
|
||||||
try {
|
|
||||||
this.udpSocket.send(packet, 0, packet.length, this.data.port, this.udpIP);
|
|
||||||
} catch (err) {
|
|
||||||
this.emit('error', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_shutdown() {
|
|
||||||
if (this.udpSocket) {
|
|
||||||
try {
|
|
||||||
this.udpSocket.close();
|
|
||||||
} catch (err) {
|
|
||||||
if (err.message !== 'Not running') this.emit('error', err);
|
|
||||||
}
|
|
||||||
this.udpSocket = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectUDP(address) {
|
|
||||||
this.udpIP = address;
|
|
||||||
this.udpSocket = udp.createSocket('udp4');
|
|
||||||
|
|
||||||
// finding local IP
|
|
||||||
// https://discordapp.com/developers/docs/topics/voice-connections#ip-discovery
|
|
||||||
this.udpSocket.once('message', message => {
|
|
||||||
const packet = new Buffer(message);
|
|
||||||
this.localIP = '';
|
|
||||||
for (let i = 4; i < packet.indexOf(0, i); i++) this.localIP += String.fromCharCode(packet[i]);
|
|
||||||
this.localPort = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
|
|
||||||
|
|
||||||
this.voiceConnection.websocket.send({
|
|
||||||
op: Constants.VoiceOPCodes.SELECT_PROTOCOL,
|
|
||||||
d: {
|
|
||||||
protocol: 'udp',
|
|
||||||
data: {
|
|
||||||
address: this.localIP,
|
|
||||||
port: this.localPort,
|
|
||||||
mode: 'xsalsa20_poly1305',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.udpSocket.on('error', (error, message) => {
|
|
||||||
this.emit('error', { error, message });
|
|
||||||
});
|
|
||||||
this.udpSocket.on('close', error => {
|
|
||||||
this.emit('close', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const blankMessage = new Buffer(70);
|
|
||||||
blankMessage.writeUIntBE(this.data.ssrc, 0, 4);
|
|
||||||
this.send(blankMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = VoiceConnectionUDPClient;
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
const WebSocket = require('ws');
|
|
||||||
const Constants = require('../../util/Constants');
|
|
||||||
const EventEmitter = require('events').EventEmitter;
|
|
||||||
|
|
||||||
class VoiceConnectionWebSocket extends EventEmitter {
|
|
||||||
constructor(voiceConnection, serverID, token, sessionID, endpoint) {
|
|
||||||
super();
|
|
||||||
this.voiceConnection = voiceConnection;
|
|
||||||
this.token = token;
|
|
||||||
this.sessionID = sessionID;
|
|
||||||
this.serverID = serverID;
|
|
||||||
this.heartbeat = null;
|
|
||||||
this.opened = false;
|
|
||||||
this.endpoint = endpoint;
|
|
||||||
this.attempts = 6;
|
|
||||||
this.setupWS();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupWS() {
|
|
||||||
this.attempts--;
|
|
||||||
this.ws = new WebSocket(`wss://${this.endpoint}`, null, { rejectUnauthorized: false });
|
|
||||||
this.ws.onopen = () => this._onOpen();
|
|
||||||
this.ws.onmessage = e => this._onMessage(e);
|
|
||||||
this.ws.onclose = e => this._onClose(e);
|
|
||||||
this.ws.onerror = e => this._onError(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
send(data) {
|
|
||||||
if (this.ws.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
_shutdown() {
|
|
||||||
if (this.ws) this.ws.close();
|
|
||||||
this.voiceConnection.manager.client.clearInterval(this.heartbeat);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onOpen() {
|
|
||||||
this.opened = true;
|
|
||||||
this.send({
|
|
||||||
op: Constants.OPCodes.DISPATCH,
|
|
||||||
d: {
|
|
||||||
server_id: this.serverID,
|
|
||||||
user_id: this.voiceConnection.manager.client.user.id,
|
|
||||||
session_id: this.sessionID,
|
|
||||||
token: this.token,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onClose(err) {
|
|
||||||
if (!this.opened && this.attempts >= 0) {
|
|
||||||
this.setupWS();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.emit('close', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onError(e) {
|
|
||||||
if (!this.opened && this.attempts >= 0) {
|
|
||||||
this.setupWS();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.emit('error', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
_setHeartbeat(interval) {
|
|
||||||
this.heartbeat = this.voiceConnection.manager.client.setInterval(() => {
|
|
||||||
this.send({
|
|
||||||
op: Constants.VoiceOPCodes.HEARTBEAT,
|
|
||||||
d: null,
|
|
||||||
});
|
|
||||||
}, interval);
|
|
||||||
this.send({
|
|
||||||
op: Constants.VoiceOPCodes.HEARTBEAT,
|
|
||||||
d: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onMessage(event) {
|
|
||||||
let packet;
|
|
||||||
try {
|
|
||||||
packet = JSON.parse(event.data);
|
|
||||||
} catch (error) {
|
|
||||||
this._onError(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (packet.op) {
|
|
||||||
case Constants.VoiceOPCodes.READY:
|
|
||||||
this._setHeartbeat(packet.d.heartbeat_interval);
|
|
||||||
this.emit('ready-for-udp', packet.d);
|
|
||||||
break;
|
|
||||||
case Constants.VoiceOPCodes.SESSION_DESCRIPTION:
|
|
||||||
this.encryptionMode = packet.d.mode;
|
|
||||||
this.secretKey = new Uint8Array(new ArrayBuffer(packet.d.secret_key.length));
|
|
||||||
for (const index in packet.d.secret_key) this.secretKey[index] = packet.d.secret_key[index];
|
|
||||||
this.emit('ready', this.secretKey);
|
|
||||||
break;
|
|
||||||
case Constants.VoiceOPCodes.SPEAKING:
|
|
||||||
/*
|
|
||||||
{ op: 5,
|
|
||||||
d: { user_id: '123123', ssrc: 1, speaking: true } }
|
|
||||||
*/
|
|
||||||
this.emit('speaking', packet.d);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.emit('unknown', packet);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = VoiceConnectionWebSocket;
|
|
||||||
145
src/client/voice/VoiceUDPClient.js
Normal file
145
src/client/voice/VoiceUDPClient.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
const udp = require('dgram');
|
||||||
|
const dns = require('dns');
|
||||||
|
const Constants = require('../../util/Constants');
|
||||||
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
|
function parseLocalPacket(message) {
|
||||||
|
try {
|
||||||
|
const packet = new Buffer(message);
|
||||||
|
let address = '';
|
||||||
|
for (let i = 4; i < packet.indexOf(0, i); i++) address += String.fromCharCode(packet[i]);
|
||||||
|
const port = parseInt(packet.readUIntLE(packet.length - 2, 2).toString(10), 10);
|
||||||
|
return { address, port };
|
||||||
|
} catch (error) {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a UDP Client for a Voice Connection
|
||||||
|
* @extends {EventEmitter}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class VoiceConnectionUDPClient extends EventEmitter {
|
||||||
|
constructor(voiceConnection) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The voice connection that this UDP client serves
|
||||||
|
* @type {VoiceConnection}
|
||||||
|
*/
|
||||||
|
this.voiceConnection = voiceConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The UDP socket
|
||||||
|
* @type {?Socket}
|
||||||
|
*/
|
||||||
|
this.socket = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The address of the discord voice server
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
this.discordAddress = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local IP address
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
this.localAddress = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local port
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
this.localPort = null;
|
||||||
|
|
||||||
|
this.voiceConnection.on('closing', this.shutdown.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
if (this.socket) {
|
||||||
|
try {
|
||||||
|
this.socket.close();
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.socket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The port of the discord voice server
|
||||||
|
* @type {number}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get discordPort() {
|
||||||
|
return this.voiceConnection.authentication.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to resolve the voice server endpoint to an address
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
findEndpointAddress() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
dns.lookup(this.voiceConnection.authentication.endpoint, (error, address) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.discordAddress = address;
|
||||||
|
resolve(address);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a packet to the UDP client
|
||||||
|
* @param {Object} packet the packet to send
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
send(packet) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.socket) throw new Error('Tried to send a UDP packet, but there is no socket available.');
|
||||||
|
if (!this.discordAddress || !this.discordPort) throw new Error('Malformed UDP address or port.');
|
||||||
|
this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => {
|
||||||
|
if (error) reject(error); else resolve(packet);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createUDPSocket(address) {
|
||||||
|
this.discordAddress = address;
|
||||||
|
const socket = this.socket = udp.createSocket('udp4');
|
||||||
|
|
||||||
|
socket.once('message', message => {
|
||||||
|
const packet = parseLocalPacket(message);
|
||||||
|
if (packet.error) {
|
||||||
|
this.emit('error', packet.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.localAddress = packet.address;
|
||||||
|
this.localPort = packet.port;
|
||||||
|
|
||||||
|
this.voiceConnection.sockets.ws.sendPacket({
|
||||||
|
op: Constants.VoiceOPCodes.SELECT_PROTOCOL,
|
||||||
|
d: {
|
||||||
|
protocol: 'udp',
|
||||||
|
data: {
|
||||||
|
address: packet.address,
|
||||||
|
port: packet.port,
|
||||||
|
mode: 'xsalsa20_poly1305',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const blankMessage = new Buffer(70);
|
||||||
|
blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4);
|
||||||
|
this.send(blankMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = VoiceConnectionUDPClient;
|
||||||
244
src/client/voice/VoiceWebSocket.js
Normal file
244
src/client/voice/VoiceWebSocket.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
const Constants = require('../../util/Constants');
|
||||||
|
const SecretKey = require('./util/SecretKey');
|
||||||
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Voice Connection's WebSocket
|
||||||
|
* @extends {EventEmitter}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class VoiceWebSocket extends EventEmitter {
|
||||||
|
constructor(voiceConnection) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Voice Connection that this WebSocket serves
|
||||||
|
* @type {VoiceConnection}
|
||||||
|
*/
|
||||||
|
this.voiceConnection = voiceConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many connection attempts have been made
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.attempts = 0;
|
||||||
|
|
||||||
|
this.connect();
|
||||||
|
this.dead = false;
|
||||||
|
this.voiceConnection.on('closing', this.shutdown.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
this.dead = true;
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client of this voice websocket
|
||||||
|
* @type {Client}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get client() {
|
||||||
|
return this.voiceConnection.voiceManager.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the current WebSocket
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
if (this.ws) {
|
||||||
|
if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
this.clearHeartbeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts connecting to the Voice WebSocket Server.
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
if (this.dead) return;
|
||||||
|
if (this.ws) this.reset();
|
||||||
|
if (this.attempts > 5) {
|
||||||
|
this.emit('error', new Error(`Too many connection attempts (${this.attempts}).`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attempts++;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual WebSocket used to connect to the Voice WebSocket Server.
|
||||||
|
* @type {WebSocket}
|
||||||
|
*/
|
||||||
|
this.ws = new WebSocket(`wss://${this.voiceConnection.authentication.endpoint}`);
|
||||||
|
this.ws.onopen = this.onOpen.bind(this);
|
||||||
|
this.ws.onmessage = this.onMessage.bind(this);
|
||||||
|
this.ws.onclose = this.onClose.bind(this);
|
||||||
|
this.ws.onerror = this.onError.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends data to the WebSocket if it is open.
|
||||||
|
* @param {string} data the data to send to the WebSocket
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
send(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error(`Voice websocket not open to send ${data}.`);
|
||||||
|
}
|
||||||
|
this.ws.send(data, null, error => {
|
||||||
|
if (error) reject(error); else resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON.stringify's a packet and then sends it to the WebSocket Server.
|
||||||
|
* @param {Object} packet the packet to send
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
sendPacket(packet) {
|
||||||
|
try {
|
||||||
|
packet = JSON.stringify(packet);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
return this.send(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the WebSocket opens
|
||||||
|
*/
|
||||||
|
onOpen() {
|
||||||
|
this.sendPacket({
|
||||||
|
op: Constants.OPCodes.DISPATCH,
|
||||||
|
d: {
|
||||||
|
server_id: this.voiceConnection.channel.guild.id,
|
||||||
|
user_id: this.client.user.id,
|
||||||
|
token: this.voiceConnection.authentication.token,
|
||||||
|
session_id: this.voiceConnection.authentication.session_id,
|
||||||
|
},
|
||||||
|
}).catch(() => {
|
||||||
|
this.emit('error', new Error('Tried to send join packet, but the WebSocket is not open.'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever a message is received from the WebSocket
|
||||||
|
* @param {MessageEvent} event the message event that was received
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
onMessage(event) {
|
||||||
|
try {
|
||||||
|
return this.onPacket(JSON.parse(event.data));
|
||||||
|
} catch (error) {
|
||||||
|
return this.onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the connection to the WebSocket Server is lost
|
||||||
|
*/
|
||||||
|
onClose() {
|
||||||
|
// TODO see if the connection is open before reconnecting
|
||||||
|
if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever an error occurs with the WebSocket.
|
||||||
|
* @param {Error} error the error that occurred
|
||||||
|
*/
|
||||||
|
onError(error) {
|
||||||
|
this.emit('error', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever a valid packet is received from the WebSocket
|
||||||
|
* @param {Object} packet the received packet
|
||||||
|
*/
|
||||||
|
onPacket(packet) {
|
||||||
|
switch (packet.op) {
|
||||||
|
case Constants.VoiceOPCodes.READY:
|
||||||
|
this.setHeartbeat(packet.d.heartbeat_interval);
|
||||||
|
/**
|
||||||
|
* Emitted once the voice websocket receives the ready packet
|
||||||
|
* @param {Object} packet the received packet
|
||||||
|
* @event VoiceWebSocket#ready
|
||||||
|
*/
|
||||||
|
this.emit('ready', packet.d);
|
||||||
|
break;
|
||||||
|
case Constants.VoiceOPCodes.SESSION_DESCRIPTION:
|
||||||
|
/**
|
||||||
|
* Emitted once the Voice Websocket receives a description of this voice session
|
||||||
|
* @param {string} encryptionMode the type of encryption being used
|
||||||
|
* @param {SecretKey} secretKey the secret key used for encryption
|
||||||
|
* @event VoiceWebSocket#sessionDescription
|
||||||
|
*/
|
||||||
|
this.emit('sessionDescription', packet.d.mode, new SecretKey(packet.d.secret_key));
|
||||||
|
break;
|
||||||
|
case Constants.VoiceOPCodes.SPEAKING:
|
||||||
|
/**
|
||||||
|
* Emitted whenever a speaking packet is received
|
||||||
|
* @param {Object} data
|
||||||
|
* @event VoiceWebSocket#speaking
|
||||||
|
*/
|
||||||
|
this.emit('speaking', packet.d);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
/**
|
||||||
|
* Emitted when an unhandled packet is received
|
||||||
|
* @param {Object} packet
|
||||||
|
* @event VoiceWebSocket#unknownPacket
|
||||||
|
*/
|
||||||
|
this.emit('unknownPacket', packet);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an interval at which to send a heartbeat packet to the WebSocket
|
||||||
|
* @param {number} interval the interval at which to send a heartbeat packet
|
||||||
|
*/
|
||||||
|
setHeartbeat(interval) {
|
||||||
|
if (!interval || isNaN(interval)) {
|
||||||
|
this.onError(new Error('Tried to set voice heartbeat but no valid interval was specified.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
/**
|
||||||
|
* Emitted whenver the voice websocket encounters a non-fatal error
|
||||||
|
* @param {string} warn the warning
|
||||||
|
* @event VoiceWebSocket#warn
|
||||||
|
*/
|
||||||
|
this.emit('warn', 'A voice heartbeat interval is being overwritten');
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
}
|
||||||
|
this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears a heartbeat interval, if one exists
|
||||||
|
*/
|
||||||
|
clearHeartbeat() {
|
||||||
|
if (!this.heartbeatInterval) {
|
||||||
|
this.emit('warn', 'Tried to clear a heartbeat interval that does not exist');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a heartbeat packet
|
||||||
|
*/
|
||||||
|
sendHeartbeat() {
|
||||||
|
this.sendPacket({ op: Constants.VoiceOPCodes.HEARTBEAT, d: null }).catch(() => {
|
||||||
|
this.emit('warn', 'Tried to send heartbeat, but connection is not open');
|
||||||
|
this.clearHeartbeat();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = VoiceWebSocket;
|
||||||
@@ -32,34 +32,25 @@ class StreamDispatcher extends EventEmitter {
|
|||||||
this._startStreaming();
|
this._startStreaming();
|
||||||
this._triggered = false;
|
this._triggered = false;
|
||||||
this._volume = streamOptions.volume;
|
this._volume = streamOptions.volume;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many passes the dispatcher should take when sending packets to reduce packet loss. Values over 5
|
* How many passes the dispatcher should take when sending packets to reduce packet loss. Values over 5
|
||||||
* aren't recommended, as it means you are using 5x more bandwidth. You _can_ edit this at runtime.
|
* aren't recommended, as it means you are using 5x more bandwidth. You _can_ edit this at runtime.
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.passes = streamOptions.passes || 1;
|
this.passes = streamOptions.passes || 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether playing is paused
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.paused = false;
|
||||||
|
|
||||||
|
this.setVolume(streamOptions.volume || 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the dispatcher starts/stops speaking
|
* How long the stream dispatcher has been "speaking" for
|
||||||
* @event StreamDispatcher#speaking
|
|
||||||
* @param {boolean} value Whether or not the dispatcher is speaking
|
|
||||||
*/
|
|
||||||
_setSpeaking(value) {
|
|
||||||
this.speaking = value;
|
|
||||||
this.emit('speaking', value);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendBuffer(buffer, sequence, timestamp) {
|
|
||||||
let repeats = this.passes;
|
|
||||||
const packet = this._createPacket(sequence, timestamp, this.player.opusEncoder.encode(buffer));
|
|
||||||
while (repeats--) {
|
|
||||||
this.player.connection.udp.send(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* how long the stream dispatcher has been "speaking" for
|
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @readonly
|
* @readonly
|
||||||
*/
|
*/
|
||||||
@@ -68,7 +59,7 @@ class StreamDispatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The total time, taking into account pauses and skips, that the dispatcher has been streaming for.
|
* The total time, taking into account pauses and skips, that the dispatcher has been streaming for
|
||||||
* @type {number}
|
* @type {number}
|
||||||
* @readonly
|
* @readonly
|
||||||
*/
|
*/
|
||||||
@@ -76,176 +67,6 @@ class StreamDispatcher extends EventEmitter {
|
|||||||
return this.time + this.streamingData.pausedTime;
|
return this.time + this.streamingData.pausedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createPacket(sequence, timestamp, buffer) {
|
|
||||||
const packetBuffer = new Buffer(buffer.length + 28);
|
|
||||||
packetBuffer.fill(0);
|
|
||||||
packetBuffer[0] = 0x80;
|
|
||||||
packetBuffer[1] = 0x78;
|
|
||||||
|
|
||||||
packetBuffer.writeUIntBE(sequence, 2, 2);
|
|
||||||
packetBuffer.writeUIntBE(timestamp, 4, 4);
|
|
||||||
packetBuffer.writeUIntBE(this.player.connection.data.ssrc, 8, 4);
|
|
||||||
|
|
||||||
packetBuffer.copy(nonce, 0, 0, 12);
|
|
||||||
buffer = NaCl.secretbox(buffer, nonce, this.player.connection.data.secret);
|
|
||||||
|
|
||||||
for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
|
|
||||||
|
|
||||||
return packetBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
_applyVolume(buffer) {
|
|
||||||
if (this._volume === 1) return buffer;
|
|
||||||
|
|
||||||
const out = new Buffer(buffer.length);
|
|
||||||
for (let i = 0; i < buffer.length; i += 2) {
|
|
||||||
if (i >= buffer.length - 1) break;
|
|
||||||
const uint = Math.min(32767, Math.max(-32767, Math.floor(this._volume * buffer.readInt16LE(i))));
|
|
||||||
out.writeInt16LE(uint, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
_send() {
|
|
||||||
try {
|
|
||||||
if (this._triggered) {
|
|
||||||
this._setSpeaking(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = this.streamingData;
|
|
||||||
|
|
||||||
if (data.missed >= 5) {
|
|
||||||
this._triggerTerminalState('end', 'Stream is not generating quickly enough.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.paused) {
|
|
||||||
// data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
|
|
||||||
data.pausedTime += data.length * 10;
|
|
||||||
this.player.connection.manager.client.setTimeout(() => this._send(), data.length * 10);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._setSpeaking(true);
|
|
||||||
|
|
||||||
if (!data.startTime) {
|
|
||||||
/**
|
|
||||||
* Emitted once the dispatcher starts streaming
|
|
||||||
* @event StreamDispatcher#start
|
|
||||||
*/
|
|
||||||
this.emit('start');
|
|
||||||
data.startTime = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bufferLength = 1920 * data.channels;
|
|
||||||
let buffer = this.stream.read(bufferLength);
|
|
||||||
if (!buffer) {
|
|
||||||
data.missed++;
|
|
||||||
data.pausedTime += data.length * 10;
|
|
||||||
this.player.connection.manager.client.setTimeout(() => this._send(), data.length * 10);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.missed = 0;
|
|
||||||
|
|
||||||
if (buffer.length !== bufferLength) {
|
|
||||||
const newBuffer = new Buffer(bufferLength).fill(0);
|
|
||||||
buffer.copy(newBuffer);
|
|
||||||
buffer = newBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer = this._applyVolume(buffer);
|
|
||||||
|
|
||||||
data.count++;
|
|
||||||
data.sequence = (data.sequence + 1) < (65536) ? data.sequence + 1 : 0;
|
|
||||||
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
|
|
||||||
|
|
||||||
this._sendBuffer(buffer, data.sequence, data.timestamp);
|
|
||||||
|
|
||||||
const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now());
|
|
||||||
this.player.connection.manager.client.setTimeout(() => this._send(), nextTime);
|
|
||||||
} catch (e) {
|
|
||||||
this._triggerTerminalState('error', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted once the stream has ended. Attach a `once` listener to this.
|
|
||||||
* @event StreamDispatcher#end
|
|
||||||
*/
|
|
||||||
_triggerEnd() {
|
|
||||||
this.emit('end');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted once the stream has encountered an error. Attach a `once` listener to this. Also emits `end`.
|
|
||||||
* @event StreamDispatcher#error
|
|
||||||
* @param {Error} err The encountered error
|
|
||||||
*/
|
|
||||||
_triggerError(err) {
|
|
||||||
this.emit('end');
|
|
||||||
this.emit('error', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
_triggerTerminalState(state, err) {
|
|
||||||
if (this._triggered) return;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emitted when the stream wants to give debug information.
|
|
||||||
* @event StreamDispatcher#debug
|
|
||||||
* @param {string} information The debug information
|
|
||||||
*/
|
|
||||||
this.emit('debug', `Triggered terminal state ${state} - stream is now dead`);
|
|
||||||
this._triggered = true;
|
|
||||||
this._setSpeaking(false);
|
|
||||||
switch (state) {
|
|
||||||
case 'end':
|
|
||||||
this._triggerEnd(err);
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
this._triggerError(err);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.emit('error', 'Unknown trigger state');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_startStreaming() {
|
|
||||||
if (!this.stream) {
|
|
||||||
this.emit('error', 'No stream');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.stream.on('end', err => this._triggerTerminalState('end', err));
|
|
||||||
this.stream.on('error', err => this._triggerTerminalState('error', err));
|
|
||||||
|
|
||||||
const data = this.streamingData;
|
|
||||||
data.length = 20;
|
|
||||||
data.missed = 0;
|
|
||||||
|
|
||||||
this.stream.once('readable', () => this._send());
|
|
||||||
}
|
|
||||||
|
|
||||||
_setPaused(paused) {
|
|
||||||
if (paused) {
|
|
||||||
this.paused = true;
|
|
||||||
this._setSpeaking(false);
|
|
||||||
} else {
|
|
||||||
this.paused = false;
|
|
||||||
this._setSpeaking(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the current stream permanently and emits an `end` event.
|
|
||||||
*/
|
|
||||||
end() {
|
|
||||||
this._triggerTerminalState('end', 'user requested');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The volume of the stream, relative to the stream's input volume
|
* The volume of the stream, relative to the stream's input volume
|
||||||
* @type {number}
|
* @type {number}
|
||||||
@@ -292,6 +113,194 @@ class StreamDispatcher extends EventEmitter {
|
|||||||
resume() {
|
resume() {
|
||||||
this._setPaused(false);
|
this._setPaused(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the current stream permanently and emits an `end` event.
|
||||||
|
*/
|
||||||
|
end() {
|
||||||
|
this._triggerTerminalState('end', 'user requested');
|
||||||
|
}
|
||||||
|
|
||||||
|
_setSpeaking(value) {
|
||||||
|
this.speaking = value;
|
||||||
|
/**
|
||||||
|
* Emitted when the dispatcher starts/stops speaking
|
||||||
|
* @event StreamDispatcher#speaking
|
||||||
|
* @param {boolean} value Whether or not the dispatcher is speaking
|
||||||
|
*/
|
||||||
|
this.emit('speaking', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendBuffer(buffer, sequence, timestamp) {
|
||||||
|
let repeats = this.passes;
|
||||||
|
const packet = this._createPacket(sequence, timestamp, this.player.opusEncoder.encode(buffer));
|
||||||
|
while (repeats--) {
|
||||||
|
this.player.voiceConnection.sockets.udp.send(packet)
|
||||||
|
.catch(e => this.emit('debug', `failed to send a packet ${e}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createPacket(sequence, timestamp, buffer) {
|
||||||
|
const packetBuffer = new Buffer(buffer.length + 28);
|
||||||
|
packetBuffer.fill(0);
|
||||||
|
packetBuffer[0] = 0x80;
|
||||||
|
packetBuffer[1] = 0x78;
|
||||||
|
|
||||||
|
packetBuffer.writeUIntBE(sequence, 2, 2);
|
||||||
|
packetBuffer.writeUIntBE(timestamp, 4, 4);
|
||||||
|
packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4);
|
||||||
|
|
||||||
|
packetBuffer.copy(nonce, 0, 0, 12);
|
||||||
|
buffer = NaCl.secretbox(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key);
|
||||||
|
|
||||||
|
for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
|
||||||
|
|
||||||
|
return packetBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyVolume(buffer) {
|
||||||
|
if (this._volume === 1) return buffer;
|
||||||
|
|
||||||
|
const out = new Buffer(buffer.length);
|
||||||
|
for (let i = 0; i < buffer.length; i += 2) {
|
||||||
|
if (i >= buffer.length - 1) break;
|
||||||
|
const uint = Math.min(32767, Math.max(-32767, Math.floor(this._volume * buffer.readInt16LE(i))));
|
||||||
|
out.writeInt16LE(uint, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
_send() {
|
||||||
|
try {
|
||||||
|
if (this._triggered) {
|
||||||
|
this._setSpeaking(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.streamingData;
|
||||||
|
|
||||||
|
if (data.missed >= 5) {
|
||||||
|
this._triggerTerminalState('end', 'Stream is not generating quickly enough.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.paused) {
|
||||||
|
// data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
|
||||||
|
data.pausedTime += data.length * 10;
|
||||||
|
this.player.voiceConnection.voiceManager.client.setTimeout(() => this._send(), data.length * 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setSpeaking(true);
|
||||||
|
|
||||||
|
if (!data.startTime) {
|
||||||
|
/**
|
||||||
|
* Emitted once the dispatcher starts streaming
|
||||||
|
* @event StreamDispatcher#start
|
||||||
|
*/
|
||||||
|
this.emit('start');
|
||||||
|
data.startTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufferLength = 1920 * data.channels;
|
||||||
|
let buffer = this.stream.read(bufferLength);
|
||||||
|
if (!buffer) {
|
||||||
|
data.missed++;
|
||||||
|
data.pausedTime += data.length * 10;
|
||||||
|
this.player.voiceConnection.voiceManager.client.setTimeout(() => this._send(), data.length * 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.missed = 0;
|
||||||
|
|
||||||
|
if (buffer.length !== bufferLength) {
|
||||||
|
const newBuffer = new Buffer(bufferLength).fill(0);
|
||||||
|
buffer.copy(newBuffer);
|
||||||
|
buffer = newBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = this._applyVolume(buffer);
|
||||||
|
|
||||||
|
data.count++;
|
||||||
|
data.sequence = (data.sequence + 1) < (65536) ? data.sequence + 1 : 0;
|
||||||
|
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
|
||||||
|
|
||||||
|
this._sendBuffer(buffer, data.sequence, data.timestamp);
|
||||||
|
|
||||||
|
const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now());
|
||||||
|
this.player.voiceConnection.voiceManager.client.setTimeout(() => this._send(), nextTime);
|
||||||
|
} catch (e) {
|
||||||
|
this._triggerTerminalState('error', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerEnd() {
|
||||||
|
/**
|
||||||
|
* Emitted once the stream has ended. Attach a `once` listener to this.
|
||||||
|
* @event StreamDispatcher#end
|
||||||
|
*/
|
||||||
|
this.emit('end');
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerError(err) {
|
||||||
|
this.emit('end');
|
||||||
|
/**
|
||||||
|
* Emitted once the stream has encountered an error. Attach a `once` listener to this. Also emits `end`.
|
||||||
|
* @event StreamDispatcher#error
|
||||||
|
* @param {Error} err The encountered error
|
||||||
|
*/
|
||||||
|
this.emit('error', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerTerminalState(state, err) {
|
||||||
|
if (this._triggered) return;
|
||||||
|
/**
|
||||||
|
* Emitted when the stream wants to give debug information.
|
||||||
|
* @event StreamDispatcher#debug
|
||||||
|
* @param {string} information The debug information
|
||||||
|
*/
|
||||||
|
this.emit('debug', `Triggered terminal state ${state} - stream is now dead`);
|
||||||
|
this._triggered = true;
|
||||||
|
this._setSpeaking(false);
|
||||||
|
switch (state) {
|
||||||
|
case 'end':
|
||||||
|
this._triggerEnd(err);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
this._triggerError(err);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.emit('error', 'Unknown trigger state');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_startStreaming() {
|
||||||
|
if (!this.stream) {
|
||||||
|
this.emit('error', 'No stream');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stream.on('end', err => this._triggerTerminalState('end', err));
|
||||||
|
this.stream.on('error', err => this._triggerTerminalState('error', err));
|
||||||
|
|
||||||
|
const data = this.streamingData;
|
||||||
|
data.length = 20;
|
||||||
|
data.missed = 0;
|
||||||
|
|
||||||
|
this.stream.once('readable', () => this._send());
|
||||||
|
}
|
||||||
|
|
||||||
|
_setPaused(paused) {
|
||||||
|
if (paused) {
|
||||||
|
this.paused = true;
|
||||||
|
this._setSpeaking(false);
|
||||||
|
} else {
|
||||||
|
this.paused = false;
|
||||||
|
this._setSpeaking(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = StreamDispatcher;
|
module.exports = StreamDispatcher;
|
||||||
|
|||||||
@@ -1,5 +1,43 @@
|
|||||||
const ConverterEngine = require('./ConverterEngine');
|
const ConverterEngine = require('./ConverterEngine');
|
||||||
const ChildProcess = require('child_process');
|
const ChildProcess = require('child_process');
|
||||||
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
|
||||||
|
class PCMConversionProcess extends EventEmitter {
|
||||||
|
constructor(process) {
|
||||||
|
super();
|
||||||
|
this.process = process;
|
||||||
|
this.input = null;
|
||||||
|
this.process.on('error', e => this.emit('error', e));
|
||||||
|
}
|
||||||
|
|
||||||
|
setInput(stream) {
|
||||||
|
this.input = stream;
|
||||||
|
stream.pipe(this.process.stdin, { end: false });
|
||||||
|
this.input.on('error', e => this.emit('error', e));
|
||||||
|
this.process.stdin.on('error', e => this.emit('error', e));
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.emit('debug', 'destroying a ffmpeg process:');
|
||||||
|
if (this.input && this.input.unpipe && this.process.stdin) {
|
||||||
|
this.input.unpipe(this.process.stdin);
|
||||||
|
this.emit('unpiped the user input stream from the process input stream');
|
||||||
|
}
|
||||||
|
if (this.process.stdin) {
|
||||||
|
this.process.stdin.end();
|
||||||
|
this.emit('ended the process stdin');
|
||||||
|
}
|
||||||
|
if (this.process.stdin.destroy) {
|
||||||
|
this.process.stdin.destroy();
|
||||||
|
this.emit('destroyed the process stdin');
|
||||||
|
}
|
||||||
|
if (this.process.kill) {
|
||||||
|
this.process.kill();
|
||||||
|
this.emit('killed the process');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
class FfmpegConverterEngine extends ConverterEngine {
|
class FfmpegConverterEngine extends ConverterEngine {
|
||||||
constructor(player) {
|
constructor(player) {
|
||||||
@@ -24,10 +62,7 @@ class FfmpegConverterEngine extends ConverterEngine {
|
|||||||
'-ss', String(seek),
|
'-ss', String(seek),
|
||||||
'pipe:1',
|
'pipe:1',
|
||||||
], { stdio: ['pipe', 'pipe', 'ignore'] });
|
], { stdio: ['pipe', 'pipe', 'ignore'] });
|
||||||
encoder.on('error', e => this.handleError(encoder, e));
|
return new PCMConversionProcess(encoder);
|
||||||
encoder.stdin.on('error', e => this.handleError(encoder, e));
|
|
||||||
encoder.stdout.on('error', e => this.handleError(encoder, e));
|
|
||||||
return encoder;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
80
src/client/voice/player/AudioPlayer.js
Normal file
80
src/client/voice/player/AudioPlayer.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
const PCMConverters = require('../pcm/ConverterEngineList');
|
||||||
|
const OpusEncoders = require('../opus/OpusEngineList');
|
||||||
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
const StreamDispatcher = require('../dispatcher/StreamDispatcher');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the Audio Player of a Voice Connection
|
||||||
|
* @extends {EventEmitter}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class AudioPlayer extends EventEmitter {
|
||||||
|
constructor(voiceConnection) {
|
||||||
|
super();
|
||||||
|
/**
|
||||||
|
* The voice connection the player belongs to
|
||||||
|
* @type {VoiceConnection}
|
||||||
|
*/
|
||||||
|
this.voiceConnection = voiceConnection;
|
||||||
|
this.audioToPCM = new (PCMConverters.fetch())();
|
||||||
|
this.opusEncoder = OpusEncoders.fetch();
|
||||||
|
this.currentConverter = null;
|
||||||
|
/**
|
||||||
|
* The current stream dispatcher, if a stream is being played
|
||||||
|
* @type {StreamDispatcher}
|
||||||
|
*/
|
||||||
|
this.dispatcher = null;
|
||||||
|
this.audioToPCM.on('error', e => this.emit('error', e));
|
||||||
|
this.streamingData = {
|
||||||
|
channels: 2,
|
||||||
|
count: 0,
|
||||||
|
sequence: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
pausedTime: 0,
|
||||||
|
};
|
||||||
|
this.voiceConnection.on('closing', () => this.cleanup(null, 'voice connection closing'));
|
||||||
|
}
|
||||||
|
|
||||||
|
playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||||
|
const options = { seek, volume, passes };
|
||||||
|
stream.on('end', () => {
|
||||||
|
this.emit('debug', 'Input stream to converter has ended');
|
||||||
|
});
|
||||||
|
stream.on('error', e => this.emit('error', e));
|
||||||
|
const conversionProcess = this.audioToPCM.createConvertStream(options.seek);
|
||||||
|
conversionProcess.on('error', e => this.emit('error', e));
|
||||||
|
conversionProcess.setInput(stream);
|
||||||
|
return this.playPCMStream(conversionProcess.process.stdout, conversionProcess, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup(checkStream, reason) {
|
||||||
|
// cleanup is a lot less aggressive than v9 because it doesn't try to kill every single stream it is aware of
|
||||||
|
this.emit('debug', `Clean up triggered due to ${reason}`);
|
||||||
|
const filter = checkStream && this.dispatcher && this.dispatcher.stream === checkStream;
|
||||||
|
if (this.currentConverter && (checkStream ? filter : true)) {
|
||||||
|
this.currentConverter.destroy();
|
||||||
|
this.currentConverter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playPCMStream(stream, converter, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||||
|
const options = { seek, volume, passes };
|
||||||
|
stream.on('end', () => this.emit('debug', 'PCM input stream ended'));
|
||||||
|
this.cleanup(null, 'outstanding play stream');
|
||||||
|
this.currentConverter = converter;
|
||||||
|
if (this.dispatcher) {
|
||||||
|
this.streamingData = this.dispatcher.streamingData;
|
||||||
|
}
|
||||||
|
stream.on('error', e => this.emit('error', e));
|
||||||
|
const dispatcher = new StreamDispatcher(this, stream, this.streamingData, options);
|
||||||
|
dispatcher.on('error', e => this.emit('error', e));
|
||||||
|
dispatcher.on('end', () => this.cleanup(dispatcher.stream, 'dispatcher ended'));
|
||||||
|
dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value));
|
||||||
|
this.dispatcher = dispatcher;
|
||||||
|
dispatcher.on('debug', m => this.emit('debug', `Stream dispatch - ${m}`));
|
||||||
|
return dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AudioPlayer;
|
||||||
@@ -101,6 +101,8 @@ class VoiceConnectionPlayer extends EventEmitter {
|
|||||||
speaking: true,
|
speaking: true,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
},
|
},
|
||||||
|
}).catch(e => {
|
||||||
|
this.emit('debug', e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,19 +25,22 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
this.queues = new Map();
|
this.queues = new Map();
|
||||||
this.pcmStreams = new Map();
|
this.pcmStreams = new Map();
|
||||||
this.opusStreams = new Map();
|
this.opusStreams = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not this receiver has been destroyed.
|
* Whether or not this receiver has been destroyed.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.destroyed = false;
|
this.destroyed = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The VoiceConnection that instantiated this
|
* The VoiceConnection that instantiated this
|
||||||
* @type {VoiceConnection}
|
* @type {VoiceConnection}
|
||||||
*/
|
*/
|
||||||
this.connection = connection;
|
this.voiceConnection = connection;
|
||||||
this._listener = (msg => {
|
|
||||||
|
this._listener = msg => {
|
||||||
const ssrc = +msg.readUInt32BE(8).toString(10);
|
const ssrc = +msg.readUInt32BE(8).toString(10);
|
||||||
const user = this.connection.ssrcMap.get(ssrc);
|
const user = this.voiceConnection.ssrcMap.get(ssrc);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (!this.queues.has(ssrc)) this.queues.set(ssrc, []);
|
if (!this.queues.has(ssrc)) this.queues.set(ssrc, []);
|
||||||
this.queues.get(ssrc).push(msg);
|
this.queues.get(ssrc).push(msg);
|
||||||
@@ -50,8 +53,8 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.handlePacket(msg, user);
|
this.handlePacket(msg, user);
|
||||||
}
|
}
|
||||||
}).bind(this);
|
};
|
||||||
this.connection.udp.udpSocket.on('message', this._listener);
|
this.voiceConnection.sockets.udp.socket.on('message', this._listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,7 +64,7 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
recreate() {
|
recreate() {
|
||||||
if (!this.destroyed) return;
|
if (!this.destroyed) return;
|
||||||
this.connection.udp.udpSocket.on('message', this._listener);
|
this.voiceConnection.sockets.udp.socket.on('message', this._listener);
|
||||||
this.destroyed = false;
|
this.destroyed = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,7 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
* Destroy this VoiceReceiver, also ending any streams that it may be controlling.
|
* Destroy this VoiceReceiver, also ending any streams that it may be controlling.
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
this.connection.udp.udpSocket.removeListener('message', this._listener);
|
this.voiceConnection.sockets.udp.socket.removeListener('message', this._listener);
|
||||||
for (const stream of this.pcmStreams) {
|
for (const stream of this.pcmStreams) {
|
||||||
stream[1]._push(null);
|
stream[1]._push(null);
|
||||||
this.pcmStreams.delete(stream[0]);
|
this.pcmStreams.delete(stream[0]);
|
||||||
@@ -89,7 +92,7 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
* @returns {ReadableStream}
|
* @returns {ReadableStream}
|
||||||
*/
|
*/
|
||||||
createOpusStream(user) {
|
createOpusStream(user) {
|
||||||
user = this.connection.manager.client.resolver.resolveUser(user);
|
user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user);
|
||||||
if (!user) throw new Error('Couldn\'t resolve the user to create Opus stream.');
|
if (!user) throw new Error('Couldn\'t resolve the user to create Opus stream.');
|
||||||
if (this.opusStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
|
if (this.opusStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
@@ -104,7 +107,7 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
* @returns {ReadableStream}
|
* @returns {ReadableStream}
|
||||||
*/
|
*/
|
||||||
createPCMStream(user) {
|
createPCMStream(user) {
|
||||||
user = this.connection.manager.client.resolver.resolveUser(user);
|
user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user);
|
||||||
if (!user) throw new Error('Couldn\'t resolve the user to create PCM stream.');
|
if (!user) throw new Error('Couldn\'t resolve the user to create PCM stream.');
|
||||||
if (this.pcmStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
|
if (this.pcmStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
@@ -114,7 +117,7 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
|
|
||||||
handlePacket(msg, user) {
|
handlePacket(msg, user) {
|
||||||
msg.copy(nonce, 0, 0, 12);
|
msg.copy(nonce, 0, 0, 12);
|
||||||
let data = NaCl.secretbox.open(msg.slice(12), nonce, this.connection.data.secret);
|
let data = NaCl.secretbox.open(msg.slice(12), nonce, this.voiceConnection.authentication.secretKey.key);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
/**
|
/**
|
||||||
* Emitted whenever a voice packet cannot be decrypted
|
* Emitted whenever a voice packet cannot be decrypted
|
||||||
@@ -141,7 +144,7 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
* @param {User} user The user that is sending the buffer (is speaking)
|
* @param {User} user The user that is sending the buffer (is speaking)
|
||||||
* @param {Buffer} buffer The decoded buffer
|
* @param {Buffer} buffer The decoded buffer
|
||||||
*/
|
*/
|
||||||
const pcm = this.connection.player.opusEncoder.decode(data);
|
const pcm = this.voiceConnection.player.opusEncoder.decode(data);
|
||||||
if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm);
|
if (this.pcmStreams.get(user.id)) this.pcmStreams.get(user.id)._push(pcm);
|
||||||
this.emit('pcm', user, pcm);
|
this.emit('pcm', user, pcm);
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/client/voice/util/SecretKey.js
Normal file
15
src/client/voice/util/SecretKey.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Represents a Secret Key used in encryption over voice
|
||||||
|
*/
|
||||||
|
class SecretKey {
|
||||||
|
constructor(key) {
|
||||||
|
/**
|
||||||
|
* The key used for encryption
|
||||||
|
* @type {Uint8Array}
|
||||||
|
*/
|
||||||
|
this.key = new Uint8Array(new ArrayBuffer(key.length));
|
||||||
|
for (const index in key) this.key[index] = key[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SecretKey;
|
||||||
@@ -58,16 +58,25 @@ class WebSocketManager extends EventEmitter {
|
|||||||
* @type {?WebSocket}
|
* @type {?WebSocket}
|
||||||
*/
|
*/
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object with keys that are websocket event names that should be ignored
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
this.disabledEvents = {};
|
||||||
|
for (const event in client.options.disabledEvents) this.disabledEvents[event] = true;
|
||||||
|
|
||||||
|
this.first = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects the client to a given gateway
|
* Connects the client to a given gateway
|
||||||
* @param {string} gateway The gateway to connect to
|
* @param {string} gateway The gateway to connect to
|
||||||
*/
|
*/
|
||||||
connect(gateway) {
|
_connect(gateway) {
|
||||||
this.client.emit('debug', `Connecting to gateway ${gateway}`);
|
this.client.emit('debug', `Connecting to gateway ${gateway}`);
|
||||||
this.normalReady = false;
|
this.normalReady = false;
|
||||||
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();
|
this.ws.onopen = () => this.eventOpen();
|
||||||
this.ws.onclose = (d) => this.eventClose(d);
|
this.ws.onclose = (d) => this.eventClose(d);
|
||||||
@@ -77,6 +86,15 @@ class WebSocketManager extends EventEmitter {
|
|||||||
this._remaining = 3;
|
this._remaining = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connect(gateway) {
|
||||||
|
if (this.first) {
|
||||||
|
this._connect(gateway);
|
||||||
|
this.first = false;
|
||||||
|
} else {
|
||||||
|
this.client.setTimeout(() => this._connect(gateway), 5500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@@ -99,6 +117,7 @@ class WebSocketManager extends EventEmitter {
|
|||||||
|
|
||||||
_send(data) {
|
_send(data) {
|
||||||
if (this.ws.readyState === WebSocket.OPEN) {
|
if (this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.emit('send', data);
|
||||||
this.ws.send(data);
|
this.ws.send(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +144,7 @@ class WebSocketManager extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
eventOpen() {
|
eventOpen() {
|
||||||
this.client.emit('debug', 'Connection to gateway opened');
|
this.client.emit('debug', 'Connection to gateway opened');
|
||||||
if (this.reconnecting) this._sendResume();
|
if (this.status === Constants.Status.RECONNECTING) this._sendResume();
|
||||||
else this._sendNewIdentify();
|
else this._sendNewIdentify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +152,11 @@ class WebSocketManager extends EventEmitter {
|
|||||||
* Sends a gateway resume packet, in cases of unexpected disconnections.
|
* Sends a gateway resume packet, in cases of unexpected disconnections.
|
||||||
*/
|
*/
|
||||||
_sendResume() {
|
_sendResume() {
|
||||||
|
if (!this.sessionID) {
|
||||||
|
this._sendNewIdentify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.client.emit('debug', 'Identifying as resumed session');
|
||||||
const payload = {
|
const payload = {
|
||||||
token: this.client.token,
|
token: this.client.token,
|
||||||
session_id: this.sessionID,
|
session_id: this.sessionID,
|
||||||
@@ -152,13 +176,15 @@ class WebSocketManager extends EventEmitter {
|
|||||||
this.reconnecting = false;
|
this.reconnecting = false;
|
||||||
const payload = this.client.options.ws;
|
const payload = this.client.options.ws;
|
||||||
payload.token = this.client.token;
|
payload.token = this.client.token;
|
||||||
if (this.client.options.shard_count > 0) {
|
if (this.client.options.shardCount > 0) {
|
||||||
payload.shard = [Number(this.client.options.shard_id), Number(this.client.options.shard_count)];
|
payload.shard = [Number(this.client.options.shardId), Number(this.client.options.shardCount)];
|
||||||
}
|
}
|
||||||
|
this.client.emit('debug', 'Identifying as new session');
|
||||||
this.send({
|
this.send({
|
||||||
op: Constants.OPCodes.IDENTIFY,
|
op: Constants.OPCodes.IDENTIFY,
|
||||||
d: payload,
|
d: payload,
|
||||||
});
|
});
|
||||||
|
this.sequence = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,6 +197,7 @@ class WebSocketManager extends EventEmitter {
|
|||||||
* Emitted whenever the client websocket is disconnected
|
* Emitted whenever the client websocket is disconnected
|
||||||
* @event Client#disconnect
|
* @event Client#disconnect
|
||||||
*/
|
*/
|
||||||
|
clearInterval(this.client.manager.heartbeatInterval);
|
||||||
if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT);
|
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;
|
||||||
@@ -194,7 +221,7 @@ class WebSocketManager extends EventEmitter {
|
|||||||
|
|
||||||
this.client.emit('raw', packet);
|
this.client.emit('raw', packet);
|
||||||
|
|
||||||
if (packet.op === 10) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
|
if (packet.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
|
||||||
return this.packetManager.handle(packet);
|
return this.packetManager.handle(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,10 +262,11 @@ class WebSocketManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
if (unavailableCount === 0) {
|
if (unavailableCount === 0) {
|
||||||
this.status = Constants.Status.NEARLY;
|
this.status = Constants.Status.NEARLY;
|
||||||
if (this.client.options.fetch_all_members) {
|
if (this.client.options.fetchAllMembers) {
|
||||||
const promises = this.client.guilds.array().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()).catch(e => {
|
||||||
this.client.emit(Constants.Event.WARN, `Error on pre-ready guild member fetching - ${e}`);
|
this.client.emit(Constants.Events.WARN, 'Error in pre-ready guild member fetching');
|
||||||
|
this.client.emit(Constants.Events.ERROR, e);
|
||||||
this._emitReady();
|
this._emitReady();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ class WebSocketPacketManager {
|
|||||||
this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk');
|
this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk');
|
||||||
this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate');
|
this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate');
|
||||||
this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync');
|
this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync');
|
||||||
|
this.register(Constants.WSEvents.RELATIONSHIP_ADD, 'RelationshipAdd');
|
||||||
|
this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, 'RelationshipRemove');
|
||||||
}
|
}
|
||||||
|
|
||||||
get client() {
|
get client() {
|
||||||
@@ -72,17 +74,22 @@ class WebSocketPacketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (packet.op === Constants.OPCodes.INVALID_SESSION) {
|
if (packet.op === Constants.OPCodes.INVALID_SESSION) {
|
||||||
|
this.ws.sessionID = null;
|
||||||
this.ws._sendNewIdentify();
|
this.ws._sendNewIdentify();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.ws.reconnecting) {
|
if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) this.ws.client.emit('debug', 'Heartbeat acknowledged');
|
||||||
|
|
||||||
|
if (this.ws.status === Constants.Status.RECONNECTING) {
|
||||||
this.ws.reconnecting = false;
|
this.ws.reconnecting = false;
|
||||||
this.ws.checkIfReady();
|
this.ws.checkIfReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setSequence(packet.s);
|
this.setSequence(packet.s);
|
||||||
|
|
||||||
|
if (this.ws.disabledEvents[packet.t] !== undefined) return false;
|
||||||
|
|
||||||
if (this.ws.status !== Constants.Status.READY) {
|
if (this.ws.status !== Constants.Status.READY) {
|
||||||
if (BeforeReadyWhitelist.indexOf(packet.t) === -1) {
|
if (BeforeReadyWhitelist.indexOf(packet.t) === -1) {
|
||||||
this.queue.push(packet);
|
this.queue.push(packet);
|
||||||
|
|||||||
13
src/client/websocket/packets/handlers/GuildEmojiUpdate.js
Normal file
13
src/client/websocket/packets/handlers/GuildEmojiUpdate.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
@@ -15,14 +15,13 @@ class GuildMembersChunkHandler extends AbstractHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guild._checkChunks();
|
guild._checkChunks();
|
||||||
client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, guild, members);
|
client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever a chunk of Guild members is received
|
* Emitted whenever a chunk of Guild members is received (all members come from the same guild)
|
||||||
* @event Client#guildMembersChunk
|
* @event Client#guildMembersChunk
|
||||||
* @param {Guild} guild The guild that the chunks relate to
|
|
||||||
* @param {GuildMember[]} members The members in the chunk
|
* @param {GuildMember[]} members The members in the chunk
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -18,51 +18,54 @@ class PresenceUpdateHandler extends AbstractHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldUser = cloneObject(user);
|
||||||
|
user.patch(data.user);
|
||||||
|
if (!user.equals(oldUser)) {
|
||||||
|
client.emit(Constants.Events.USER_UPDATE, oldUser, user);
|
||||||
|
}
|
||||||
|
|
||||||
if (guild) {
|
if (guild) {
|
||||||
const memberInGuild = guild.members.get(user.id);
|
let member = guild.members.get(user.id);
|
||||||
if (!memberInGuild && data.status !== 'offline') {
|
if (!member && data.status !== 'offline') {
|
||||||
const member = guild._addMember({
|
member = guild._addMember({
|
||||||
user,
|
user,
|
||||||
roles: data.roles,
|
roles: data.roles,
|
||||||
deaf: false,
|
deaf: false,
|
||||||
mute: false,
|
mute: false,
|
||||||
}, false);
|
}, false);
|
||||||
client.emit(Constants.Events.GUILD_MEMBER_AVAILABLE, guild, member);
|
client.emit(Constants.Events.GUILD_MEMBER_AVAILABLE, member);
|
||||||
|
}
|
||||||
|
if (member) {
|
||||||
|
const oldMember = cloneObject(member);
|
||||||
|
if (member.presence) {
|
||||||
|
oldMember.frozenPresence = cloneObject(member.presence);
|
||||||
|
}
|
||||||
|
guild._setPresence(user.id, data);
|
||||||
|
client.emit(Constants.Events.PRESENCE_UPDATE, oldMember, member);
|
||||||
|
} else {
|
||||||
|
guild._setPresence(user.id, data);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
data.user.username = data.user.username || user.username;
|
|
||||||
data.user.id = data.user.id || user.id;
|
|
||||||
data.user.discriminator = data.user.discriminator || user.discriminator;
|
|
||||||
data.user.status = data.status || user.status;
|
|
||||||
data.user.game = data.game;
|
|
||||||
|
|
||||||
const same = data.user.username === user.username &&
|
|
||||||
data.user.id === user.id &&
|
|
||||||
data.user.discriminator === user.discriminator &&
|
|
||||||
data.user.avatar === user.avatar &&
|
|
||||||
data.user.status === user.status &&
|
|
||||||
JSON.stringify(data.user.game) === JSON.stringify(user.game);
|
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
const oldUser = cloneObject(user);
|
|
||||||
user.patch(data.user);
|
|
||||||
client.emit(Constants.Events.PRESENCE_UPDATE, oldUser, user);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever a user changes one of their details or starts/stop playing a game
|
* Emitted whenever a guild member's presence changes, or they change one of their details.
|
||||||
* @event Client#presenceUpdate
|
* @event Client#presenceUpdate
|
||||||
* @param {User} oldUser The user before the presence update
|
* @param {GuildMember} oldMember The member before the presence update
|
||||||
* @param {User} newUser The user after the presence update
|
* @param {GuildMember} newMember The member after the presence update
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted whenever a user's details (e.g. username) are changed.
|
||||||
|
* @event Client#userUpdate
|
||||||
|
* @param {User} oldUser The user before the update
|
||||||
|
* @param {User} newUser The user after the update
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {Guild} guild The guild that the member became available in
|
|
||||||
* @param {GuildMember} member The member that became available
|
* @param {GuildMember} member The member that became available
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,28 @@ class ReadyHandler extends AbstractHandler {
|
|||||||
|
|
||||||
const clientUser = new ClientUser(client, data.user);
|
const clientUser = new ClientUser(client, data.user);
|
||||||
client.user = clientUser;
|
client.user = clientUser;
|
||||||
client.readyTime = new Date();
|
client.readyAt = new Date();
|
||||||
client.users.set(clientUser.id, clientUser);
|
client.users.set(clientUser.id, clientUser);
|
||||||
|
|
||||||
for (const guild of data.guilds) client.dataManager.newGuild(guild);
|
for (const guild of data.guilds) client.dataManager.newGuild(guild);
|
||||||
for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM);
|
for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM);
|
||||||
|
|
||||||
if (!client.user.bot) client.setInterval(client.syncGuilds.bind(client), 30000);
|
for (const relation of data.relationships) {
|
||||||
|
const user = client.dataManager.newUser(relation.user);
|
||||||
|
if (relation.type === 1) {
|
||||||
|
client.user.friends.set(user.id, user);
|
||||||
|
} else if (relation.type === 2) {
|
||||||
|
client.user.blocked.set(user.id, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.presences = data.presences || [];
|
||||||
|
for (const presence of data.presences) {
|
||||||
|
client.dataManager.newUser(presence.user);
|
||||||
|
client._setPresence(presence.user.id, presence);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
if (!client.users.has('1')) {
|
if (!client.users.has('1')) {
|
||||||
|
|||||||
19
src/client/websocket/packets/handlers/RelationshipAdd.js
Normal file
19
src/client/websocket/packets/handlers/RelationshipAdd.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const AbstractHandler = require('./AbstractHandler');
|
||||||
|
|
||||||
|
class RelationshipAddHandler extends AbstractHandler {
|
||||||
|
handle(packet) {
|
||||||
|
const client = this.packetManager.client;
|
||||||
|
const data = packet.d;
|
||||||
|
if (data.type === 1) {
|
||||||
|
client.fetchUser(data.id).then(user => {
|
||||||
|
client.user.friends.set(user.id, user);
|
||||||
|
});
|
||||||
|
} else if (data.type === 2) {
|
||||||
|
client.fetchUser(data.id).then(user => {
|
||||||
|
client.user.blocked.set(user.id, user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RelationshipAddHandler;
|
||||||
19
src/client/websocket/packets/handlers/RelationshipRemove.js
Normal file
19
src/client/websocket/packets/handlers/RelationshipRemove.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const AbstractHandler = require('./AbstractHandler');
|
||||||
|
|
||||||
|
class RelationshipRemoveHandler extends AbstractHandler {
|
||||||
|
handle(packet) {
|
||||||
|
const client = this.packetManager.client;
|
||||||
|
const data = packet.d;
|
||||||
|
if (data.type === 2) {
|
||||||
|
if (client.user.blocked.has(data.id)) {
|
||||||
|
client.user.blocked.delete(data.id);
|
||||||
|
}
|
||||||
|
} else if (data.type === 1) {
|
||||||
|
if (client.user.friends.has(data.id)) {
|
||||||
|
client.user.friends.delete(data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RelationshipRemoveHandler;
|
||||||
@@ -12,9 +12,7 @@ class VoiceServerUpdate extends AbstractHandler {
|
|||||||
handle(packet) {
|
handle(packet) {
|
||||||
const client = this.packetManager.client;
|
const client = this.packetManager.client;
|
||||||
const data = packet.d;
|
const data = packet.d;
|
||||||
if (client.voice.pending.has(data.guild_id)) {
|
client.emit('self.voiceServer', data);
|
||||||
client.voice._receivedVoiceServer(data.guild_id, data.token, data.endpoint);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ class VoiceStateUpdateHandler extends AbstractHandler {
|
|||||||
// if the member left the voice channel, unset their speaking property
|
// if the member left the voice channel, unset their speaking property
|
||||||
if (!data.channel_id) member.speaking = null;
|
if (!data.channel_id) member.speaking = null;
|
||||||
|
|
||||||
if (client.voice.pending.has(guild.id) && member.user.id === client.user.id && data.channel_id) {
|
if (member.user.id === client.user.id && data.channel_id) {
|
||||||
client.voice._receivedVoiceStateUpdate(data.guild_id, data.session_id);
|
client.emit('self.voiceStateUpdate', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChannel = client.channels.get(data.channel_id);
|
const newChannel = client.channels.get(data.channel_id);
|
||||||
|
|||||||
13
src/index.js
13
src/index.js
@@ -1,16 +1,21 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Client: require('./client/Client'),
|
Client: require('./client/Client'),
|
||||||
|
WebhookClient: require('./client/WebhookClient'),
|
||||||
Shard: require('./sharding/Shard'),
|
Shard: require('./sharding/Shard'),
|
||||||
|
ShardClientUtil: require('./sharding/ShardClientUtil'),
|
||||||
ShardingManager: require('./sharding/ShardingManager'),
|
ShardingManager: require('./sharding/ShardingManager'),
|
||||||
|
|
||||||
Collection: require('./util/Collection'),
|
Collection: require('./util/Collection'),
|
||||||
|
splitMessage: require('./util/SplitMessage'),
|
||||||
|
escapeMarkdown: require('./util/EscapeMarkdown'),
|
||||||
|
fetchRecommendedShards: require('./util/FetchRecommendedShards'),
|
||||||
|
|
||||||
Channel: require('./structures/Channel'),
|
Channel: require('./structures/Channel'),
|
||||||
ClientUser: require('./structures/ClientUser'),
|
ClientUser: require('./structures/ClientUser'),
|
||||||
DMChannel: require('./structures/DMChannel'),
|
DMChannel: require('./structures/DMChannel'),
|
||||||
Emoji: require('./structures/Emoji'),
|
Emoji: require('./structures/Emoji'),
|
||||||
EvaluatedPermissions: require('./structures/EvaluatedPermissions'),
|
EvaluatedPermissions: require('./structures/EvaluatedPermissions'),
|
||||||
|
Game: require('./structures/Presence').Game,
|
||||||
GroupDMChannel: require('./structures/GroupDMChannel'),
|
GroupDMChannel: require('./structures/GroupDMChannel'),
|
||||||
Guild: require('./structures/Guild'),
|
Guild: require('./structures/Guild'),
|
||||||
GuildChannel: require('./structures/GuildChannel'),
|
GuildChannel: require('./structures/GuildChannel'),
|
||||||
@@ -23,10 +28,12 @@ module.exports = {
|
|||||||
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,
|
||||||
Role: require('./structures/Role'),
|
Role: require('./structures/Role'),
|
||||||
TextChannel: require('./structures/TextChannel'),
|
TextChannel: require('./structures/TextChannel'),
|
||||||
User: require('./structures/User'),
|
User: require('./structures/User'),
|
||||||
VoiceChannel: require('./structures/VoiceChannel'),
|
VoiceChannel: require('./structures/VoiceChannel'),
|
||||||
|
Webhook: require('./structures/Webhook'),
|
||||||
|
|
||||||
version: require(path.join(__dirname, '..', 'package')).version,
|
version: require('../package').version,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const childProcess = require('child_process');
|
const childProcess = require('child_process');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const makeError = require('../util/MakeError');
|
||||||
|
const makePlainError = require('../util/MakePlainError');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Shard spawned by the ShardingManager.
|
* Represents a Shard spawned by the ShardingManager.
|
||||||
@@ -8,35 +10,39 @@ 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
|
||||||
*/
|
*/
|
||||||
constructor(manager, id) {
|
constructor(manager, id, args = []) {
|
||||||
/**
|
/**
|
||||||
* The manager of the spawned shard
|
* Manager that created the shard
|
||||||
* @type {ShardingManager}
|
* @type {ShardingManager}
|
||||||
*/
|
*/
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The shard ID
|
* ID of the shard
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The process of the shard
|
* The environment variables for the shard
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
this.env = Object.assign({}, process.env, {
|
||||||
|
SHARD_ID: this.id,
|
||||||
|
SHARD_COUNT: this.manager.totalShards,
|
||||||
|
CLIENT_TOKEN: this.manager.token,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process of the shard
|
||||||
* @type {ChildProcess}
|
* @type {ChildProcess}
|
||||||
*/
|
*/
|
||||||
this.process = childProcess.fork(path.resolve(this.manager.file), [], {
|
this.process = childProcess.fork(path.resolve(this.manager.file), args, {
|
||||||
env: {
|
env: this.env,
|
||||||
SHARD_ID: this.id,
|
|
||||||
SHARD_COUNT: this.manager.totalShards,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
this.process.on('message', this._handleMessage.bind(this));
|
||||||
this.process.on('message', message => {
|
|
||||||
this.manager.emit('message', this, message);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.process.once('exit', () => {
|
this.process.once('exit', () => {
|
||||||
if (this.manager.respawn) this.manager.createShard(this.id);
|
if (this.manager.respawn) this.manager.createShard(this.id);
|
||||||
});
|
});
|
||||||
@@ -59,6 +65,38 @@ class Shard {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a Client property value of the shard.
|
||||||
|
* @param {string} prop Name of the Client property to get, using periods for nesting
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
* @example
|
||||||
|
* shard.fetchClientValue('guilds.size').then(count => {
|
||||||
|
* console.log(`${count} guilds in shard ${shard.id}`);
|
||||||
|
* }).catch(console.error);
|
||||||
|
*/
|
||||||
|
fetchClientValue(prop) {
|
||||||
|
if (this._fetches.has(prop)) return this._fetches.get(prop);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
const listener = message => {
|
||||||
|
if (!message || message._fetchProp !== prop) return;
|
||||||
|
this.process.removeListener('message', listener);
|
||||||
|
this._fetches.delete(prop);
|
||||||
|
resolve(message._result);
|
||||||
|
};
|
||||||
|
this.process.on('message', listener);
|
||||||
|
|
||||||
|
this.send({ _fetchProp: prop }).catch(err => {
|
||||||
|
this.process.removeListener('message', listener);
|
||||||
|
this._fetches.delete(prop);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._fetches.set(prop, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluates a script on the shard, in the context of the Client.
|
* Evaluates a script on the shard, in the context of the Client.
|
||||||
* @param {string} script JavaScript to run on the shard
|
* @param {string} script JavaScript to run on the shard
|
||||||
@@ -69,20 +107,10 @@ class Shard {
|
|||||||
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
const listener = message => {
|
const listener = message => {
|
||||||
if (!message) return;
|
if (!message || message._eval !== script) return;
|
||||||
if (message._evalResult) {
|
this.process.removeListener('message', listener);
|
||||||
this.process.removeListener('message', listener);
|
this._evals.delete(script);
|
||||||
this._evals.delete(script);
|
if (!message._error) resolve(message._result); else reject(makeError(message._error));
|
||||||
resolve(message._evalResult);
|
|
||||||
} else if (message._evalError) {
|
|
||||||
this.process.removeListener('message', listener);
|
|
||||||
const err = new Error(message._evalError.message, message._evalError.fileName, message._evalError.lineNumber);
|
|
||||||
err.name = message._evalError.name;
|
|
||||||
err.columnNumber = message._evalError.columnNumber;
|
|
||||||
err.stack = message._evalError.stack;
|
|
||||||
this._evals.delete(script);
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
this.process.on('message', listener);
|
this.process.on('message', listener);
|
||||||
|
|
||||||
@@ -98,35 +126,30 @@ class Shard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a Client property value of the shard.
|
* Handles an IPC message
|
||||||
* @param {string} prop Name of the Client property to get, using periods for nesting
|
* @param {*} message Message received
|
||||||
* @returns {Promise<*>}
|
* @private
|
||||||
* @example
|
|
||||||
* shard.fetchClientValue('guilds.size').then(count => {
|
|
||||||
* console.log(`${count} guilds in shard ${shard.id}`);
|
|
||||||
* }).catch(console.error);
|
|
||||||
*/
|
*/
|
||||||
fetchClientValue(prop) {
|
_handleMessage(message) {
|
||||||
if (this._fetches.has(prop)) return this._fetches.get(prop);
|
if (message) {
|
||||||
|
// Shard is requesting a property fetch
|
||||||
|
if (message._sFetchProp) {
|
||||||
|
this.manager.fetchClientValues(message._sFetchProp)
|
||||||
|
.then(results => this.send({ _sFetchProp: message._sFetchProp, _result: results }))
|
||||||
|
.catch(err => this.send({ _sFetchProp: message._sFetchProp, _error: makePlainError(err) }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
// Shard is requesting an eval broadcast
|
||||||
const listener = message => {
|
if (message._sEval) {
|
||||||
if (typeof message !== 'object' || message._fetchProp !== prop) return;
|
this.manager.broadcastEval(message._sEval)
|
||||||
this.process.removeListener('message', listener);
|
.then(results => this.send({ _sEval: message._sEval, _result: results }))
|
||||||
this._fetches.delete(prop);
|
.catch(err => this.send({ _sEval: message._sEval, _error: makePlainError(err) }));
|
||||||
resolve(message._fetchPropValue);
|
return;
|
||||||
};
|
}
|
||||||
this.process.on('message', listener);
|
}
|
||||||
|
|
||||||
this.send({ _fetchProp: prop }).catch(err => {
|
this.manager.emit('message', this, message);
|
||||||
this.process.removeListener('message', listener);
|
|
||||||
this._fetches.delete(prop);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this._fetches.set(prop, promise);
|
|
||||||
return promise;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
142
src/sharding/ShardClientUtil.js
Normal file
142
src/sharding/ShardClientUtil.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
const makeError = require('../util/MakeError');
|
||||||
|
const makePlainError = require('../util/MakePlainError');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for sharded clients spawned as a child process, such as from a ShardingManager
|
||||||
|
*/
|
||||||
|
class ShardClientUtil {
|
||||||
|
/**
|
||||||
|
* @param {Client} client Client of the current shard
|
||||||
|
*/
|
||||||
|
constructor(client) {
|
||||||
|
this.client = client;
|
||||||
|
process.on('message', this._handleMessage.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID of this shard
|
||||||
|
* @type {number}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get id() {
|
||||||
|
return this.client.options.shardId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total number of shards
|
||||||
|
* @type {number}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get count() {
|
||||||
|
return this.client.options.shardCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the master process
|
||||||
|
* @param {*} message Message to send
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
send(message) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const sent = process.send(message, err => {
|
||||||
|
if (err) reject(err); else resolve();
|
||||||
|
});
|
||||||
|
if (!sent) throw new Error('Failed to send message to master process.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a Client property value of each shard.
|
||||||
|
* @param {string} prop Name of the Client property to get, using periods for nesting
|
||||||
|
* @returns {Promise<Array>}
|
||||||
|
* @example
|
||||||
|
* client.shard.fetchClientValues('guilds.size').then(results => {
|
||||||
|
* console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
|
||||||
|
* }).catch(console.error);
|
||||||
|
*/
|
||||||
|
fetchClientValues(prop) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const listener = message => {
|
||||||
|
if (!message || message._sFetchProp !== prop) return;
|
||||||
|
process.removeListener('message', listener);
|
||||||
|
if (!message._error) resolve(message._result); else reject(makeError(message._error));
|
||||||
|
};
|
||||||
|
process.on('message', listener);
|
||||||
|
|
||||||
|
this.send({ _sFetchProp: prop }).catch(err => {
|
||||||
|
process.removeListener('message', listener);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates a script on all shards, in the context of the Clients.
|
||||||
|
* @param {string} script JavaScript to run on each shard
|
||||||
|
* @returns {Promise<Array>} Results of the script execution
|
||||||
|
*/
|
||||||
|
broadcastEval(script) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const listener = message => {
|
||||||
|
if (!message || message._sEval !== script) return;
|
||||||
|
process.removeListener('message', listener);
|
||||||
|
if (!message._error) resolve(message._result); else reject(makeError(message._error));
|
||||||
|
};
|
||||||
|
process.on('message', listener);
|
||||||
|
|
||||||
|
this.send({ _sEval: script }).catch(err => {
|
||||||
|
process.removeListener('message', listener);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles an IPC message
|
||||||
|
* @param {*} message Message received
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_handleMessage(message) {
|
||||||
|
if (!message) return;
|
||||||
|
if (message._fetchProp) {
|
||||||
|
const props = message._fetchProp.split('.');
|
||||||
|
let value = this.client;
|
||||||
|
for (const prop of props) value = value[prop];
|
||||||
|
this._respond('fetchProp', { _fetchProp: message._fetchProp, _result: value });
|
||||||
|
} else if (message._eval) {
|
||||||
|
try {
|
||||||
|
this._respond('eval', { _eval: message._eval, _result: this.client._eval(message._eval) });
|
||||||
|
} catch (err) {
|
||||||
|
this._respond('eval', { _eval: message._eval, _error: makePlainError(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to the master process, emitting an error from the client upon failure
|
||||||
|
* @param {string} type Type of response to send
|
||||||
|
* @param {*} message Message to send
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_respond(type, message) {
|
||||||
|
this.send(message).catch(err =>
|
||||||
|
this.client.emit('error', `Error when sending ${type} response to master process: ${err}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates/gets the singleton of this class
|
||||||
|
* @param {Client} client Client to use
|
||||||
|
* @returns {ShardUtil}
|
||||||
|
*/
|
||||||
|
static singleton(client) {
|
||||||
|
if (!this._singleton) {
|
||||||
|
this._singleton = new this(client);
|
||||||
|
} else {
|
||||||
|
client.emit('error', 'Multiple clients created in child process; only the first will handle sharding helpers.');
|
||||||
|
}
|
||||||
|
return this._singleton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ShardClientUtil;
|
||||||
@@ -1,24 +1,35 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const EventEmitter = require('events').EventEmitter;
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
const mergeDefault = require('../util/MergeDefault');
|
||||||
const Shard = require('./Shard');
|
const Shard = require('./Shard');
|
||||||
const Collection = require('../util/Collection');
|
const Collection = require('../util/Collection');
|
||||||
|
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.
|
||||||
* <warn>The Sharding Manager is still experimental</warn>
|
* <warn>The Sharding Manager is still experimental</warn>
|
||||||
* @extends {EventEmitter}
|
* @extends {EventEmitter}
|
||||||
*/
|
*/
|
||||||
class ShardingManager extends EventEmitter {
|
class ShardingManager extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @param {string} file Path to your shard script file
|
* @param {string} file Path to your shard script file
|
||||||
* @param {number} [totalShards=1] Number of shards to default to spawning
|
* @param {Object} [options] Options for the sharding manager
|
||||||
* @param {boolean} [respawn=true] Respawn a shard when it dies
|
* @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto"
|
||||||
|
* @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting
|
||||||
|
* @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning
|
||||||
|
* @param {string} [options.token] Token to use for automatic shard count and passing to shards
|
||||||
*/
|
*/
|
||||||
constructor(file, totalShards = 1, respawn = true) {
|
constructor(file, options = {}) {
|
||||||
super();
|
super();
|
||||||
|
options = mergeDefault({
|
||||||
|
totalShards: 'auto',
|
||||||
|
respawn: true,
|
||||||
|
shardArgs: [],
|
||||||
|
token: null,
|
||||||
|
}, options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the shard script file
|
* Path to the shard script file
|
||||||
@@ -32,16 +43,36 @@ class ShardingManager extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount of shards that this manager is going to spawn
|
* Amount of shards that this manager is going to spawn
|
||||||
* @type {number}
|
* @type {number|string}
|
||||||
*/
|
*/
|
||||||
this.totalShards = totalShards;
|
this.totalShards = options.totalShards;
|
||||||
if (typeof totalShards !== 'number' || isNaN(totalShards)) {
|
if (this.totalShards !== 'auto') {
|
||||||
throw new TypeError('Amount of shards must be a number.');
|
if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
|
||||||
|
throw new TypeError('Amount of shards must be a number.');
|
||||||
|
}
|
||||||
|
if (this.totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||||
|
if (this.totalShards !== Math.floor(this.totalShards)) {
|
||||||
|
throw new RangeError('Amount of shards must be an integer.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
|
|
||||||
if (totalShards !== Math.floor(totalShards)) throw new RangeError('Amount of shards must be an integer.');
|
|
||||||
|
|
||||||
this.respawn = respawn;
|
/**
|
||||||
|
* Whether shards should automatically respawn upon exiting
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.respawn = options.respawn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of arguments to pass to shards.
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
this.shardArgs = options.shardArgs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token to use for obtaining the automatic shard count, and passing to shards
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
this.token = options.token ? options.token.replace(/^Bot\s*/i, '') : null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A collection of shards that this manager has spawned
|
* A collection of shards that this manager has spawned
|
||||||
@@ -52,11 +83,11 @@ class ShardingManager extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Spawns a single shard.
|
* Spawns a single shard.
|
||||||
* @param {number} id The ID of the shard to spawn. THIS IS NOT NEEDED IN ANY NORMAL CASE!
|
* @param {number} id The ID of the shard to spawn. **This is usually not necessary.**
|
||||||
* @returns {Promise<Shard>}
|
* @returns {Promise<Shard>}
|
||||||
*/
|
*/
|
||||||
createShard(id = this.shards.size) {
|
createShard(id = this.shards.size) {
|
||||||
const shard = new Shard(this, id);
|
const shard = new Shard(this, id, this.shardArgs);
|
||||||
this.shards.set(id, shard);
|
this.shards.set(id, shard);
|
||||||
/**
|
/**
|
||||||
* Emitted upon launching a shard
|
* Emitted upon launching a shard
|
||||||
@@ -74,10 +105,29 @@ 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) {
|
||||||
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
|
return new Promise((resolve, reject) => {
|
||||||
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
|
if (amount === 'auto') {
|
||||||
if (amount !== Math.floor(amount)) throw new RangeError('Amount of shards must be an integer.');
|
fetchRecommendedShards(this.token).then(count => {
|
||||||
|
this.totalShards = count;
|
||||||
|
resolve(this._spawn(count, delay));
|
||||||
|
}).catch(reject);
|
||||||
|
} else {
|
||||||
|
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
|
||||||
|
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||||
|
if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
|
||||||
|
resolve(this._spawn(amount, delay));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually spawns shards, unlike that poser above >:(
|
||||||
|
* @param {number} amount Number of shards to spawn
|
||||||
|
* @param {number} delay How long to wait in between spawning each shard (in milliseconds)
|
||||||
|
* @returns {Promise<Collection<number, Shard>>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_spawn(amount, delay) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
|
if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
|
||||||
this.totalShards = amount;
|
this.totalShards = amount;
|
||||||
|
|||||||
@@ -32,12 +32,21 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time the channel was created
|
* The timestamp the channel was created at
|
||||||
|
* @type {number}
|
||||||
* @readonly
|
* @readonly
|
||||||
* @type {Date}
|
|
||||||
*/
|
*/
|
||||||
get creationDate() {
|
get createdTimestamp() {
|
||||||
return new Date((this.id / 4194304) + 1420070400000);
|
return (this.id / 4194304) + 1420070400000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the channel was created
|
||||||
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get createdAt() {
|
||||||
|
return new Date(this.createdTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +56,7 @@ class Channel {
|
|||||||
* // delete the channel
|
* // delete the channel
|
||||||
* channel.delete()
|
* channel.delete()
|
||||||
* .then() // success
|
* .then() // success
|
||||||
* .catch(console.log); // log error
|
* .catch(console.error); // log error
|
||||||
*/
|
*/
|
||||||
delete() {
|
delete() {
|
||||||
return this.client.rest.methods.deleteChannel(this);
|
return this.client.rest.methods.deleteChannel(this);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
|
const Collection = require('../util/Collection');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the logged in client's Discord User
|
* Represents the logged in client's Discord User
|
||||||
@@ -19,8 +20,22 @@ class ClientUser extends User {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
this.email = data.email;
|
this.email = data.email;
|
||||||
|
this.localPresence = {};
|
||||||
this._typing = new Map();
|
this._typing = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Collection of friends for the logged in user.
|
||||||
|
* <warn>This is only filled for user accounts, not bot accounts!</warn>
|
||||||
|
* @type {Collection<string, User>}
|
||||||
|
*/
|
||||||
|
this.friends = new Collection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Collection of blocked users for the logged in user.
|
||||||
|
* <warn>This is only filled for user accounts, not bot accounts!</warn>
|
||||||
|
* @type {Collection<string, User>}
|
||||||
|
*/
|
||||||
|
this.blocked = new Collection();
|
||||||
}
|
}
|
||||||
|
|
||||||
edit(data) {
|
edit(data) {
|
||||||
@@ -37,7 +52,7 @@ class ClientUser extends User {
|
|||||||
* // set username
|
* // set username
|
||||||
* client.user.setUsername('discordjs')
|
* client.user.setUsername('discordjs')
|
||||||
* .then(user => console.log(`My new username is ${user.username}`))
|
* .then(user => console.log(`My new username is ${user.username}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setUsername(username) {
|
setUsername(username) {
|
||||||
return this.client.rest.methods.updateCurrentUser({ username });
|
return this.client.rest.methods.updateCurrentUser({ username });
|
||||||
@@ -52,7 +67,7 @@ class ClientUser extends User {
|
|||||||
* // set email
|
* // set email
|
||||||
* client.user.setEmail('bob@gmail.com')
|
* client.user.setEmail('bob@gmail.com')
|
||||||
* .then(user => console.log(`My new email is ${user.email}`))
|
* .then(user => console.log(`My new email is ${user.email}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setEmail(email) {
|
setEmail(email) {
|
||||||
return this.client.rest.methods.updateCurrentUser({ email });
|
return this.client.rest.methods.updateCurrentUser({ email });
|
||||||
@@ -65,9 +80,9 @@ class ClientUser extends User {
|
|||||||
* @returns {Promise<ClientUser>}
|
* @returns {Promise<ClientUser>}
|
||||||
* @example
|
* @example
|
||||||
* // set password
|
* // set password
|
||||||
* client.user.setPassword('password')
|
* client.user.setPassword('password123')
|
||||||
* .then(user => console.log('New password set!'))
|
* .then(user => console.log('New password set!'))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setPassword(password) {
|
setPassword(password) {
|
||||||
return this.client.rest.methods.updateCurrentUser({ password });
|
return this.client.rest.methods.updateCurrentUser({ password });
|
||||||
@@ -75,68 +90,144 @@ class ClientUser extends User {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the avatar of the logged in Client.
|
* Set the avatar of the logged in Client.
|
||||||
* @param {Base64Resolvable} avatar The new avatar
|
* @param {FileResolvable|Base64Resolveable} avatar The new avatar
|
||||||
* @returns {Promise<ClientUser>}
|
* @returns {Promise<ClientUser>}
|
||||||
* @example
|
* @example
|
||||||
* // set avatar
|
* // set avatar
|
||||||
* client.user.setAvatar(fs.readFileSync('./avatar.png'))
|
* client.user.setAvatar('./avatar.png')
|
||||||
* .then(user => console.log(`New avatar set!`))
|
* .then(user => console.log(`New avatar set!`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setAvatar(avatar) {
|
setAvatar(avatar) {
|
||||||
return this.client.rest.methods.updateCurrentUser({ avatar });
|
return new Promise(resolve => {
|
||||||
|
if (avatar.startsWith('data:')) {
|
||||||
|
resolve(this.client.rest.methods.updateCurrentUser({ avatar }));
|
||||||
|
} else {
|
||||||
|
this.client.resolver.resolveFile(avatar).then(data => {
|
||||||
|
resolve(this.client.rest.methods.updateCurrentUser({ avatar: data }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the status and playing game of the logged in client.
|
* Set the status of the logged in user.
|
||||||
* @param {string} [status] The status, can be `online` or `idle`
|
* @param {string} status can be `online`, `idle`, `invisible` or `dnd` (do not disturb)
|
||||||
* @param {string|Object} [game] The game that is being played
|
|
||||||
* @param {string} [url] If you want to display as streaming, set this as the URL to the stream (must be a
|
|
||||||
* twitch.tv URl)
|
|
||||||
* @returns {Promise<ClientUser>}
|
* @returns {Promise<ClientUser>}
|
||||||
* @example
|
|
||||||
* // set status
|
|
||||||
* client.user.setStatus('status', 'game')
|
|
||||||
* .then(user => console.log('Changed status!'))
|
|
||||||
* .catch(console.log);
|
|
||||||
*/
|
*/
|
||||||
setStatus(status, game = null, url = null) {
|
setStatus(status) {
|
||||||
|
return this.setPresence({ status });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current game of the logged in user.
|
||||||
|
* @param {string} game the game being played
|
||||||
|
* @param {string} [streamingURL] an optional URL to a twitch stream, if one is available.
|
||||||
|
* @returns {Promise<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 => {
|
return new Promise(resolve => {
|
||||||
if (status === 'online' || status === 'here' || status === 'available') {
|
if (!icon) resolve(this.client.rest.methods.createGuild({ name, icon, region }));
|
||||||
this.idleStatus = null;
|
if (icon.startsWith('data:')) {
|
||||||
} else if (status === 'idle' || status === 'away') {
|
resolve(this.client.rest.methods.createGuild({ name, icon, region }));
|
||||||
this.idleStatus = Date.now();
|
|
||||||
} else {
|
} else {
|
||||||
this.idleStatus = this.idleStatus || null;
|
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>}
|
||||||
|
*/
|
||||||
|
setPresence(data) {
|
||||||
|
// {"op":3,"d":{"status":"dnd","since":0,"game":null,"afk":false}}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let status = this.localPresence.status || this.presence.status;
|
||||||
|
let game = this.localPresence.game;
|
||||||
|
let afk = this.localPresence.afk || this.presence.afk;
|
||||||
|
|
||||||
|
if (!game && this.presence.game) {
|
||||||
|
game = {
|
||||||
|
name: this.presence.game.name,
|
||||||
|
type: this.presence.game.type,
|
||||||
|
url: this.presence.game.url,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof game === 'string' && !game.length) game = null;
|
if (data.status) {
|
||||||
|
if (typeof data.status !== 'string') throw new TypeError('Status must be a string');
|
||||||
if (game === null) {
|
status = data.status;
|
||||||
this.userGame = null;
|
|
||||||
} else if (!game) {
|
|
||||||
this.userGame = this.userGame || null;
|
|
||||||
} else if (typeof game === 'string') {
|
|
||||||
this.userGame = { name: game };
|
|
||||||
} else {
|
|
||||||
this.userGame = game;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url) {
|
if (data.game) {
|
||||||
this.userGame.url = url;
|
game = data.game;
|
||||||
this.userGame.type = 1;
|
if (game.url) game.type = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof data.afk !== 'undefined') afk = data.afk;
|
||||||
|
afk = Boolean(afk);
|
||||||
|
|
||||||
|
this.localPresence = { status, game, afk };
|
||||||
|
this.localPresence.since = 0;
|
||||||
|
this.localPresence.game = this.localPresence.game || null;
|
||||||
|
|
||||||
this.client.ws.send({
|
this.client.ws.send({
|
||||||
op: 3,
|
op: 3,
|
||||||
d: {
|
d: this.localPresence,
|
||||||
idle_since: this.idleStatus,
|
|
||||||
game: this.userGame,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.status = this.idleStatus ? 'idle' : 'online';
|
this.client._setPresence(this.id, this.localPresence);
|
||||||
this.game = this.userGame;
|
|
||||||
resolve(this);
|
resolve(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,12 +51,21 @@ class Emoji {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time the emoji was created
|
* The timestamp the emoji was created at
|
||||||
|
* @type {number}
|
||||||
* @readonly
|
* @readonly
|
||||||
* @type {Date}
|
|
||||||
*/
|
*/
|
||||||
get creationDate() {
|
get createdTimestamp() {
|
||||||
return new Date((this.id / 4194304) + 1420070400000);
|
return (this.id / 4194304) + 1420070400000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the emoji was created
|
||||||
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get createdAt() {
|
||||||
|
return new Date(this.createdTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,7 +95,7 @@ class Emoji {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
* @example
|
* @example
|
||||||
* // send an emoji:
|
* // send an emoji:
|
||||||
* const emoji = guild.emojis.array()[0];
|
* const emoji = guild.emojis.first();
|
||||||
* msg.reply(`Hello! ${emoji}`);
|
* msg.reply(`Hello! ${emoji}`);
|
||||||
*/
|
*/
|
||||||
toString() {
|
toString() {
|
||||||
|
|||||||
@@ -50,7 +50,17 @@ class EvaluatedPermissions {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
hasPermissions(permissions, explicit = false) {
|
hasPermissions(permissions, explicit = false) {
|
||||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
return permissions.every(p => this.hasPermission(p, explicit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the user has all specified permissions, and lists any missing permissions.
|
||||||
|
* @param {PermissionResolvable[]} permissions The permissions to check for
|
||||||
|
* @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions
|
||||||
|
* @returns {array}
|
||||||
|
*/
|
||||||
|
missingPermissions(permissions, explicit = false) {
|
||||||
|
return permissions.filter(p => !this.hasPermission(p, explicit));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class GroupDMChannel extends Channel {
|
|||||||
/**
|
/**
|
||||||
* The owner of this Group DM.
|
* The owner of this Group DM.
|
||||||
* @type {User}
|
* @type {User}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get owner() {
|
get owner() {
|
||||||
return this.client.users.get(this.ownerID);
|
return this.client.users.get(this.ownerID);
|
||||||
@@ -100,8 +101,8 @@ class GroupDMChannel extends Channel {
|
|||||||
this.ownerID === channel.ownerID;
|
this.ownerID === channel.ownerID;
|
||||||
|
|
||||||
if (equal) {
|
if (equal) {
|
||||||
const thisIDs = this.recipients.array().map(r => r.id);
|
const thisIDs = this.recipients.keyArray();
|
||||||
const otherIDs = channel.recipients.map(r => r.id);
|
const otherIDs = channel.recipients.keyArray();
|
||||||
return arraysEqual(thisIDs, otherIDs);
|
return arraysEqual(thisIDs, otherIDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
const Role = require('./Role');
|
const Role = require('./Role');
|
||||||
const Emoji = require('./Emoji');
|
const Emoji = require('./Emoji');
|
||||||
|
const Presence = require('./Presence').Presence;
|
||||||
const GuildMember = require('./GuildMember');
|
const GuildMember = require('./GuildMember');
|
||||||
const Constants = require('../util/Constants');
|
const Constants = require('../util/Constants');
|
||||||
const Collection = require('../util/Collection');
|
const Collection = require('../util/Collection');
|
||||||
@@ -100,6 +101,12 @@ 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[]}
|
||||||
@@ -137,10 +144,15 @@ class Guild {
|
|||||||
*/
|
*/
|
||||||
this.verificationLevel = data.verification_level;
|
this.verificationLevel = data.verification_level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp the client user joined the guild at
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.joinedTimestamp = data.joined_at ? new Date(data.joined_at).getTime() : this.joinedTimestamp;
|
||||||
|
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.available = !data.unavailable;
|
this.available = !data.unavailable;
|
||||||
this.features = data.features || this.features || [];
|
this.features = data.features || this.features || [];
|
||||||
this._joinedTimestamp = data.joined_at ? new Date(data.joined_at).getTime() : this._joinedTimestamp;
|
|
||||||
|
|
||||||
if (data.members) {
|
if (data.members) {
|
||||||
this.members.clear();
|
this.members.clear();
|
||||||
@@ -170,11 +182,7 @@ class Guild {
|
|||||||
|
|
||||||
if (data.presences) {
|
if (data.presences) {
|
||||||
for (const presence of data.presences) {
|
for (const presence of data.presences) {
|
||||||
const user = this.client.users.get(presence.user.id);
|
this._setPresence(presence.user.id, presence);
|
||||||
if (user) {
|
|
||||||
user.status = presence.status;
|
|
||||||
user.game = presence.game;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,20 +205,30 @@ class Guild {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time the guild was created
|
* The timestamp the guild was created at
|
||||||
|
* @type {number}
|
||||||
* @readonly
|
* @readonly
|
||||||
* @type {Date}
|
|
||||||
*/
|
*/
|
||||||
get creationDate() {
|
get createdTimestamp() {
|
||||||
return new Date((this.id / 4194304) + 1420070400000);
|
return (this.id / 4194304) + 1420070400000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date at which the logged-in client joined the guild.
|
* The time the guild was created
|
||||||
* @type {Date}
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get joinDate() {
|
get createdAt() {
|
||||||
return new Date(this._joinedTimestamp);
|
return new Date(this.createdTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the client user joined the guild
|
||||||
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get joinedAt() {
|
||||||
|
return new Date(this.joinedTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -278,6 +296,14 @@ class Guild {
|
|||||||
return this.client.rest.methods.getGuildInvites(this);
|
return this.client.rest.methods.getGuildInvites(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all webhooks for the guild.
|
||||||
|
* @returns {Collection<Webhook>}
|
||||||
|
*/
|
||||||
|
fetchWebhooks() {
|
||||||
|
return this.client.rest.methods.getGuildWebhooks(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a single guild member from a user.
|
* Fetch a single guild member from a user.
|
||||||
* @param {UserResolvable} user The user to fetch the member for
|
* @param {UserResolvable} user The user to fetch the member for
|
||||||
@@ -329,7 +355,7 @@ class Guild {
|
|||||||
* region: 'london',
|
* region: 'london',
|
||||||
* })
|
* })
|
||||||
* .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`))
|
* .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
edit(data) {
|
edit(data) {
|
||||||
return this.client.rest.methods.updateGuild(this, data);
|
return this.client.rest.methods.updateGuild(this, data);
|
||||||
@@ -343,7 +369,7 @@ class Guild {
|
|||||||
* // edit the guild name
|
* // edit the guild name
|
||||||
* guild.setName('Discord Guild')
|
* guild.setName('Discord Guild')
|
||||||
* .then(updated => console.log(`Updated guild name to ${guild.name}`))
|
* .then(updated => console.log(`Updated guild name to ${guild.name}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setName(name) {
|
setName(name) {
|
||||||
return this.edit({ name });
|
return this.edit({ name });
|
||||||
@@ -357,7 +383,7 @@ class Guild {
|
|||||||
* // edit the guild region
|
* // edit the guild region
|
||||||
* guild.setRegion('london')
|
* guild.setRegion('london')
|
||||||
* .then(updated => console.log(`Updated guild region to ${guild.region}`))
|
* .then(updated => console.log(`Updated guild region to ${guild.region}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setRegion(region) {
|
setRegion(region) {
|
||||||
return this.edit({ region });
|
return this.edit({ region });
|
||||||
@@ -371,7 +397,7 @@ class Guild {
|
|||||||
* // edit the guild verification level
|
* // edit the guild verification level
|
||||||
* guild.setVerificationLevel(1)
|
* guild.setVerificationLevel(1)
|
||||||
* .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`))
|
* .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setVerificationLevel(verificationLevel) {
|
setVerificationLevel(verificationLevel) {
|
||||||
return this.edit({ verificationLevel });
|
return this.edit({ verificationLevel });
|
||||||
@@ -385,7 +411,7 @@ class Guild {
|
|||||||
* // edit the guild AFK channel
|
* // edit the guild AFK channel
|
||||||
* guild.setAFKChannel(channel)
|
* guild.setAFKChannel(channel)
|
||||||
* .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`))
|
* .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setAFKChannel(afkChannel) {
|
setAFKChannel(afkChannel) {
|
||||||
return this.edit({ afkChannel });
|
return this.edit({ afkChannel });
|
||||||
@@ -399,7 +425,7 @@ class Guild {
|
|||||||
* // edit the guild AFK channel
|
* // edit the guild AFK channel
|
||||||
* guild.setAFKTimeout(60)
|
* guild.setAFKTimeout(60)
|
||||||
* .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`))
|
* .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setAFKTimeout(afkTimeout) {
|
setAFKTimeout(afkTimeout) {
|
||||||
return this.edit({ afkTimeout });
|
return this.edit({ afkTimeout });
|
||||||
@@ -413,7 +439,7 @@ class Guild {
|
|||||||
* // edit the guild icon
|
* // edit the guild icon
|
||||||
* guild.setIcon(fs.readFileSync('./icon.png'))
|
* guild.setIcon(fs.readFileSync('./icon.png'))
|
||||||
* .then(updated => console.log('Updated the guild icon'))
|
* .then(updated => console.log('Updated the guild icon'))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setIcon(icon) {
|
setIcon(icon) {
|
||||||
return this.edit({ icon });
|
return this.edit({ icon });
|
||||||
@@ -427,7 +453,7 @@ class Guild {
|
|||||||
* // edit the guild owner
|
* // edit the guild owner
|
||||||
* guild.setOwner(guilds.members[0])
|
* guild.setOwner(guilds.members[0])
|
||||||
* .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`))
|
* .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setOwner(owner) {
|
setOwner(owner) {
|
||||||
return this.edit({ owner });
|
return this.edit({ owner });
|
||||||
@@ -441,7 +467,7 @@ class Guild {
|
|||||||
* // edit the guild splash
|
* // edit the guild splash
|
||||||
* guild.setIcon(fs.readFileSync('./splash.png'))
|
* guild.setIcon(fs.readFileSync('./splash.png'))
|
||||||
* .then(updated => console.log('Updated the guild splash'))
|
* .then(updated => console.log('Updated the guild splash'))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setSplash(splash) {
|
setSplash(splash) {
|
||||||
return this.edit({ splash });
|
return this.edit({ splash });
|
||||||
@@ -514,7 +540,7 @@ class Guild {
|
|||||||
* // create a new text channel
|
* // create a new text channel
|
||||||
* guild.createChannel('new-general', 'text')
|
* guild.createChannel('new-general', 'text')
|
||||||
* .then(channel => console.log(`Created new channel ${channel}`))
|
* .then(channel => console.log(`Created new channel ${channel}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
createChannel(name, type) {
|
createChannel(name, type) {
|
||||||
return this.client.rest.methods.createChannel(this, name, type);
|
return this.client.rest.methods.createChannel(this, name, type);
|
||||||
@@ -528,12 +554,12 @@ class Guild {
|
|||||||
* // create a new role
|
* // create a new role
|
||||||
* guild.createRole()
|
* guild.createRole()
|
||||||
* .then(role => console.log(`Created role ${role}`))
|
* .then(role => console.log(`Created role ${role}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
* @example
|
* @example
|
||||||
* // create a new role with data
|
* // create a new role with data
|
||||||
* guild.createRole({ name: 'Super Cool People' })
|
* guild.createRole({ name: 'Super Cool People' })
|
||||||
* .then(role => console.log(`Created role ${role}`))
|
* .then(role => console.log(`Created role ${role}`))
|
||||||
* .catch(console.log)
|
* .catch(console.error)
|
||||||
*/
|
*/
|
||||||
createRole(data) {
|
createRole(data) {
|
||||||
const create = this.client.rest.methods.createGuildRole(this);
|
const create = this.client.rest.methods.createGuildRole(this);
|
||||||
@@ -541,6 +567,43 @@ class Guild {
|
|||||||
return create.then(role => role.edit(data));
|
return create.then(role => role.edit(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new custom emoji in the guild.
|
||||||
|
* @param {FileResolveable} attachment The image for the emoji.
|
||||||
|
* @param {string} name The name for the emoji.
|
||||||
|
* @returns {Promise<Emoji>} The created emoji.
|
||||||
|
* @example
|
||||||
|
* // create a new emoji from a url
|
||||||
|
* guild.createEmoji('https://i.imgur.com/w3duR07.png', 'rip')
|
||||||
|
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
|
||||||
|
* .catch(console.error);
|
||||||
|
* @example
|
||||||
|
* // create a new emoji from a file on your computer
|
||||||
|
* guild.createEmoji('./memes/banana.png', 'banana')
|
||||||
|
* .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`))
|
||||||
|
* .catch(console.error);
|
||||||
|
*/
|
||||||
|
createEmoji(attachment, name) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.resolver.resolveFile(attachment).then(file => {
|
||||||
|
let base64 = new Buffer(file, 'binary').toString('base64');
|
||||||
|
let dataURI = `data:;base64,${base64}`;
|
||||||
|
this.client.rest.methods.createEmoji(this, dataURI, name)
|
||||||
|
.then(resolve).catch(reject);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an emoji.
|
||||||
|
* @param {Emoji|string} emoji The emoji to delete.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
deleteEmoji(emoji) {
|
||||||
|
if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji);
|
||||||
|
return this.client.rest.methods.deleteEmoji(emoji);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Causes the Client to leave the guild.
|
* Causes the Client to leave the guild.
|
||||||
* @returns {Promise<Guild>}
|
* @returns {Promise<Guild>}
|
||||||
@@ -548,7 +611,7 @@ class Guild {
|
|||||||
* // leave a guild
|
* // leave a guild
|
||||||
* guild.leave()
|
* guild.leave()
|
||||||
* .then(g => console.log(`Left the guild ${g}`))
|
* .then(g => console.log(`Left the guild ${g}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
leave() {
|
leave() {
|
||||||
return this.client.rest.methods.leaveGuild(this);
|
return this.client.rest.methods.leaveGuild(this);
|
||||||
@@ -561,12 +624,38 @@ class Guild {
|
|||||||
* // delete a guild
|
* // delete a guild
|
||||||
* guild.delete()
|
* guild.delete()
|
||||||
* .then(g => console.log(`Deleted the guild ${g}`))
|
* .then(g => console.log(`Deleted the guild ${g}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
delete() {
|
delete() {
|
||||||
return this.client.rest.methods.deleteGuild(this);
|
return this.client.rest.methods.deleteGuild(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the position of a role in this guild
|
||||||
|
* @param {string|Role} role the role to edit, can be a role object or a role ID.
|
||||||
|
* @param {number} position the new position of the role
|
||||||
|
* @returns {Promise<Guild>}
|
||||||
|
*/
|
||||||
|
setRolePosition(role, position) {
|
||||||
|
if (role instanceof Role) {
|
||||||
|
role = role.id;
|
||||||
|
} else if (typeof role !== 'string') {
|
||||||
|
return Promise.reject(new Error('Supplied role is not a role or string'));
|
||||||
|
}
|
||||||
|
|
||||||
|
position = Number(position);
|
||||||
|
if (isNaN(position)) {
|
||||||
|
return Promise.reject(new Error('Supplied position is not a number'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedRoles = this.roles.array().map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
position: r.id === role ? position : (r.position < position ? r.position : r.position + 1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return this.client.rest.methods.setRolePositions(this.id, updatedRoles);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@@ -616,6 +705,7 @@ class Guild {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_addMember(guildUser, emitEvent = true) {
|
_addMember(guildUser, emitEvent = true) {
|
||||||
|
const existing = this.members.has(guildUser.user.id);
|
||||||
if (!(guildUser.user instanceof User)) guildUser.user = this.client.dataManager.newUser(guildUser.user);
|
if (!(guildUser.user instanceof User)) guildUser.user = this.client.dataManager.newUser(guildUser.user);
|
||||||
|
|
||||||
guildUser.joined_at = guildUser.joined_at || 0;
|
guildUser.joined_at = guildUser.joined_at || 0;
|
||||||
@@ -636,11 +726,10 @@ class Guild {
|
|||||||
/**
|
/**
|
||||||
* Emitted whenever a user joins a guild.
|
* Emitted whenever a user joins a guild.
|
||||||
* @event Client#guildMemberAdd
|
* @event Client#guildMemberAdd
|
||||||
* @param {Guild} guild The guild that the user has joined
|
* @param {GuildMember} member The member that has joined a guild
|
||||||
* @param {GuildMember} member The member that has joined
|
|
||||||
*/
|
*/
|
||||||
if (this.client.ws.status === Constants.Status.READY && emitEvent) {
|
if (this.client.ws.status === Constants.Status.READY && emitEvent && !existing) {
|
||||||
this.client.emit(Constants.Events.GUILD_MEMBER_ADD, this, member);
|
this.client.emit(Constants.Events.GUILD_MEMBER_ADD, member);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._checkChunks();
|
this._checkChunks();
|
||||||
@@ -659,11 +748,10 @@ class Guild {
|
|||||||
/**
|
/**
|
||||||
* 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 {Guild} guild The guild that the update affects
|
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
this.client.emit(Constants.Events.GUILD_MEMBER_UPDATE, this, oldMember, member);
|
this.client.emit(Constants.Events.GUILD_MEMBER_UPDATE, oldMember, member);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -691,6 +779,14 @@ class Guild {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setPresence(id, presence) {
|
||||||
|
if (this.presences.get(id)) {
|
||||||
|
this.presences.get(id).update(presence);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.presences.set(id, new Presence(presence));
|
||||||
|
}
|
||||||
|
|
||||||
_checkChunks() {
|
_checkChunks() {
|
||||||
if (this._fetchWaiter) {
|
if (this._fetchWaiter) {
|
||||||
if (this.members.size === this.memberCount) {
|
if (this.members.size === this.memberCount) {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class GuildChannel extends Channel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Overwrites the permissions for a user or role in this channel.
|
* Overwrites the permissions for a user or role in this channel.
|
||||||
* @param {Role|UserResolvable} userOrRole The user or role to update
|
* @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
|
||||||
* @param {PermissionOverwriteOptions} options The configuration for the update
|
* @param {PermissionOverwriteOptions} options The configuration for the update
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
* @example
|
* @example
|
||||||
@@ -120,7 +120,7 @@ class GuildChannel extends Channel {
|
|||||||
* SEND_MESSAGES: false
|
* SEND_MESSAGES: false
|
||||||
* })
|
* })
|
||||||
* .then(() => console.log('Done!'))
|
* .then(() => console.log('Done!'))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
overwritePermissions(userOrRole, options) {
|
overwritePermissions(userOrRole, options) {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -130,6 +130,9 @@ class GuildChannel extends Channel {
|
|||||||
|
|
||||||
if (userOrRole instanceof Role) {
|
if (userOrRole instanceof Role) {
|
||||||
payload.type = 'role';
|
payload.type = 'role';
|
||||||
|
} else if (this.guild.roles.has(userOrRole)) {
|
||||||
|
userOrRole = this.guild.roles.get(userOrRole);
|
||||||
|
payload.type = 'role';
|
||||||
} else {
|
} else {
|
||||||
userOrRole = this.client.resolver.resolveUser(userOrRole);
|
userOrRole = this.client.resolver.resolveUser(userOrRole);
|
||||||
payload.type = 'member';
|
payload.type = 'member';
|
||||||
@@ -141,8 +144,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.allow;
|
payload.allow = prevOverwrite.allowData;
|
||||||
payload.deny = prevOverwrite.deny;
|
payload.deny = prevOverwrite.denyData;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const perm in options) {
|
for (const perm in options) {
|
||||||
@@ -170,7 +173,7 @@ class GuildChannel extends Channel {
|
|||||||
* // set a new channel name
|
* // set a new channel name
|
||||||
* channel.setName('not_general')
|
* channel.setName('not_general')
|
||||||
* .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))
|
* .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setName(name) {
|
setName(name) {
|
||||||
return this.client.rest.methods.updateChannel(this, { name });
|
return this.client.rest.methods.updateChannel(this, { name });
|
||||||
@@ -184,7 +187,7 @@ class GuildChannel extends Channel {
|
|||||||
* // set a new channel position
|
* // set a new channel position
|
||||||
* channel.setPosition(2)
|
* channel.setPosition(2)
|
||||||
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
|
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setPosition(position) {
|
setPosition(position) {
|
||||||
return this.client.rest.methods.updateChannel(this, { position });
|
return this.client.rest.methods.updateChannel(this, { position });
|
||||||
@@ -198,7 +201,7 @@ class GuildChannel extends Channel {
|
|||||||
* // set a new channel topic
|
* // set a new channel topic
|
||||||
* channel.setTopic('needs more rate limiting')
|
* channel.setTopic('needs more rate limiting')
|
||||||
* .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
|
* .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setTopic(topic) {
|
setTopic(topic) {
|
||||||
return this.client.rest.methods.updateChannel(this, { topic });
|
return this.client.rest.methods.updateChannel(this, { topic });
|
||||||
@@ -236,12 +239,12 @@ class GuildChannel extends Channel {
|
|||||||
this.name === channel.name;
|
this.name === channel.name;
|
||||||
|
|
||||||
if (equal) {
|
if (equal) {
|
||||||
if (channel.permission_overwrites) {
|
if (this.permissionOverwrites && channel.permissionOverwrites) {
|
||||||
const thisIDSet = Array.from(this.permissionOverwrites.keys());
|
const thisIDSet = this.permissionOverwrites.keyArray();
|
||||||
const otherIDSet = channel.permission_overwrites.map(overwrite => overwrite.id);
|
const otherIDSet = channel.permissionOverwrites.keyArray();
|
||||||
equal = arraysEqual(thisIDSet, otherIDSet);
|
equal = arraysEqual(thisIDSet, otherIDSet);
|
||||||
} else {
|
} else {
|
||||||
equal = false;
|
equal = !this.permissionOverwrites && !channel.permissionOverwrites;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,17 +82,32 @@ class GuildMember {
|
|||||||
*/
|
*/
|
||||||
this.nickname = data.nick || null;
|
this.nickname = data.nick || null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp the member joined the guild at
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.joinedTimestamp = new Date(data.joined_at).getTime();
|
||||||
|
|
||||||
this.user = data.user;
|
this.user = data.user;
|
||||||
this._roles = data.roles;
|
this._roles = data.roles;
|
||||||
this._joinDate = new Date(data.joined_at).getTime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date this member joined the guild
|
* The time the member joined the guild
|
||||||
* @type {Date}
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get joinDate() {
|
get joinedAt() {
|
||||||
return new Date(this._joinDate);
|
return new Date(this.joinedTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The presence of this Guild Member
|
||||||
|
* @type {Presence}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get presence() {
|
||||||
|
return this.frozenPresence || this.guild.presences.get(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,11 +132,10 @@ class GuildMember {
|
|||||||
/**
|
/**
|
||||||
* The role of the member with the highest position.
|
* The role of the member with the highest position.
|
||||||
* @type {Role}
|
* @type {Role}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get highestRole() {
|
get highestRole() {
|
||||||
return this.roles.reduce((prev, role) =>
|
return this.roles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev);
|
||||||
!prev || role.position > prev.position || (role.position === prev.position && role.id < prev.id) ? role : prev
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,6 +177,7 @@ class GuildMember {
|
|||||||
/**
|
/**
|
||||||
* 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}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get permissions() {
|
get permissions() {
|
||||||
if (this.user.id === this.guild.ownerID) return new EvaluatedPermissions(this, Constants.ALL_PERMISSIONS);
|
if (this.user.id === this.guild.ownerID) return new EvaluatedPermissions(this, Constants.ALL_PERMISSIONS);
|
||||||
@@ -180,25 +195,27 @@ class GuildMember {
|
|||||||
/**
|
/**
|
||||||
* Whether the member is kickable by the client user.
|
* Whether the member is kickable by the client user.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get kickable() {
|
get kickable() {
|
||||||
if (this.user.id === this.guild.ownerID) return false;
|
if (this.user.id === this.guild.ownerID) return false;
|
||||||
if (this.user.id === this.client.user.id) return false;
|
if (this.user.id === this.client.user.id) return false;
|
||||||
const clientMember = this.member(this.client.member);
|
const clientMember = this.guild.member(this.client.user);
|
||||||
if (!clientMember.hasPermission(Constants.PermissionFlags.KICK_MEMBERS)) return false;
|
if (!clientMember.hasPermission(Constants.PermissionFlags.KICK_MEMBERS)) return false;
|
||||||
return clientMember.highestRole.position > this.highestRole.positon;
|
return clientMember.highestRole.comparePositionTo(this.highestRole) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the member is bannable by the client user.
|
* Whether the member is bannable by the client user.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get bannable() {
|
get bannable() {
|
||||||
if (this.user.id === this.guild.ownerID) return false;
|
if (this.user.id === this.guild.ownerID) return false;
|
||||||
if (this.user.id === this.client.user.id) return false;
|
if (this.user.id === this.client.user.id) return false;
|
||||||
const clientMember = this.member(this.client.member);
|
const clientMember = this.guild.member(this.client.user);
|
||||||
if (!clientMember.hasPermission(Constants.PermissionFlags.BAN_MEMBERS)) return false;
|
if (!clientMember.hasPermission(Constants.PermissionFlags.BAN_MEMBERS)) return false;
|
||||||
return clientMember.highestRole.position > this.highestRole.positon;
|
return clientMember.highestRole.comparePositionTo(this.highestRole) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,7 +248,17 @@ class GuildMember {
|
|||||||
*/
|
*/
|
||||||
hasPermissions(permissions, explicit = false) {
|
hasPermissions(permissions, explicit = false) {
|
||||||
if (!explicit && this.user.id === this.guild.ownerID) return true;
|
if (!explicit && this.user.id === this.guild.ownerID) return true;
|
||||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
return permissions.every(p => this.hasPermission(p, explicit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the roles of the member allows them to perform specific actions, and lists any missing permissions.
|
||||||
|
* @param {PermissionResolvable[]} permissions The permissions to check for
|
||||||
|
* @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions
|
||||||
|
* @returns {array}
|
||||||
|
*/
|
||||||
|
missingPermissions(permissions, explicit = false) {
|
||||||
|
return permissions.filter(p => !this.hasPermission(p, explicit));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -380,7 +407,7 @@ class GuildMember {
|
|||||||
* console.log(`Hello from ${member}!`);
|
* console.log(`Hello from ${member}!`);
|
||||||
*/
|
*/
|
||||||
toString() {
|
toString() {
|
||||||
return String(this.user);
|
return `<@${this.nickname ? '!' : ''}${this.user.id}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
||||||
|
|||||||
@@ -92,23 +92,38 @@ class Invite {
|
|||||||
*/
|
*/
|
||||||
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);
|
||||||
|
|
||||||
this._createdAt = new Date(data.created_at).getTime();
|
/**
|
||||||
|
* The timestamp the invite was created at
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.createdTimestamp = new Date(data.created_at).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The creation date of the invite
|
* The time the invite was created
|
||||||
* @type {Date}
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get createdAt() {
|
get createdAt() {
|
||||||
return new Date(this._createdAt);
|
return new Date(this.createdTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The creation date of the invite
|
* The timestamp the invite will expire at
|
||||||
* @type {Date}
|
* @type {number}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get creationDate() {
|
get expiresTimestamp() {
|
||||||
return new Date(this._createdAt);
|
return this.createdTimestamp + (this.maxAge * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the invite will expire
|
||||||
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get expiresAt() {
|
||||||
|
return new Date(this.expiresTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const Attachment = require('./MessageAttachment');
|
|||||||
const Embed = require('./MessageEmbed');
|
const Embed = require('./MessageEmbed');
|
||||||
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');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Message on Discord
|
* Represents a Message on Discord
|
||||||
@@ -31,6 +32,12 @@ class Message {
|
|||||||
*/
|
*/
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the message
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.type = Constants.MessageTypes[data.type];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The content of the message
|
* The content of the message
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -87,6 +94,18 @@ class Message {
|
|||||||
this.attachments = new Collection();
|
this.attachments = new Collection();
|
||||||
for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment));
|
for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp the message was sent at
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.createdTimestamp = new Date(data.timestamp).getTime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp the message was last edited at (if applicable)
|
||||||
|
* @type {?number}
|
||||||
|
*/
|
||||||
|
this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object containing a further users, roles or channels collections
|
* An object containing a further users, roles or channels collections
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
@@ -128,8 +147,6 @@ class Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._timestamp = new Date(data.timestamp).getTime();
|
|
||||||
this._editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
|
|
||||||
this._edits = [];
|
this._edits = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,9 +156,9 @@ class Message {
|
|||||||
if (this.guild) this.member = this.guild.member(this.author);
|
if (this.guild) this.member = this.guild.member(this.author);
|
||||||
}
|
}
|
||||||
if (data.content) this.content = data.content;
|
if (data.content) this.content = data.content;
|
||||||
if (data.timestamp) this._timestamp = new Date(data.timestamp).getTime();
|
if (data.timestamp) this.createdTimestamp = new Date(data.timestamp).getTime();
|
||||||
if (data.edited_timestamp) {
|
if (data.edited_timestamp) {
|
||||||
this._editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
|
this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null;
|
||||||
}
|
}
|
||||||
if ('tts' in data) this.tts = data.tts;
|
if ('tts' in data) this.tts = data.tts;
|
||||||
if ('mention_everyone' in data) this.mentions.everyone = data.mention_everyone;
|
if ('mention_everyone' in data) this.mentions.everyone = data.mention_everyone;
|
||||||
@@ -185,24 +202,27 @@ class Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the message was sent
|
* The time the message was sent
|
||||||
* @type {Date}
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get timestamp() {
|
get createdAt() {
|
||||||
return new Date(this._timestamp);
|
return new Date(this.createdTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the message was edited, the timestamp at which it was last edited
|
* The time the message was last edited at (if applicable)
|
||||||
* @type {?Date}
|
* @type {?Date}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get editedTimestamp() {
|
get editedAt() {
|
||||||
return new Date(this._editedTimestamp);
|
return this.editedTimestamp ? new Date(this.editedTimestamp) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The guild the message was sent in (if in a guild channel)
|
* The guild the message was sent in (if in a guild channel)
|
||||||
* @type {?Guild}
|
* @type {?Guild}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get guild() {
|
get guild() {
|
||||||
return this.channel.guild || null;
|
return this.channel.guild || null;
|
||||||
@@ -212,11 +232,11 @@ class Message {
|
|||||||
* The message contents with all mentions replaced by the equivalent text. If mentions cannot be resolved to a name,
|
* The message contents with all mentions replaced by the equivalent text. If mentions cannot be resolved to a name,
|
||||||
* the relevant mention in the message content will not be converted.
|
* the relevant mention in the message content will not be converted.
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get cleanContent() {
|
get cleanContent() {
|
||||||
return this.content
|
return this.content
|
||||||
.replace(/@everyone/g, '@\u200Beveryone')
|
.replace(/@(everyone|here)/g, '@\u200b$1')
|
||||||
.replace(/@here/g, '@\u200Bhere')
|
|
||||||
.replace(/<@!?[0-9]+>/g, (input) => {
|
.replace(/<@!?[0-9]+>/g, (input) => {
|
||||||
const id = input.replace(/<|!|>|@/g, '');
|
const id = input.replace(/<|!|>|@/g, '');
|
||||||
if (this.channel.type === 'dm' || this.channel.type === 'group') {
|
if (this.channel.type === 'dm' || this.channel.type === 'group') {
|
||||||
@@ -250,6 +270,7 @@ 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
|
||||||
*/
|
*/
|
||||||
get edits() {
|
get edits() {
|
||||||
return this._edits.slice().unshift(this);
|
return this._edits.slice().unshift(this);
|
||||||
@@ -258,6 +279,7 @@ class Message {
|
|||||||
/**
|
/**
|
||||||
* Whether the message is editable by the client user.
|
* Whether the message is editable by the client user.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get editable() {
|
get editable() {
|
||||||
return this.author.id === this.client.user.id;
|
return this.author.id === this.client.user.id;
|
||||||
@@ -266,6 +288,7 @@ class Message {
|
|||||||
/**
|
/**
|
||||||
* Whether the message is deletable by the client user.
|
* Whether the message is deletable by the client user.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get deletable() {
|
get deletable() {
|
||||||
return this.author.id === this.client.user.id || (this.guild &&
|
return this.author.id === this.client.user.id || (this.guild &&
|
||||||
@@ -276,6 +299,7 @@ class Message {
|
|||||||
/**
|
/**
|
||||||
* Whether the message is pinnable by the client user.
|
* Whether the message is pinnable by the client user.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get pinnable() {
|
get pinnable() {
|
||||||
return !this.guild ||
|
return !this.guild ||
|
||||||
@@ -301,7 +325,7 @@ class Message {
|
|||||||
* // update the content of a message
|
* // update the content of a message
|
||||||
* message.edit('This is my new content!')
|
* message.edit('This is my new content!')
|
||||||
* .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.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
edit(content) {
|
edit(content) {
|
||||||
return this.client.rest.methods.updateMessage(this, content);
|
return this.client.rest.methods.updateMessage(this, content);
|
||||||
@@ -314,8 +338,8 @@ class Message {
|
|||||||
* @returns {Promise<Message>}
|
* @returns {Promise<Message>}
|
||||||
*/
|
*/
|
||||||
editCode(lang, content) {
|
editCode(lang, content) {
|
||||||
content = this.client.resolver.resolveString(content).replace(/```/g, '`\u200b``');
|
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||||
return this.edit(`\`\`\`${lang ? lang : ''}\n${content}\n\`\`\``);
|
return this.edit(`\`\`\`${lang || ''}\n${content}\n\`\`\``);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -342,7 +366,7 @@ class Message {
|
|||||||
* // delete a message
|
* // delete a message
|
||||||
* message.delete()
|
* message.delete()
|
||||||
* .then(msg => console.log(`Deleted message from ${msg.author}`))
|
* .then(msg => console.log(`Deleted message from ${msg.author}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
delete(timeout = 0) {
|
delete(timeout = 0) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -363,7 +387,7 @@ class Message {
|
|||||||
* // 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.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
reply(content, options = {}) {
|
reply(content, options = {}) {
|
||||||
content = this.client.resolver.resolveString(content);
|
content = this.client.resolver.resolveString(content);
|
||||||
@@ -401,8 +425,8 @@ class Message {
|
|||||||
|
|
||||||
if (equal && rawData) {
|
if (equal && rawData) {
|
||||||
equal = this.mentions.everyone === message.mentions.everyone &&
|
equal = this.mentions.everyone === message.mentions.everyone &&
|
||||||
this._timestamp === new Date(rawData.timestamp).getTime() &&
|
this.createdTimestamp === new Date(rawData.timestamp).getTime() &&
|
||||||
this._editedTimestamp === new Date(rawData.edited_timestamp).getTime();
|
this.editedTimestamp === new Date(rawData.edited_timestamp).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
return equal;
|
return equal;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class MessageCollector extends EventEmitter {
|
|||||||
* @typedef {Object} CollectorOptions
|
* @typedef {Object} CollectorOptions
|
||||||
* @property {number} [time] Duration for the collector in milliseconds
|
* @property {number} [time] Duration for the collector in milliseconds
|
||||||
* @property {number} [max] Maximum number of messages to handle
|
* @property {number} [max] Maximum number of messages to handle
|
||||||
|
* @property {number} [maxMatches] Maximum number of successfully filtered messages to obtain
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,12 +87,46 @@ class MessageCollector extends EventEmitter {
|
|||||||
* @event MessageCollector#message
|
* @event MessageCollector#message
|
||||||
*/
|
*/
|
||||||
this.emit('message', message, this);
|
this.emit('message', message, this);
|
||||||
if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
|
if (this.collected.size >= this.options.maxMatches) this.stop('matchesLimit');
|
||||||
|
else if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a promise that resolves when a valid message is sent. Rejects
|
||||||
|
* with collected messages if the Collector ends before receiving a message.
|
||||||
|
* @type {Promise<Message>}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get next() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.ended) {
|
||||||
|
reject(this.collected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
this.removeListener('message', onMessage);
|
||||||
|
this.removeListener('end', onEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessage = (...args) => {
|
||||||
|
cleanup();
|
||||||
|
resolve(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnd = (...args) => {
|
||||||
|
cleanup();
|
||||||
|
reject(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.once('message', onMessage);
|
||||||
|
this.once('end', onEnd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the collector and emits `end`.
|
* Stops the collector and emits `end`.
|
||||||
* @param {string} [reason='user'] An optional reason for stopping the collector
|
* @param {string} [reason='user'] An optional reason for stopping the collector
|
||||||
|
|||||||
93
src/structures/Presence.js
Normal file
93
src/structures/Presence.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Represents a User's presence
|
||||||
|
*/
|
||||||
|
class Presence {
|
||||||
|
constructor(data) {
|
||||||
|
if (!data) return;
|
||||||
|
/**
|
||||||
|
* The status of the presence:
|
||||||
|
*
|
||||||
|
* * **`online`** - user is online
|
||||||
|
* * **`offline`** - user is offline or invisible
|
||||||
|
* * **`idle`** - user is AFK
|
||||||
|
* * **`dnd`** - user is in Do not Disturb
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.status = data.status || 'offline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The game that the user is playing, `null` if they aren't playing a game.
|
||||||
|
* @type {?Game}
|
||||||
|
*/
|
||||||
|
this.game = data.game ? new Game(data.game) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data) {
|
||||||
|
this.status = data.status || this.status;
|
||||||
|
this.game = data.game ? new Game(data.game) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this presence is equal to another
|
||||||
|
* @param {Presence} other the presence to compare
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
equals(other) {
|
||||||
|
return (
|
||||||
|
other &&
|
||||||
|
this.status === other.status &&
|
||||||
|
this.game ? this.game.equals(other.game) : !other.game
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Game that is part of a User's presence.
|
||||||
|
*/
|
||||||
|
class Game {
|
||||||
|
constructor(data) {
|
||||||
|
/**
|
||||||
|
* The name of the game being played
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.name = data.name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the game status
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.type = data.type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the game is being streamed, a link to the stream
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
this.url = data.url || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the game is being streamed
|
||||||
|
* @type {boolean}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get streaming() {
|
||||||
|
return this.type === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this game is equal to another game
|
||||||
|
* @param {Game} other the other game to compare
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
equals(other) {
|
||||||
|
return (
|
||||||
|
other &&
|
||||||
|
this.name === other.name &&
|
||||||
|
this.type === other.type &&
|
||||||
|
this.url === other.url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.Presence = Presence;
|
||||||
|
exports.Game = Game;
|
||||||
@@ -72,12 +72,21 @@ class Role {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time the role was created
|
* The timestamp the role was created at
|
||||||
|
* @type {number}
|
||||||
* @readonly
|
* @readonly
|
||||||
* @type {Date}
|
|
||||||
*/
|
*/
|
||||||
get creationDate() {
|
get createdTimestamp() {
|
||||||
return new Date((this.id / 4194304) + 1420070400000);
|
return (this.id / 4194304) + 1420070400000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the role was created
|
||||||
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get createdAt() {
|
||||||
|
return new Date(this.createdTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,6 +103,7 @@ class Role {
|
|||||||
/**
|
/**
|
||||||
* The cached guild members that have this role.
|
* The cached guild members that have this role.
|
||||||
* @type {Collection<string, GuildMember>}
|
* @type {Collection<string, GuildMember>}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get members() {
|
get members() {
|
||||||
return this.guild.members.filter(m => m.roles.has(this.id));
|
return this.guild.members.filter(m => m.roles.has(this.id));
|
||||||
@@ -140,7 +150,17 @@ class Role {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
hasPermissions(permissions, explicit = false) {
|
hasPermissions(permissions, explicit = false) {
|
||||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
return permissions.every(p => this.hasPermission(p, explicit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares this role's position to another role's.
|
||||||
|
* @param {Role} role Role to compare to this one
|
||||||
|
* @returns {number} Negative number if the this role's position is lower (other role's is higher),
|
||||||
|
* positive number if the this one is higher (other's is lower), 0 if equal
|
||||||
|
*/
|
||||||
|
comparePositionTo(role) {
|
||||||
|
return this.constructor.comparePositions(this, role);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,7 +171,7 @@ class Role {
|
|||||||
* // edit a role
|
* // edit a role
|
||||||
* role.edit({name: 'new role'})
|
* role.edit({name: 'new role'})
|
||||||
* .then(r => console.log(`Edited role ${r}`))
|
* .then(r => console.log(`Edited role ${r}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
edit(data) {
|
edit(data) {
|
||||||
return this.client.rest.methods.updateGuildRole(this, data);
|
return this.client.rest.methods.updateGuildRole(this, data);
|
||||||
@@ -165,7 +185,7 @@ class Role {
|
|||||||
* // set the name of the role
|
* // set the name of the role
|
||||||
* role.setName('new role')
|
* role.setName('new role')
|
||||||
* .then(r => console.log(`Edited name of role ${r}`))
|
* .then(r => console.log(`Edited name of role ${r}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setName(name) {
|
setName(name) {
|
||||||
return this.client.rest.methods.updateGuildRole(this, { name });
|
return this.client.rest.methods.updateGuildRole(this, { name });
|
||||||
@@ -179,7 +199,7 @@ class Role {
|
|||||||
* // set the color of a role
|
* // set the color of a role
|
||||||
* role.setColor('#FF0000')
|
* role.setColor('#FF0000')
|
||||||
* .then(r => console.log(`Set color of role ${r}`))
|
* .then(r => console.log(`Set color of role ${r}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setColor(color) {
|
setColor(color) {
|
||||||
return this.client.rest.methods.updateGuildRole(this, { color });
|
return this.client.rest.methods.updateGuildRole(this, { color });
|
||||||
@@ -193,7 +213,7 @@ class Role {
|
|||||||
* // set the hoist of the role
|
* // set the hoist of the role
|
||||||
* role.setHoist(true)
|
* role.setHoist(true)
|
||||||
* .then(r => console.log(`Role hoisted: ${r.hoist}`))
|
* .then(r => console.log(`Role hoisted: ${r.hoist}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setHoist(hoist) {
|
setHoist(hoist) {
|
||||||
return this.client.rest.methods.updateGuildRole(this, { hoist });
|
return this.client.rest.methods.updateGuildRole(this, { hoist });
|
||||||
@@ -207,10 +227,10 @@ class Role {
|
|||||||
* // set the position of the role
|
* // set the position of the role
|
||||||
* role.setPosition(1)
|
* role.setPosition(1)
|
||||||
* .then(r => console.log(`Role position: ${r.position}`))
|
* .then(r => console.log(`Role position: ${r.position}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setPosition(position) {
|
setPosition(position) {
|
||||||
return this.client.rest.methods.updateGuildRole(this, { position });
|
return this.guild.setRolePosition(this, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,12 +241,26 @@ class Role {
|
|||||||
* // set the permissions of the role
|
* // set the permissions of the role
|
||||||
* role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS'])
|
* role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS'])
|
||||||
* .then(r => console.log(`Role updated ${r}`))
|
* .then(r => console.log(`Role updated ${r}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setPermissions(permissions) {
|
setPermissions(permissions) {
|
||||||
return this.client.rest.methods.updateGuildRole(this, { permissions });
|
return this.client.rest.methods.updateGuildRole(this, { permissions });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether this role is mentionable
|
||||||
|
* @param {boolean} mentionable Whether this role should be mentionable
|
||||||
|
* @returns {Promise<Role>}
|
||||||
|
* @example
|
||||||
|
* // make the role mentionable
|
||||||
|
* role.setMentionable(true)
|
||||||
|
* .then(r => console.log(`Role updated ${r}`))
|
||||||
|
* .catch(console.error);
|
||||||
|
*/
|
||||||
|
setMentionable(mentionable) {
|
||||||
|
return this.client.rest.methods.updateGuildRole(this, { mentionable });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the role
|
* Deletes the role
|
||||||
* @returns {Promise<Role>}
|
* @returns {Promise<Role>}
|
||||||
@@ -234,7 +268,7 @@ class Role {
|
|||||||
* // delete a role
|
* // delete a role
|
||||||
* role.delete()
|
* role.delete()
|
||||||
* .then(r => console.log(`Deleted role ${r}`))
|
* .then(r => console.log(`Deleted role ${r}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
delete() {
|
delete() {
|
||||||
return this.client.rest.methods.deleteGuildRole(this);
|
return this.client.rest.methods.deleteGuildRole(this);
|
||||||
@@ -265,6 +299,18 @@ class Role {
|
|||||||
toString() {
|
toString() {
|
||||||
return `<@&${this.id}>`;
|
return `<@&${this.id}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the positions of two roles.
|
||||||
|
* @param {Role} role1 First role to compare
|
||||||
|
* @param {Role} role2 Second role to compare
|
||||||
|
* @returns {number} Negative number if the first role's position is lower (second role's is higher),
|
||||||
|
* positive number if the first's is higher (second's is lower), 0 if equal
|
||||||
|
*/
|
||||||
|
static comparePositions(role1, role2) {
|
||||||
|
if (role1.position === role2.position) return role2.id - role1.id;
|
||||||
|
return role1.position - role2.position;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Role;
|
module.exports = Role;
|
||||||
|
|||||||
@@ -42,6 +42,38 @@ class TextChannel extends GuildChannel {
|
|||||||
return members;
|
return members;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all webhooks for the channel.
|
||||||
|
* @returns {Promise<Collection<string, Webhook>>}
|
||||||
|
*/
|
||||||
|
fetchWebhooks() {
|
||||||
|
return this.client.rest.methods.getChannelWebhooks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a webhook for the channel.
|
||||||
|
* @param {string} name The name of the webhook.
|
||||||
|
* @param {FileResolvable} avatar The avatar for the webhook.
|
||||||
|
* @returns {Promise<Webhook>} webhook The created webhook.
|
||||||
|
* @example
|
||||||
|
* channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png')
|
||||||
|
* .then(webhook => console.log(`Created Webhook ${webhook}`))
|
||||||
|
* .catch(console.log)
|
||||||
|
*/
|
||||||
|
createWebhook(name, avatar) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (avatar) {
|
||||||
|
this.client.resolver.resolveFile(avatar).then(file => {
|
||||||
|
let base64 = new Buffer(file, 'binary').toString('base64');
|
||||||
|
let dataURI = `data:;base64,${base64}`;
|
||||||
|
this.client.rest.methods.createWebhook(this, name, dataURI).then(resolve).catch(reject);
|
||||||
|
}).catch(reject);
|
||||||
|
} else {
|
||||||
|
this.client.rest.methods.createWebhook(this, name).then(resolve).catch(reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
||||||
sendMessage() { return; }
|
sendMessage() { return; }
|
||||||
sendTTSMessage() { return; }
|
sendTTSMessage() { return; }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const TextBasedChannel = require('./interface/TextBasedChannel');
|
const TextBasedChannel = require('./interface/TextBasedChannel');
|
||||||
const Constants = require('../util/Constants');
|
const Constants = require('../util/Constants');
|
||||||
|
const Presence = require('./Presence').Presence;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a User on Discord.
|
* Represents a User on Discord.
|
||||||
@@ -47,45 +48,43 @@ class User {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.bot = Boolean(data.bot);
|
this.bot = Boolean(data.bot);
|
||||||
|
|
||||||
/**
|
|
||||||
* The status of the user:
|
|
||||||
*
|
|
||||||
* * **`online`** - user is online
|
|
||||||
* * **`offline`** - user is offline
|
|
||||||
* * **`idle`** - user is AFK
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
this.status = data.status || this.status || 'offline';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents data about a Game
|
|
||||||
* @property {string} name the name of the game being played.
|
|
||||||
* @property {string} [url] the URL of the stream, if the game is being streamed.
|
|
||||||
* @property {number} [type] if being streamed, this is `1`.
|
|
||||||
* @typedef {object} Game
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The game that the user is playing, `null` if they aren't playing a game.
|
|
||||||
* @type {Game}
|
|
||||||
*/
|
|
||||||
this.game = data.game;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
patch(data) {
|
patch(data) {
|
||||||
for (const prop of ['id', 'username', 'discriminator', 'status', 'game', 'avatar', 'bot']) {
|
for (const prop of ['id', 'username', 'discriminator', 'avatar', 'bot']) {
|
||||||
if (typeof data[prop] !== 'undefined') this[prop] = data[prop];
|
if (typeof data[prop] !== 'undefined') this[prop] = data[prop];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time the user was created
|
* The timestamp the user was created at
|
||||||
|
* @type {number}
|
||||||
* @readonly
|
* @readonly
|
||||||
* @type {Date}
|
|
||||||
*/
|
*/
|
||||||
get creationDate() {
|
get createdTimestamp() {
|
||||||
return new Date((this.id / 4194304) + 1420070400000);
|
return (this.id / 4194304) + 1420070400000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time the user was created
|
||||||
|
* @type {Date}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get createdAt() {
|
||||||
|
return new Date(this.createdTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The presence of this user
|
||||||
|
* @type {Presence}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get presence() {
|
||||||
|
if (this.client.presences.has(this.id)) return this.client.presences.get(this.id);
|
||||||
|
for (const guild of this.client.guilds.values()) {
|
||||||
|
if (guild.presences.has(this.id)) return guild.presences.get(this.id);
|
||||||
|
}
|
||||||
|
return new Presence();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,6 +135,46 @@ class User {
|
|||||||
return this.client.rest.methods.deleteChannel(this);
|
return this.client.rest.methods.deleteChannel(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a friend request to the user
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
addFriend() {
|
||||||
|
return this.client.rest.methods.addFriend(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the user from your friends
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
removeFriend() {
|
||||||
|
return this.client.rest.methods.removeFriend(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks the user
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
block() {
|
||||||
|
return this.client.rest.methods.blockUser(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unblocks the user
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
unblock() {
|
||||||
|
return this.client.rest.methods.unblockUser(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the profile of the user
|
||||||
|
* @returns {Promise<UserProfile>}
|
||||||
|
*/
|
||||||
|
fetchProfile() {
|
||||||
|
return this.client.rest.methods.fetchUserProfile(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
|
* Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
|
||||||
* It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
|
* It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
|
||||||
@@ -150,16 +189,6 @@ class User {
|
|||||||
this.avatar === user.avatar &&
|
this.avatar === user.avatar &&
|
||||||
this.bot === Boolean(user.bot);
|
this.bot === Boolean(user.bot);
|
||||||
|
|
||||||
if (equal) {
|
|
||||||
if (user.status) equal = this.status === user.status;
|
|
||||||
if (equal && user.game) {
|
|
||||||
equal = this.game &&
|
|
||||||
this.game.name === user.game.name &&
|
|
||||||
this.game.type === user.game.type &&
|
|
||||||
this.game.url === user.game.url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return equal;
|
return equal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
src/structures/UserConnection.js
Normal file
48
src/structures/UserConnection.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Represents a User Connection object (or "platform identity")
|
||||||
|
*/
|
||||||
|
class UserConnection {
|
||||||
|
constructor(user, data) {
|
||||||
|
/**
|
||||||
|
* The user that owns the Connection
|
||||||
|
* @type {User}
|
||||||
|
*/
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.setup(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(data) {
|
||||||
|
/**
|
||||||
|
* The type of the Connection
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.type = data.type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The username of the connection account
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.name = data.name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the connection account
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.id = data.id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the connection is revoked
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
|
this.revoked = data.revoked;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* an array of partial server integrations (not yet implemented in this lib)
|
||||||
|
* @type {Object[]}
|
||||||
|
*/
|
||||||
|
this.integrations = data.integrations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserConnection;
|
||||||
49
src/structures/UserProfile.js
Normal file
49
src/structures/UserProfile.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const Collection = require('../util/Collection');
|
||||||
|
const UserConnection = require('./UserConnection');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a user's profile on Discord.
|
||||||
|
*/
|
||||||
|
class UserProfile {
|
||||||
|
constructor(user, data) {
|
||||||
|
/**
|
||||||
|
* The owner of the profile
|
||||||
|
* @type {User}
|
||||||
|
*/
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Client that created the instance of the the User.
|
||||||
|
* @type {Client}
|
||||||
|
*/
|
||||||
|
this.client = this.user.client;
|
||||||
|
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guilds that the ClientUser and the User share
|
||||||
|
* @type {Collection<Guild>}
|
||||||
|
*/
|
||||||
|
this.mutualGuilds = new Collection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's connections
|
||||||
|
* @type {Collection<UserConnection>}
|
||||||
|
*/
|
||||||
|
this.connections = new Collection();
|
||||||
|
|
||||||
|
this.setup(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(data) {
|
||||||
|
for (const guild of data.mutual_guilds) {
|
||||||
|
if (this.client.guilds.has(guild.id)) {
|
||||||
|
this.mutualGuilds.set(guild.id, this.client.guilds.get(guild.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const connection of data.connected_accounts) {
|
||||||
|
this.connections.set(connection.id, new UserConnection(this.user, connection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserProfile;
|
||||||
@@ -45,6 +45,22 @@ class VoiceChannel extends GuildChannel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the client has permission join the voice channel
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get joinable() {
|
||||||
|
return this.permissionsFor(this.client.user).hasPermission('CONNECT');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the client has permission to send audio to the voice channel
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get speakable() {
|
||||||
|
return this.permissionsFor(this.client.user).hasPermission('SPEAK');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the bitrate of the channel
|
* Sets the bitrate of the channel
|
||||||
* @param {number} bitrate The new bitrate
|
* @param {number} bitrate The new bitrate
|
||||||
@@ -53,7 +69,7 @@ class VoiceChannel extends GuildChannel {
|
|||||||
* // set the bitrate of a voice channel
|
* // set the bitrate of a voice channel
|
||||||
* voiceChannel.setBitrate(48000)
|
* voiceChannel.setBitrate(48000)
|
||||||
* .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`))
|
* .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setBitrate(bitrate) {
|
setBitrate(bitrate) {
|
||||||
return this.rest.client.rest.methods.updateChannel(this, { bitrate });
|
return this.rest.client.rest.methods.updateChannel(this, { bitrate });
|
||||||
@@ -66,7 +82,7 @@ class VoiceChannel extends GuildChannel {
|
|||||||
* // join a voice channel
|
* // join a voice channel
|
||||||
* voiceChannel.join()
|
* voiceChannel.join()
|
||||||
* .then(connection => console.log('Connected!'))
|
* .then(connection => console.log('Connected!'))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
join() {
|
join() {
|
||||||
return this.client.voice.joinChannel(this);
|
return this.client.voice.joinChannel(this);
|
||||||
|
|||||||
205
src/structures/Webhook.js
Normal file
205
src/structures/Webhook.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const escapeMarkdown = require('../util/EscapeMarkdown');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Webhook
|
||||||
|
*/
|
||||||
|
class Webhook {
|
||||||
|
constructor(client, dataOrID, token) {
|
||||||
|
if (client) {
|
||||||
|
/**
|
||||||
|
* The client that instantiated the Channel
|
||||||
|
* @type {Client}
|
||||||
|
*/
|
||||||
|
this.client = client;
|
||||||
|
Object.defineProperty(this, 'client', { enumerable: false, configurable: false });
|
||||||
|
if (dataOrID) this.setup(dataOrID);
|
||||||
|
} else {
|
||||||
|
this.id = dataOrID;
|
||||||
|
this.token = token;
|
||||||
|
this.client = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(data) {
|
||||||
|
/**
|
||||||
|
* The name of the Webhook
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.name = data.name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The token for the Webhook
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.token = data.token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The avatar for the Webhook
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.avatar = data.avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the Webhook
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.id = data.id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The guild the Webhook belongs to
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.guildID = data.guild_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The channel the Webhook belongs to
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.channelID = data.channel_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The owner of the Webhook
|
||||||
|
* @type {User}
|
||||||
|
*/
|
||||||
|
if (data.user) this.owner = data.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options that can be passed into sendMessage, sendTTSMessage, sendFile, sendCode
|
||||||
|
* @typedef {Object} WebhookMessageOptions
|
||||||
|
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
|
||||||
|
* @property {boolean} [disableEveryone=this.options.disableEveryone] Whether or not @everyone and @here
|
||||||
|
* should be replaced with plain-text
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message with this webhook
|
||||||
|
* @param {StringResolvable} content The content to send.
|
||||||
|
* @param {WebhookMessageOptions} [options={}] The options to provide.
|
||||||
|
* @returns {Promise<Message|Message[]>}
|
||||||
|
* @example
|
||||||
|
* // send a message
|
||||||
|
* webhook.sendMessage('hello!')
|
||||||
|
* .then(message => console.log(`Sent message: ${message.content}`))
|
||||||
|
* .catch(console.error);
|
||||||
|
*/
|
||||||
|
sendMessage(content, options = {}) {
|
||||||
|
return this.client.rest.methods.sendWebhookMessage(this, content, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a raw slack message with this webhook
|
||||||
|
* @param {Object} body The raw body to send.
|
||||||
|
* @returns {Promise}
|
||||||
|
* @example
|
||||||
|
* // send a slack message
|
||||||
|
* webhook.sendSlackMessage({
|
||||||
|
* 'username': 'Wumpus',
|
||||||
|
* 'attachments': [{
|
||||||
|
* 'pretext': 'this looks pretty cool',
|
||||||
|
* 'color': '#F0F',
|
||||||
|
* 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png',
|
||||||
|
* 'footer': 'Powered by sneks',
|
||||||
|
* 'ts': new Date().getTime() / 1000
|
||||||
|
* }]
|
||||||
|
* }).catch(console.log);
|
||||||
|
*/
|
||||||
|
sendSlackMessage(body) {
|
||||||
|
return this.client.rest.methods.sendSlackWebhookMessage(this, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a text-to-speech message with this webhook
|
||||||
|
* @param {StringResolvable} content The content to send
|
||||||
|
* @param {WebhookMessageOptions} [options={}] The options to provide
|
||||||
|
* @returns {Promise<Message|Message[]>}
|
||||||
|
* @example
|
||||||
|
* // send a TTS message
|
||||||
|
* webhook.sendTTSMessage('hello!')
|
||||||
|
* .then(message => console.log(`Sent tts message: ${message.content}`))
|
||||||
|
* .catch(console.error);
|
||||||
|
*/
|
||||||
|
sendTTSMessage(content, options = {}) {
|
||||||
|
Object.assign(options, { tts: true });
|
||||||
|
return this.client.rest.methods.sendWebhookMessage(this, content, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a file with this webhook
|
||||||
|
* @param {FileResolvable} attachment The file to send
|
||||||
|
* @param {string} [fileName="file.jpg"] The name and extension of the file
|
||||||
|
* @param {StringResolvable} [content] Text message to send with the attachment
|
||||||
|
* @param {WebhookMessageOptions} [options] The options to provide
|
||||||
|
* @returns {Promise<Message>}
|
||||||
|
*/
|
||||||
|
sendFile(attachment, fileName, content, options = {}) {
|
||||||
|
if (!fileName) {
|
||||||
|
if (typeof attachment === 'string') {
|
||||||
|
fileName = path.basename(attachment);
|
||||||
|
} else if (attachment && attachment.path) {
|
||||||
|
fileName = path.basename(attachment.path);
|
||||||
|
} else {
|
||||||
|
fileName = 'file.jpg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.client.resolver.resolveFile(attachment).then(file => {
|
||||||
|
this.client.rest.methods.sendWebhookMessage(this, content, options, {
|
||||||
|
file,
|
||||||
|
name: fileName,
|
||||||
|
}).then(resolve).catch(reject);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a code block with this webhook
|
||||||
|
* @param {string} lang Language for the code block
|
||||||
|
* @param {StringResolvable} content Content of the code block
|
||||||
|
* @param {WebhookMessageOptions} options The options to provide
|
||||||
|
* @returns {Promise<Message|Message[]>}
|
||||||
|
*/
|
||||||
|
sendCode(lang, content, options = {}) {
|
||||||
|
if (options.split) {
|
||||||
|
if (typeof options.split !== 'object') options.split = {};
|
||||||
|
if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`;
|
||||||
|
if (!options.split.append) options.split.append = '\n```';
|
||||||
|
}
|
||||||
|
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||||
|
return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit the Webhook.
|
||||||
|
* @param {string} name The new name for the Webhook
|
||||||
|
* @param {FileResolvable} avatar The new avatar for the Webhook.
|
||||||
|
* @returns {Promise<Webhook>}
|
||||||
|
*/
|
||||||
|
edit(name = this.name, avatar) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (avatar) {
|
||||||
|
this.client.resolver.resolveFile(avatar).then(file => {
|
||||||
|
const dataURI = this.client.resolver.resolveBase64(file);
|
||||||
|
this.client.rest.methods.editWebhook(this, name, dataURI)
|
||||||
|
.then(resolve).catch(reject);
|
||||||
|
}).catch(reject);
|
||||||
|
} else {
|
||||||
|
this.client.rest.methods.editWebhook(this, name)
|
||||||
|
.then(data => {
|
||||||
|
this.setup(data);
|
||||||
|
}).catch(reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the Webhook
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
delete() {
|
||||||
|
return this.client.rest.methods.deleteWebhook(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Webhook;
|
||||||
@@ -2,6 +2,7 @@ const path = require('path');
|
|||||||
const Message = require('../Message');
|
const Message = require('../Message');
|
||||||
const MessageCollector = require('../MessageCollector');
|
const MessageCollector = require('../MessageCollector');
|
||||||
const Collection = require('../../util/Collection');
|
const Collection = require('../../util/Collection');
|
||||||
|
const escapeMarkdown = require('../../util/EscapeMarkdown');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for classes that have text-channel-like features
|
* Interface for classes that have text-channel-like features
|
||||||
@@ -27,7 +28,7 @@ class TextBasedChannel {
|
|||||||
* @typedef {Object} MessageOptions
|
* @typedef {Object} MessageOptions
|
||||||
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
|
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
|
||||||
* @property {string} [nonce=''] The nonce for the message
|
* @property {string} [nonce=''] The nonce for the message
|
||||||
* @property {boolean} [disable_everyone=this.client.options.disable_everyone] Whether or not @everyone and @here
|
* @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here
|
||||||
* should be replaced with plain-text
|
* should be replaced with plain-text
|
||||||
* @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if
|
* @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if
|
||||||
* it exceeds the character limit. If an object is provided, these are the options for splitting the message.
|
* it exceeds the character limit. If an object is provided, these are the options for splitting the message.
|
||||||
@@ -38,8 +39,8 @@ class TextBasedChannel {
|
|||||||
* @typedef {Object} SplitOptions
|
* @typedef {Object} SplitOptions
|
||||||
* @property {number} [maxLength=1950] Maximum character length per message piece
|
* @property {number} [maxLength=1950] Maximum character length per message piece
|
||||||
* @property {string} [char='\n'] Character to split the message with
|
* @property {string} [char='\n'] Character to split the message with
|
||||||
* @property {string} [prepend=''] Text to prepend to each middle piece
|
* @property {string} [prepend=''] Text to prepend to every piece except the first
|
||||||
* @property {string} [append=''] Text to append to each middle piece
|
* @property {string} [append=''] Text to append to every piece except the last
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +52,7 @@ class TextBasedChannel {
|
|||||||
* // send a message
|
* // send a message
|
||||||
* channel.sendMessage('hello!')
|
* channel.sendMessage('hello!')
|
||||||
* .then(message => console.log(`Sent message: ${message.content}`))
|
* .then(message => console.log(`Sent message: ${message.content}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
sendMessage(content, options = {}) {
|
sendMessage(content, options = {}) {
|
||||||
return this.client.rest.methods.sendMessage(this, content, options);
|
return this.client.rest.methods.sendMessage(this, content, options);
|
||||||
@@ -66,7 +67,7 @@ class TextBasedChannel {
|
|||||||
* // send a TTS message
|
* // send a TTS message
|
||||||
* channel.sendTTSMessage('hello!')
|
* channel.sendTTSMessage('hello!')
|
||||||
* .then(message => console.log(`Sent tts message: ${message.content}`))
|
* .then(message => console.log(`Sent tts message: ${message.content}`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
sendTTSMessage(content, options = {}) {
|
sendTTSMessage(content, options = {}) {
|
||||||
Object.assign(options, { tts: true });
|
Object.assign(options, { tts: true });
|
||||||
@@ -111,15 +112,16 @@ class TextBasedChannel {
|
|||||||
sendCode(lang, content, options = {}) {
|
sendCode(lang, content, options = {}) {
|
||||||
if (options.split) {
|
if (options.split) {
|
||||||
if (typeof options.split !== 'object') options.split = {};
|
if (typeof options.split !== 'object') options.split = {};
|
||||||
if (!options.split.prepend) options.split.prepend = `\`\`\`${lang ? lang : ''}\n`;
|
if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`;
|
||||||
if (!options.split.append) options.split.append = '\n```';
|
if (!options.split.append) options.split.append = '\n```';
|
||||||
}
|
}
|
||||||
content = this.client.resolver.resolveString(content).replace(/```/g, '`\u200b``');
|
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||||
return this.sendMessage(`\`\`\`${lang ? lang : ''}\n${content}\n\`\`\``, options);
|
return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a single message from this channel, regardless of it being cached or not.
|
* Gets a single message from this channel, regardless of it being cached or not.
|
||||||
|
* <warn>Only OAuth bot accounts can use this method.</warn>
|
||||||
* @param {string} messageID The ID of the message to get
|
* @param {string} messageID The ID of the message to get
|
||||||
* @returns {Promise<Message>}
|
* @returns {Promise<Message>}
|
||||||
* @example
|
* @example
|
||||||
@@ -158,7 +160,7 @@ class TextBasedChannel {
|
|||||||
* // get messages
|
* // get messages
|
||||||
* channel.fetchMessages({limit: 10})
|
* channel.fetchMessages({limit: 10})
|
||||||
* .then(messages => console.log(`Received ${messages.size} messages`))
|
* .then(messages => console.log(`Received ${messages.size} messages`))
|
||||||
* .catch(console.log);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
fetchMessages(options = {}) {
|
fetchMessages(options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -241,6 +243,7 @@ class TextBasedChannel {
|
|||||||
/**
|
/**
|
||||||
* Whether or not the typing indicator is being shown in the channel.
|
* Whether or not the typing indicator is being shown in the channel.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get typing() {
|
get typing() {
|
||||||
return this.client.user._typing.has(this.id);
|
return this.client.user._typing.has(this.id);
|
||||||
@@ -249,6 +252,7 @@ class TextBasedChannel {
|
|||||||
/**
|
/**
|
||||||
* Number of times `startTyping` has been called.
|
* Number of times `startTyping` has been called.
|
||||||
* @type {number}
|
* @type {number}
|
||||||
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get typingCount() {
|
get typingCount() {
|
||||||
if (this.client.user._typing.has(this.id)) return this.client.user._typing.get(this.id).count;
|
if (this.client.user._typing.has(this.id)) return this.client.user._typing.get(this.id).count;
|
||||||
@@ -307,23 +311,28 @@ class TextBasedChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk delete a given Collection or Array of messages in one go. Returns the deleted messages after.
|
* Bulk delete given messages.
|
||||||
* Only OAuth Bot accounts may use this method.
|
* Only OAuth Bot accounts may use this method.
|
||||||
* @param {Collection<string, Message>|Message[]} messages The messages to delete
|
* @param {Collection<string, Message>|Message[]|number} messages Messages to delete, or number of messages to delete
|
||||||
* @returns {Collection<string, Message>}
|
* @returns {Promise<Collection<string, Message>>} Deleted messages
|
||||||
*/
|
*/
|
||||||
bulkDelete(messages) {
|
bulkDelete(messages) {
|
||||||
if (messages instanceof Collection) messages = messages.array();
|
return new Promise((resolve, reject) => {
|
||||||
if (!(messages instanceof Array)) return Promise.reject(new TypeError('Messages must be an Array or Collection.'));
|
if (!isNaN(messages)) {
|
||||||
const messageIDs = messages.map(m => m.id);
|
this.fetchMessages({ limit: messages }).then(msgs => resolve(this.bulkDelete(msgs)));
|
||||||
return this.client.rest.methods.bulkDeleteMessages(this, messageIDs);
|
} else if (messages instanceof Array || messages instanceof Collection) {
|
||||||
|
const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id);
|
||||||
|
resolve(this.client.rest.methods.bulkDeleteMessages(this, messageIDs));
|
||||||
|
} else {
|
||||||
|
reject(new TypeError('Messages must be an Array, Collection, or number.'));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_cacheMessage(message) {
|
_cacheMessage(message) {
|
||||||
const maxSize = this.client.options.max_message_cache;
|
const maxSize = this.client.options.messageCacheMaxSize;
|
||||||
if (maxSize === 0) return null;
|
if (maxSize === 0) return null;
|
||||||
if (this.messages.size >= maxSize) this.messages.delete(this.messages.keys().next().value);
|
if (this.messages.size >= maxSize && maxSize > 0) this.messages.delete(this.messages.firstKey());
|
||||||
|
|
||||||
this.messages.set(message.id, message);
|
this.messages.set(message.id, message);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,48 @@
|
|||||||
* @extends {Map}
|
* @extends {Map}
|
||||||
*/
|
*/
|
||||||
class Collection extends Map {
|
class Collection extends Map {
|
||||||
|
constructor(iterable) {
|
||||||
|
super(iterable);
|
||||||
|
this._array = null;
|
||||||
|
this._keyArray = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, val) {
|
||||||
|
super.set(key, val);
|
||||||
|
this._array = null;
|
||||||
|
this._keyArray = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key) {
|
||||||
|
super.delete(key);
|
||||||
|
this._array = null;
|
||||||
|
this._keyArray = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an ordered array of the values of this collection.
|
* Creates an ordered array of the values of this collection, and caches it internally. The array will only be
|
||||||
|
* reconstructed if an item is added to or removed from the collection, or if you add/remove elements on the array.
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
* @example
|
* @example
|
||||||
* // identical to:
|
* // identical to:
|
||||||
* Array.from(collection.values());
|
* Array.from(collection.values());
|
||||||
*/
|
*/
|
||||||
array() {
|
array() {
|
||||||
return Array.from(this.values());
|
if (!this._array || this._array.length !== this.size) this._array = Array.from(this.values());
|
||||||
|
return this._array;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an ordered array of the keys of this collection.
|
* Creates an ordered array of the keys of this collection, and caches it internally. The array will only be
|
||||||
|
* reconstructed if an item is added to or removed from the collection, or if you add/remove elements on the array.
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
* @example
|
* @example
|
||||||
* // identical to:
|
* // identical to:
|
||||||
* Array.from(collection.keys());
|
* Array.from(collection.keys());
|
||||||
*/
|
*/
|
||||||
keyArray() {
|
keyArray() {
|
||||||
return Array.from(this.keys());
|
if (!this._keyArray || this._keyArray.length !== this.size) this._keyArray = Array.from(this.keys());
|
||||||
|
return this._keyArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,11 +205,27 @@ class Collection extends Map {
|
|||||||
*/
|
*/
|
||||||
filter(fn, thisArg) {
|
filter(fn, thisArg) {
|
||||||
if (thisArg) fn = fn.bind(thisArg);
|
if (thisArg) fn = fn.bind(thisArg);
|
||||||
const collection = new Collection();
|
const results = new Collection();
|
||||||
for (const [key, val] of this) {
|
for (const [key, val] of this) {
|
||||||
if (fn(val, key, this)) collection.set(key, val);
|
if (fn(val, key, this)) results.set(key, val);
|
||||||
}
|
}
|
||||||
return collection;
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identical to
|
||||||
|
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).
|
||||||
|
* @param {function} fn Function used to test (should return a boolean)
|
||||||
|
* @param {Object} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Collection}
|
||||||
|
*/
|
||||||
|
filterArray(fn, thisArg) {
|
||||||
|
if (thisArg) fn = fn.bind(thisArg);
|
||||||
|
const results = [];
|
||||||
|
for (const [key, val] of this) {
|
||||||
|
if (fn(val, key, this)) results.push(val);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,6 +286,21 @@ class Collection extends Map {
|
|||||||
return currentVal;
|
return currentVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines this collection with others into a new collection. None of the source collections are modified.
|
||||||
|
* @param {Collection} collections Collections to merge
|
||||||
|
* @returns {Collection}
|
||||||
|
* @example const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl);
|
||||||
|
*/
|
||||||
|
concat(...collections) {
|
||||||
|
const newColl = new this.constructor();
|
||||||
|
for (const [key, val] of this) newColl.set(key, val);
|
||||||
|
for (const coll of collections) {
|
||||||
|
for (const [key, val] of coll) newColl.set(key, val);
|
||||||
|
}
|
||||||
|
return newColl;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the items in this collection have a delete method (e.g. messages), invoke
|
* If the items in this collection have a delete method (e.g. messages), invoke
|
||||||
* the delete method. Returns an array of promises
|
* the delete method. Returns an array of promises
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
|
exports.Package = require('../../package.json');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for a Client.
|
* Options for a Client.
|
||||||
* @typedef {Object} ClientOptions
|
* @typedef {Object} ClientOptions
|
||||||
* @property {string} [api_request_method='sequential'] 'sequential' or 'burst'. Sequential executes all requests in
|
* @property {string} [apiRequestMethod='sequential'] 'sequential' or 'burst'. Sequential executes all requests in
|
||||||
* the order they are triggered, whereas burst runs multiple at a time, and doesn't guarantee a particular order.
|
* the order they are triggered, whereas burst runs multiple at a time, and doesn't guarantee a particular order.
|
||||||
* @property {number} [shard_id=0] The ID of this shard
|
* @property {number} [shardId=0] The ID of this shard
|
||||||
* @property {number} [shard_count=0] The number of shards
|
* @property {number} [shardCount=0] The number of shards
|
||||||
* @property {number} [max_message_cache=200] Number of messages to cache per channel
|
* @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel
|
||||||
* @property {number} [message_cache_lifetime=0] How long until a message should be uncached by the message sweeping
|
* @property {boolean} [sync=false] Whether to periodically sync guilds
|
||||||
|
* (-1 for unlimited - don't do this without message sweeping, otherwise memory usage will climb indefinitely)
|
||||||
|
* @property {number} [messageCacheLifetime=0] How long until a message should be uncached by the message sweeping
|
||||||
* (in seconds, 0 for forever)
|
* (in seconds, 0 for forever)
|
||||||
* @property {number} [message_sweep_interval=0] How frequently to remove messages from the cache that are older than
|
* @property {number} [messageSweepInterval=0] How frequently to remove messages from the cache that are older than
|
||||||
* the max message lifetime (in seconds, 0 for never)
|
* the message cache lifetime (in seconds, 0 for never)
|
||||||
* @property {boolean} [fetch_all_members=false] Whether to cache all guild members and users upon startup
|
* @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as
|
||||||
* @property {boolean} [disable_everyone=false] Default value for MessageOptions.disable_everyone
|
* upon joining a guild
|
||||||
* @property {number} [rest_ws_bridge_timeout=5000] Maximum time permitted between REST responses and their
|
* @property {boolean} [disableEveryone=false] Default value for MessageOptions.disableEveryone
|
||||||
|
* @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their
|
||||||
* corresponding websocket events
|
* corresponding websocket events
|
||||||
|
* @property {string[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be
|
||||||
|
* processed. Disabling useless events such as 'TYPING_START' can result in significant performance increases on
|
||||||
|
* large-scale bots.
|
||||||
* @property {WebsocketOptions} [ws] Options for the websocket
|
* @property {WebsocketOptions} [ws] Options for the websocket
|
||||||
*/
|
*/
|
||||||
exports.DefaultOptions = {
|
exports.DefaultOptions = {
|
||||||
api_request_method: 'sequential',
|
apiRequestMethod: 'sequential',
|
||||||
shard_id: 0,
|
shardId: 0,
|
||||||
shard_count: 0,
|
shardCount: 0,
|
||||||
max_message_cache: 200,
|
messageCacheMaxSize: 200,
|
||||||
message_cache_lifetime: 0,
|
messageCacheLifetime: 0,
|
||||||
message_sweep_interval: 0,
|
messageSweepInterval: 0,
|
||||||
fetch_all_members: false,
|
fetchAllMembers: false,
|
||||||
disable_everyone: false,
|
disableEveryone: false,
|
||||||
rest_ws_bridge_timeout: 5000,
|
restWsBridgeTimeout: 5000,
|
||||||
protocol_version: 6,
|
disabledEvents: [],
|
||||||
|
sync: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Websocket options.
|
* Websocket options. These are left as snake_case to match the API.
|
||||||
* @typedef {Object} WebsocketOptions
|
* @typedef {Object} WebsocketOptions
|
||||||
* @property {number} [large_threshold=250] Number of members in a guild to be considered large
|
* @property {number} [large_threshold=250] Number of members in a guild to be considered large
|
||||||
* @property {boolean} [compress=true] Whether to compress data sent on the connection
|
* @property {boolean} [compress=true] Whether to compress data sent on the connection
|
||||||
@@ -47,23 +56,6 @@ exports.DefaultOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Status = {
|
|
||||||
READY: 0,
|
|
||||||
CONNECTING: 1,
|
|
||||||
RECONNECTING: 2,
|
|
||||||
IDLE: 3,
|
|
||||||
NEARLY: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.ChannelTypes = {
|
|
||||||
text: 0,
|
|
||||||
DM: 1,
|
|
||||||
voice: 2,
|
|
||||||
groupDM: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Package = require('../../package.json');
|
|
||||||
|
|
||||||
exports.Errors = {
|
exports.Errors = {
|
||||||
NO_TOKEN: 'Request to use token, but token was unavailable to the client.',
|
NO_TOKEN: 'Request to use token, but token was unavailable to the client.',
|
||||||
NO_BOT_ACCOUNT: 'You ideally should be using a bot account!',
|
NO_BOT_ACCOUNT: 'You ideally should be using a bot account!',
|
||||||
@@ -72,16 +64,17 @@ exports.Errors = {
|
|||||||
NOT_A_PERMISSION: 'Invalid permission string or number.',
|
NOT_A_PERMISSION: 'Invalid permission string or number.',
|
||||||
INVALID_RATE_LIMIT_METHOD: 'Unknown rate limiting method.',
|
INVALID_RATE_LIMIT_METHOD: 'Unknown rate limiting method.',
|
||||||
BAD_LOGIN: 'Incorrect login details were provided.',
|
BAD_LOGIN: 'Incorrect login details were provided.',
|
||||||
INVALID_SHARD: 'Invalid shard settings were provided',
|
INVALID_SHARD: 'Invalid shard settings were provided.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const API = `https://discordapp.com/api/v${exports.DefaultOptions.protocol_version}`;
|
const PROTOCOL_VERSION = exports.PROTOCOL_VERSION = 6;
|
||||||
|
const API = exports.API = `https://discordapp.com/api/v${PROTOCOL_VERSION}`;
|
||||||
const Endpoints = exports.Endpoints = {
|
const Endpoints = exports.Endpoints = {
|
||||||
// general endpoints
|
// general
|
||||||
login: `${API}/auth/login`,
|
login: `${API}/auth/login`,
|
||||||
logout: `${API}/auth/logout`,
|
logout: `${API}/auth/logout`,
|
||||||
gateway: `${API}/gateway`,
|
gateway: `${API}/gateway`,
|
||||||
|
botGateway: `${API}/gateway/bot`,
|
||||||
invite: (id) => `${API}/invite/${id}`,
|
invite: (id) => `${API}/invite/${id}`,
|
||||||
inviteLink: (id) => `https://discord.gg/${id}`,
|
inviteLink: (id) => `https://discord.gg/${id}`,
|
||||||
CDN: 'https://cdn.discordapp.com',
|
CDN: 'https://cdn.discordapp.com',
|
||||||
@@ -89,9 +82,11 @@ const Endpoints = exports.Endpoints = {
|
|||||||
// users
|
// users
|
||||||
user: (userID) => `${API}/users/${userID}`,
|
user: (userID) => `${API}/users/${userID}`,
|
||||||
userChannels: (userID) => `${Endpoints.user(userID)}/channels`,
|
userChannels: (userID) => `${Endpoints.user(userID)}/channels`,
|
||||||
|
userProfile: (userID) => `${Endpoints.user(userID)}/profile`,
|
||||||
avatar: (userID, avatar) => userID === '1' ? avatar : `${Endpoints.user(userID)}/avatars/${avatar}.jpg`,
|
avatar: (userID, avatar) => userID === '1' ? avatar : `${Endpoints.user(userID)}/avatars/${avatar}.jpg`,
|
||||||
me: `${API}/users/@me`,
|
me: `${API}/users/@me`,
|
||||||
meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`,
|
meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`,
|
||||||
|
relationships: (userID) => `${Endpoints.user(userID)}/relationships`,
|
||||||
|
|
||||||
// guilds
|
// guilds
|
||||||
guilds: `${API}/guilds`,
|
guilds: `${API}/guilds`,
|
||||||
@@ -108,6 +103,7 @@ const Endpoints = exports.Endpoints = {
|
|||||||
guildMember: (guildID, memberID) => `${Endpoints.guildMembers(guildID)}/${memberID}`,
|
guildMember: (guildID, memberID) => `${Endpoints.guildMembers(guildID)}/${memberID}`,
|
||||||
stupidInconsistentGuildEndpoint: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`,
|
stupidInconsistentGuildEndpoint: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`,
|
||||||
guildChannels: (guildID) => `${Endpoints.guild(guildID)}/channels`,
|
guildChannels: (guildID) => `${Endpoints.guild(guildID)}/channels`,
|
||||||
|
guildEmojis: (guildID) => `${Endpoints.guild(guildID)}/emojis`,
|
||||||
|
|
||||||
// channels
|
// channels
|
||||||
channels: `${API}/channels`,
|
channels: `${API}/channels`,
|
||||||
@@ -117,6 +113,25 @@ const Endpoints = exports.Endpoints = {
|
|||||||
channelTyping: (channelID) => `${Endpoints.channel(channelID)}/typing`,
|
channelTyping: (channelID) => `${Endpoints.channel(channelID)}/typing`,
|
||||||
channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`,
|
channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`,
|
||||||
channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`,
|
channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`,
|
||||||
|
channelWebhooks: (channelID) => `${Endpoints.channel(channelID)}/webhooks`,
|
||||||
|
|
||||||
|
// webhooks
|
||||||
|
webhook: (webhookID, token) => `${API}/webhooks/${webhookID}${token ? `/${token}` : ''}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Status = {
|
||||||
|
READY: 0,
|
||||||
|
CONNECTING: 1,
|
||||||
|
RECONNECTING: 2,
|
||||||
|
IDLE: 3,
|
||||||
|
NEARLY: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.ChannelTypes = {
|
||||||
|
text: 0,
|
||||||
|
DM: 1,
|
||||||
|
voice: 2,
|
||||||
|
groupDM: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.OPCodes = {
|
exports.OPCodes = {
|
||||||
@@ -130,6 +145,8 @@ exports.OPCodes = {
|
|||||||
RECONNECT: 7,
|
RECONNECT: 7,
|
||||||
REQUEST_GUILD_MEMBERS: 8,
|
REQUEST_GUILD_MEMBERS: 8,
|
||||||
INVALID_SESSION: 9,
|
INVALID_SESSION: 9,
|
||||||
|
HELLO: 10,
|
||||||
|
HEARTBEAT_ACK: 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.VoiceOPCodes = {
|
exports.VoiceOPCodes = {
|
||||||
@@ -145,52 +162,49 @@ exports.Events = {
|
|||||||
READY: 'ready',
|
READY: 'ready',
|
||||||
GUILD_CREATE: 'guildCreate',
|
GUILD_CREATE: 'guildCreate',
|
||||||
GUILD_DELETE: 'guildDelete',
|
GUILD_DELETE: 'guildDelete',
|
||||||
|
GUILD_UPDATE: 'guildUpdate',
|
||||||
GUILD_UNAVAILABLE: 'guildUnavailable',
|
GUILD_UNAVAILABLE: 'guildUnavailable',
|
||||||
GUILD_AVAILABLE: 'guildAvailable',
|
GUILD_AVAILABLE: 'guildAvailable',
|
||||||
GUILD_UPDATE: 'guildUpdate',
|
|
||||||
GUILD_BAN_ADD: 'guildBanAdd',
|
|
||||||
GUILD_BAN_REMOVE: 'guildBanRemove',
|
|
||||||
GUILD_MEMBER_ADD: 'guildMemberAdd',
|
GUILD_MEMBER_ADD: 'guildMemberAdd',
|
||||||
GUILD_MEMBER_REMOVE: 'guildMemberRemove',
|
GUILD_MEMBER_REMOVE: 'guildMemberRemove',
|
||||||
GUILD_MEMBER_UPDATE: 'guildMemberUpdate',
|
GUILD_MEMBER_UPDATE: 'guildMemberUpdate',
|
||||||
GUILD_ROLE_CREATE: 'guildRoleCreate',
|
|
||||||
GUILD_ROLE_DELETE: 'guildRoleDelete',
|
|
||||||
GUILD_ROLE_UPDATE: 'guildRoleUpdate',
|
|
||||||
GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable',
|
GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable',
|
||||||
|
GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking',
|
||||||
|
GUILD_MEMBERS_CHUNK: 'guildMembersChunk',
|
||||||
|
GUILD_ROLE_CREATE: 'roleCreate',
|
||||||
|
GUILD_ROLE_DELETE: 'roleDelete',
|
||||||
|
GUILD_ROLE_UPDATE: 'roleUpdate',
|
||||||
|
GUILD_EMOJI_CREATE: 'guildEmojiCreate',
|
||||||
|
GUILD_EMOJI_DELETE: 'guildEmojiDelete',
|
||||||
|
GUILD_EMOJI_UPDATE: 'guildEmojiUpdate',
|
||||||
|
GUILD_BAN_ADD: 'guildBanAdd',
|
||||||
|
GUILD_BAN_REMOVE: 'guildBanRemove',
|
||||||
CHANNEL_CREATE: 'channelCreate',
|
CHANNEL_CREATE: 'channelCreate',
|
||||||
CHANNEL_DELETE: 'channelDelete',
|
CHANNEL_DELETE: 'channelDelete',
|
||||||
CHANNEL_UPDATE: 'channelUpdate',
|
CHANNEL_UPDATE: 'channelUpdate',
|
||||||
PRESENCE_UPDATE: 'presenceUpdate',
|
CHANNEL_PINS_UPDATE: 'channelPinsUpdate',
|
||||||
USER_UPDATE: 'userUpdate',
|
|
||||||
VOICE_STATE_UPDATE: 'voiceStateUpdate',
|
|
||||||
TYPING_START: 'typingStart',
|
|
||||||
TYPING_STOP: 'typingStop',
|
|
||||||
WARN: 'warn',
|
|
||||||
GUILD_MEMBERS_CHUNK: 'guildMembersChunk',
|
|
||||||
MESSAGE_CREATE: 'message',
|
MESSAGE_CREATE: 'message',
|
||||||
MESSAGE_DELETE: 'messageDelete',
|
MESSAGE_DELETE: 'messageDelete',
|
||||||
MESSAGE_UPDATE: 'messageUpdate',
|
MESSAGE_UPDATE: 'messageUpdate',
|
||||||
|
MESSAGE_BULK_DELETE: 'messageDeleteBulk',
|
||||||
|
USER_UPDATE: 'userUpdate',
|
||||||
|
PRESENCE_UPDATE: 'presenceUpdate',
|
||||||
|
VOICE_STATE_UPDATE: 'voiceStateUpdate',
|
||||||
|
TYPING_START: 'typingStart',
|
||||||
|
TYPING_STOP: 'typingStop',
|
||||||
DISCONNECT: 'disconnect',
|
DISCONNECT: 'disconnect',
|
||||||
RECONNECTING: 'reconnecting',
|
RECONNECTING: 'reconnecting',
|
||||||
GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking',
|
ERROR: 'error',
|
||||||
MESSAGE_BULK_DELETE: 'messageDeleteBulk',
|
WARN: 'warn',
|
||||||
CHANNEL_PINS_UPDATE: 'channelPinsUpdate',
|
|
||||||
DEBUG: 'debug',
|
DEBUG: 'debug',
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.WSEvents = {
|
exports.WSEvents = {
|
||||||
CHANNEL_CREATE: 'CHANNEL_CREATE',
|
|
||||||
CHANNEL_DELETE: 'CHANNEL_DELETE',
|
|
||||||
CHANNEL_UPDATE: 'CHANNEL_UPDATE',
|
|
||||||
MESSAGE_CREATE: 'MESSAGE_CREATE',
|
|
||||||
MESSAGE_DELETE: 'MESSAGE_DELETE',
|
|
||||||
MESSAGE_UPDATE: 'MESSAGE_UPDATE',
|
|
||||||
PRESENCE_UPDATE: 'PRESENCE_UPDATE',
|
|
||||||
READY: 'READY',
|
READY: 'READY',
|
||||||
GUILD_BAN_ADD: 'GUILD_BAN_ADD',
|
GUILD_SYNC: 'GUILD_SYNC',
|
||||||
GUILD_BAN_REMOVE: 'GUILD_BAN_REMOVE',
|
|
||||||
GUILD_CREATE: 'GUILD_CREATE',
|
GUILD_CREATE: 'GUILD_CREATE',
|
||||||
GUILD_DELETE: 'GUILD_DELETE',
|
GUILD_DELETE: 'GUILD_DELETE',
|
||||||
|
GUILD_UPDATE: 'GUILD_UPDATE',
|
||||||
GUILD_MEMBER_ADD: 'GUILD_MEMBER_ADD',
|
GUILD_MEMBER_ADD: 'GUILD_MEMBER_ADD',
|
||||||
GUILD_MEMBER_REMOVE: 'GUILD_MEMBER_REMOVE',
|
GUILD_MEMBER_REMOVE: 'GUILD_MEMBER_REMOVE',
|
||||||
GUILD_MEMBER_UPDATE: 'GUILD_MEMBER_UPDATE',
|
GUILD_MEMBER_UPDATE: 'GUILD_MEMBER_UPDATE',
|
||||||
@@ -198,16 +212,35 @@ exports.WSEvents = {
|
|||||||
GUILD_ROLE_CREATE: 'GUILD_ROLE_CREATE',
|
GUILD_ROLE_CREATE: 'GUILD_ROLE_CREATE',
|
||||||
GUILD_ROLE_DELETE: 'GUILD_ROLE_DELETE',
|
GUILD_ROLE_DELETE: 'GUILD_ROLE_DELETE',
|
||||||
GUILD_ROLE_UPDATE: 'GUILD_ROLE_UPDATE',
|
GUILD_ROLE_UPDATE: 'GUILD_ROLE_UPDATE',
|
||||||
GUILD_UPDATE: 'GUILD_UPDATE',
|
GUILD_BAN_ADD: 'GUILD_BAN_ADD',
|
||||||
TYPING_START: 'TYPING_START',
|
GUILD_BAN_REMOVE: 'GUILD_BAN_REMOVE',
|
||||||
|
CHANNEL_CREATE: 'CHANNEL_CREATE',
|
||||||
|
CHANNEL_DELETE: 'CHANNEL_DELETE',
|
||||||
|
CHANNEL_UPDATE: 'CHANNEL_UPDATE',
|
||||||
|
CHANNEL_PINS_UPDATE: 'CHANNEL_PINS_UPDATE',
|
||||||
|
MESSAGE_CREATE: 'MESSAGE_CREATE',
|
||||||
|
MESSAGE_DELETE: 'MESSAGE_DELETE',
|
||||||
|
MESSAGE_UPDATE: 'MESSAGE_UPDATE',
|
||||||
|
MESSAGE_DELETE_BULK: 'MESSAGE_DELETE_BULK',
|
||||||
USER_UPDATE: 'USER_UPDATE',
|
USER_UPDATE: 'USER_UPDATE',
|
||||||
|
PRESENCE_UPDATE: 'PRESENCE_UPDATE',
|
||||||
VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE',
|
VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE',
|
||||||
|
TYPING_START: 'TYPING_START',
|
||||||
FRIEND_ADD: 'RELATIONSHIP_ADD',
|
FRIEND_ADD: 'RELATIONSHIP_ADD',
|
||||||
FRIEND_REMOVE: 'RELATIONSHIP_REMOVE',
|
FRIEND_REMOVE: 'RELATIONSHIP_REMOVE',
|
||||||
VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE',
|
VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE',
|
||||||
MESSAGE_DELETE_BULK: 'MESSAGE_DELETE_BULK',
|
RELATIONSHIP_ADD: 'RELATIONSHIP_ADD',
|
||||||
CHANNEL_PINS_UPDATE: 'CHANNEL_PINS_UPDATE',
|
RELATIONSHIP_REMOVE: 'RELATIONSHIP_REMOVE',
|
||||||
GUILD_SYNC: 'GUILD_SYNC',
|
};
|
||||||
|
|
||||||
|
exports.MessageTypes = {
|
||||||
|
0: 'DEFAULT',
|
||||||
|
1: 'RECIPIENT_ADD',
|
||||||
|
2: 'RECIPIENT_REMOVE',
|
||||||
|
3: 'CALL',
|
||||||
|
4: 'CHANNEL_NAME_CHANGE',
|
||||||
|
5: 'CHANNEL_ICON_CHANGE',
|
||||||
|
6: 'PINS_ADD',
|
||||||
};
|
};
|
||||||
|
|
||||||
const PermissionFlags = exports.PermissionFlags = {
|
const PermissionFlags = exports.PermissionFlags = {
|
||||||
@@ -238,11 +271,11 @@ const PermissionFlags = exports.PermissionFlags = {
|
|||||||
CHANGE_NICKNAME: 1 << 26,
|
CHANGE_NICKNAME: 1 << 26,
|
||||||
MANAGE_NICKNAMES: 1 << 27,
|
MANAGE_NICKNAMES: 1 << 27,
|
||||||
MANAGE_ROLES_OR_PERMISSIONS: 1 << 28,
|
MANAGE_ROLES_OR_PERMISSIONS: 1 << 28,
|
||||||
|
MANAGE_WEBHOOKS: 1 << 29,
|
||||||
|
MANAGE_EMOJIS: 1 << 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ALL_PERMISSIONS = 0;
|
let _ALL_PERMISSIONS = 0;
|
||||||
for (const key in PermissionFlags) _ALL_PERMISSIONS |= PermissionFlags[key];
|
for (const key in PermissionFlags) _ALL_PERMISSIONS |= PermissionFlags[key];
|
||||||
|
|
||||||
exports.ALL_PERMISSIONS = _ALL_PERMISSIONS;
|
exports.ALL_PERMISSIONS = _ALL_PERMISSIONS;
|
||||||
|
|
||||||
exports.DEFAULT_PERMISSIONS = 104324097;
|
exports.DEFAULT_PERMISSIONS = 104324097;
|
||||||
|
|||||||
5
src/util/EscapeMarkdown.js
Normal file
5
src/util/EscapeMarkdown.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = function escapeMarkdown(text, onlyCodeBlock = false, onlyInlineCode = false) {
|
||||||
|
if (onlyCodeBlock) return text.replace(/```/g, '`\u200b``');
|
||||||
|
if (onlyInlineCode) return text.replace(/\\(`|\\)/g, '$1').replace(/(`|\\)/g, '\\$1');
|
||||||
|
return text.replace(/\\(\*|_|`|~|\\)/g, '$1').replace(/(\*|_|`|~|\\)/g, '\\$1');
|
||||||
|
};
|
||||||
19
src/util/FetchRecommendedShards.js
Normal file
19
src/util/FetchRecommendedShards.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const superagent = require('superagent');
|
||||||
|
const botGateway = require('./Constants').Endpoints.botGateway;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the recommended shard count from Discord
|
||||||
|
* @param {number} token Discord auth token
|
||||||
|
* @returns {Promise<number>} the recommended number of shards
|
||||||
|
*/
|
||||||
|
module.exports = function fetchRecommendedShards(token) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!token) throw new Error('A token must be provided.');
|
||||||
|
superagent.get(botGateway)
|
||||||
|
.set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`)
|
||||||
|
.end((err, res) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
resolve(res.body.shards);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
6
src/util/MakeError.js
Normal file
6
src/util/MakeError.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = function makeError(obj) {
|
||||||
|
const err = new Error(obj.message);
|
||||||
|
err.name = obj.name;
|
||||||
|
err.stack = obj.stack;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
7
src/util/MakePlainError.js
Normal file
7
src/util/MakePlainError.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = function makePlainError(err) {
|
||||||
|
const obj = {};
|
||||||
|
obj.name = err.name;
|
||||||
|
obj.message = err.message;
|
||||||
|
obj.stack = err.stack;
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
@@ -4,29 +4,30 @@ const Discord = require('../');
|
|||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const client = new Discord.Client({ fetch_all_members: false, api_request_method: 'sequential' });
|
const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' });
|
||||||
|
|
||||||
const { email, password, token } = require('./auth.json');
|
const { email, password, token, usertoken, song } = require('./auth.json');
|
||||||
|
|
||||||
client.login(token).then(atoken => console.log('logged in with token ' + atoken)).catch(console.log);
|
client.login(token).then(atoken => console.log('logged in with token ' + atoken)).catch(console.error);
|
||||||
|
|
||||||
|
client.ws.on('send', console.log);
|
||||||
|
|
||||||
client.on('ready', () => {
|
client.on('ready', () => {
|
||||||
console.log('ready!');
|
console.log('ready!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.on('userUpdate', (o, n) => {
|
||||||
|
console.log(o.username, n.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('guildMemberAdd', (g, m) => console.log(`${m.user.username} joined ${g.name}`));
|
||||||
|
|
||||||
client.on('channelCreate', channel => {
|
client.on('channelCreate', channel => {
|
||||||
console.log(`made ${channel.name}`);
|
console.log(`made ${channel.name}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('guildMemberAdd', (g, m) => {
|
client.on('error', m => console.log('debug', m));
|
||||||
console.log(`${m.user.username} joined ${g.name}`);
|
client.on('reconnecting', m => console.log('debug', m));
|
||||||
})
|
|
||||||
|
|
||||||
client.on('guildMemberUpdate', (g, o, n) => {
|
|
||||||
console.log(o.nickname, n.nickname);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('debug', console.log);
|
|
||||||
|
|
||||||
client.on('message', message => {
|
client.on('message', message => {
|
||||||
if (true) {
|
if (true) {
|
||||||
@@ -36,6 +37,23 @@ client.on('message', message => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.content === 'imma queue pls') {
|
||||||
|
let count = 0;
|
||||||
|
let ecount = 0;
|
||||||
|
for(let x = 0; x < 4000; x++) {
|
||||||
|
message.channel.sendMessage(`this is message ${x} of 3999`)
|
||||||
|
.then(m => {
|
||||||
|
count++;
|
||||||
|
console.log('reached', count, ecount);
|
||||||
|
})
|
||||||
|
.catch(m => {
|
||||||
|
console.error(m);
|
||||||
|
ecount++;
|
||||||
|
console.log('reached', count, ecount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (message.content === 'myperms?') {
|
if (message.content === 'myperms?') {
|
||||||
message.channel.sendMessage('Your permissions are:\n' +
|
message.channel.sendMessage('Your permissions are:\n' +
|
||||||
JSON.stringify(message.channel.permissionsFor(message.author).serialize(), null, 4));
|
JSON.stringify(message.channel.permissionsFor(message.author).serialize(), null, 4));
|
||||||
@@ -57,7 +75,7 @@ client.on('message', message => {
|
|||||||
request
|
request
|
||||||
.get('url')
|
.get('url')
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
client.user.setAvatar(res.body).catch(console.log)
|
client.user.setAvatar(res.body).catch(console.error)
|
||||||
.then(user => message.channel.sendMessage('Done!'));
|
.then(user => message.channel.sendMessage('Done!'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -65,11 +83,11 @@ client.on('message', message => {
|
|||||||
if (message.content.startsWith('gn')) {
|
if (message.content.startsWith('gn')) {
|
||||||
message.guild.setName(message.content.substr(3))
|
message.guild.setName(message.content.substr(3))
|
||||||
.then(guild => console.log('guild updated to', guild.name))
|
.then(guild => console.log('guild updated to', guild.name))
|
||||||
.catch(console.log);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content === 'leave') {
|
if (message.content === 'leave') {
|
||||||
message.guild.leave().then(guild => console.log('left guild', guild.name)).catch(console.log);
|
message.guild.leave().then(guild => console.log('left guild', guild.name)).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content === 'stats') {
|
if (message.content === 'stats') {
|
||||||
@@ -79,7 +97,7 @@ client.on('message', message => {
|
|||||||
m += `I am aware of ${client.channels.size} channels overall\n`;
|
m += `I am aware of ${client.channels.size} channels overall\n`;
|
||||||
m += `I am aware of ${client.guilds.size} guilds overall\n`;
|
m += `I am aware of ${client.guilds.size} guilds overall\n`;
|
||||||
m += `I am aware of ${client.users.size} users overall\n`;
|
m += `I am aware of ${client.users.size} users overall\n`;
|
||||||
message.channel.sendMessage(m).then(msg => msg.edit('nah')).catch(console.log);
|
message.channel.sendMessage(m).then(msg => msg.edit('nah')).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content === 'messageme!') {
|
if (message.content === 'messageme!') {
|
||||||
@@ -94,7 +112,7 @@ client.on('message', message => {
|
|||||||
message.guild.member(message.mentions[0]).kick().then(member => {
|
message.guild.member(message.mentions[0]).kick().then(member => {
|
||||||
console.log(member);
|
console.log(member);
|
||||||
message.channel.sendMessage('Kicked!' + member.user.username);
|
message.channel.sendMessage('Kicked!' + member.user.username);
|
||||||
}).catch(console.log);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content === 'ratelimittest') {
|
if (message.content === 'ratelimittest') {
|
||||||
@@ -108,17 +126,17 @@ client.on('message', message => {
|
|||||||
if (message.content === 'makerole') {
|
if (message.content === 'makerole') {
|
||||||
message.guild.createRole().then(role => {
|
message.guild.createRole().then(role => {
|
||||||
message.channel.sendMessage(`Made role ${role.name}`);
|
message.channel.sendMessage(`Made role ${role.name}`);
|
||||||
}).catch(console.log);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function nameLoop(user) {
|
function nameLoop(user) {
|
||||||
// user.setUsername(user.username + 'a').then(nameLoop).catch(console.log);
|
// user.setUsername(user.username + 'a').then(nameLoop).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chanLoop(channel) {
|
function chanLoop(channel) {
|
||||||
channel.setName(channel.name + 'a').then(chanLoop).catch(console.log);
|
channel.setName(channel.name + 'a').then(chanLoop).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.on('message', msg => {
|
client.on('message', msg => {
|
||||||
@@ -142,6 +160,7 @@ let disp, con;
|
|||||||
|
|
||||||
client.on('message', msg => {
|
client.on('message', msg => {
|
||||||
if (msg.content.startsWith('/play')) {
|
if (msg.content.startsWith('/play')) {
|
||||||
|
console.log('I am now going to play', msg.content);
|
||||||
const chan = msg.content.split(' ').slice(1).join(' ');
|
const chan = msg.content.split(' ').slice(1).join(' ');
|
||||||
con.playStream(ytdl(chan, {filter : 'audioonly'}), { passes : 4 });
|
con.playStream(ytdl(chan, {filter : 'audioonly'}), { passes : 4 });
|
||||||
}
|
}
|
||||||
@@ -151,11 +170,10 @@ client.on('message', msg => {
|
|||||||
.then(conn => {
|
.then(conn => {
|
||||||
con = conn;
|
con = conn;
|
||||||
msg.reply('done');
|
msg.reply('done');
|
||||||
disp = conn.player.playStream(ytdl('https://www.youtube.com/watch?v=oQBiPwklN_Q', {filter : 'audioonly'}), { passes : 3 });
|
disp = conn.playStream(ytdl(song, {filter:'audioonly'}), { passes : 3 });
|
||||||
conn.player.on('debug', console.log);
|
conn.player.on('debug', console.log);
|
||||||
conn.player.on('error', err => console.log(123, err));
|
conn.player.on('error', err => console.log(123, err));
|
||||||
disp.on('error', err => console.log(123, err));
|
|
||||||
})
|
})
|
||||||
.catch(console.log);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ const Discord = require('../');
|
|||||||
const { token } = require('./auth.json');
|
const { token } = require('./auth.json');
|
||||||
|
|
||||||
const client = new Discord.Client({
|
const client = new Discord.Client({
|
||||||
shard_id: process.argv[2],
|
shardId: process.argv[2],
|
||||||
shard_count: process.argv[3],
|
shardCount: process.argv[3],
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('message', msg => {
|
client.on('message', msg => {
|
||||||
@@ -20,7 +20,12 @@ client.on('message', msg => {
|
|||||||
process.send(123);
|
process.send(123);
|
||||||
|
|
||||||
client.on('ready', () => {
|
client.on('ready', () => {
|
||||||
console.log('Ready');
|
console.log('Ready', client.options.shardId);
|
||||||
|
if (client.options.shardId === 0)
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('kek dying');
|
||||||
|
client.destroy();
|
||||||
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.login(token).catch(console.log);
|
client.login(token).catch(console.error);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const Discord = require('../');
|
const Discord = require('../');
|
||||||
|
|
||||||
const sharder = new Discord.ShardingManager(`${process.cwd()}/test/shard.js`);
|
const sharder = new Discord.ShardingManager(`${process.cwd()}/test/shard.js`, 5, false);
|
||||||
|
|
||||||
sharder.on('launch', id => console.log(`launched ${id}`));
|
sharder.on('launch', id => console.log(`launched ${id}`));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user