mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-10 16:43:31 +01:00
v10 prep
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -35,4 +35,8 @@ build/Release
|
||||
node_modules
|
||||
test/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:
|
||||
- node_modules
|
||||
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:
|
||||
|
||||
1. Fork & clone the repository
|
||||
2. Run `npm install`, or `npm install --no-optional` if you're not working on voice
|
||||
3. Code your heart out!
|
||||
4. Run `npm test` to run ESLint
|
||||
5. Run `npm run docs` to build any documentation changes
|
||||
2. Run `npm install`
|
||||
3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript`
|
||||
4. Code your heart out!
|
||||
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)
|
||||
|
||||
37
README.md
37
README.md
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<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>
|
||||
</p>
|
||||
|
||||
@@ -16,12 +16,14 @@ discord.js is a powerful node.js module that allows you to interact with the [Di
|
||||
|
||||
## Installation
|
||||
**Node.js 6.0.0 or newer is required.**
|
||||
With voice support: `npm install --save discord.js --production`
|
||||
Without voice support: `npm install --save discord.js --production --no-optional`
|
||||
Without voice support: `npm install discord.js --save`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||
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.
|
||||
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).
|
||||
discord.js will automatically prefer node-opus over opusscript.
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript.
|
||||
Using opusscript is only recommended for development on Windows, since getting node-opus to build there can be a bit of a challenge.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
## Example Usage
|
||||
```js
|
||||
@@ -41,17 +43,24 @@ client.on('message', message => {
|
||||
client.login('your token');
|
||||
```
|
||||
|
||||
A bot template using discord.js can be generated using [generator-discordbot](https://www.npmjs.com/package/generator-discordbot).
|
||||
|
||||
## Links
|
||||
* [Website](http://hydrabolt.github.io/discord.js/)
|
||||
* [Discord.js Server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API Server](https://discord.gg/rV4BwdK)
|
||||
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||
* [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)
|
||||
* [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
|
||||
Before reporting an issue, please read the [documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master).
|
||||
If you can't find help there, you can ask in the official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||
## Contributing
|
||||
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||
[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
|
||||
**Node.js 6.0.0 or newer is required.**
|
||||
With voice support: `npm install --save discord.js --production`
|
||||
Without voice support: `npm install --save discord.js --production --no-optional`
|
||||
Without voice support: `npm install discord.js --save`
|
||||
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||
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.
|
||||
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).
|
||||
discord.js will automatically prefer node-opus over opusscript.
|
||||
The preferred audio engine is node-opus, as it performs significantly better than opusscript.
|
||||
Using opusscript is only recommended for development on Windows, since getting node-opus to build there can be a bit of a challenge.
|
||||
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||
|
||||
## Guides
|
||||
* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/)
|
||||
@@ -33,15 +35,15 @@ discord.js will automatically prefer node-opus over opusscript.
|
||||
|
||||
## Links
|
||||
* [Website](http://hydrabolt.github.io/discord.js/)
|
||||
* [Discord.js Server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API Server](https://discord.gg/rV4BwdK)
|
||||
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||
* [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)
|
||||
* [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
|
||||
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).
|
||||
|
||||
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",
|
||||
"version": "9.3.1",
|
||||
"version": "10.0.0",
|
||||
"description": "A powerful library for interacting with the Discord API",
|
||||
"main": "./src/index",
|
||||
"scripts": {
|
||||
@@ -28,7 +28,10 @@
|
||||
"dependencies": {
|
||||
"superagent": "^2.2.0",
|
||||
"tweetnacl": "^0.14.3",
|
||||
"ws": "^1.1.1",
|
||||
"ws": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"node-opus": "^0.2.1",
|
||||
"opusscript": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -36,9 +39,6 @@
|
||||
"jsdoc-parse": "^1.2.0",
|
||||
"eslint": "^3.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-opus": "^0.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ const ClientVoiceManager = require('./voice/ClientVoiceManager');
|
||||
const WebSocketManager = require('./websocket/WebSocketManager');
|
||||
const ActionsManager = require('./actions/ActionsManager');
|
||||
const Collection = require('../util/Collection');
|
||||
const Presence = require('../structures/Presence').Presence;
|
||||
const ShardClientUtil = require('../sharding/ShardClientUtil');
|
||||
|
||||
/**
|
||||
* The starting point for making a Discord Bot.
|
||||
@@ -18,22 +20,19 @@ class Client extends EventEmitter {
|
||||
/**
|
||||
* @param {ClientOptions} [options] Options for the client
|
||||
*/
|
||||
constructor(options) {
|
||||
constructor(options = {}) {
|
||||
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
|
||||
* @type {ClientOptions}
|
||||
*/
|
||||
this.options = mergeDefault(Constants.DefaultOptions, options);
|
||||
|
||||
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;
|
||||
}
|
||||
this._validateOptions();
|
||||
|
||||
/**
|
||||
* The REST manager of the client
|
||||
@@ -84,6 +83,12 @@ class Client extends EventEmitter {
|
||||
*/
|
||||
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
|
||||
* @type {Collection<string, User>}
|
||||
@@ -103,10 +108,21 @@ class Client extends EventEmitter {
|
||||
this.channels = new Collection();
|
||||
|
||||
/**
|
||||
* The authorization token for the logged in user/bot.
|
||||
* @type {?string}
|
||||
* A Collection of presences for friends of the logged in user.
|
||||
* <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
|
||||
@@ -130,38 +146,20 @@ class Client extends EventEmitter {
|
||||
* The date at which the Client was regarded as being in the `READY` state.
|
||||
* @type {?Date}
|
||||
*/
|
||||
this.readyTime = null;
|
||||
this.readyAt = null;
|
||||
|
||||
this._timeouts = new Set();
|
||||
this._intervals = new Set();
|
||||
|
||||
if (this.options.message_sweep_interval > 0) {
|
||||
this.setInterval(this.sweepMessages.bind(this), this.options.message_sweep_interval * 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 });
|
||||
}
|
||||
});
|
||||
if (this.options.messageSweepInterval > 0) {
|
||||
this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The status for the logged in Client.
|
||||
* @readonly
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
get status() {
|
||||
return this.ws.status;
|
||||
@@ -169,17 +167,17 @@ class Client extends EventEmitter {
|
||||
|
||||
/**
|
||||
* The uptime for the logged in Client.
|
||||
* @readonly
|
||||
* @type {?number}
|
||||
* @readonly
|
||||
*/
|
||||
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.
|
||||
* @readonly
|
||||
* @type {Collection<string, VoiceConnection>}
|
||||
* @readonly
|
||||
*/
|
||||
get voiceConnections() {
|
||||
return this.voice.connections;
|
||||
@@ -192,10 +190,21 @@ class Client extends EventEmitter {
|
||||
*/
|
||||
get emojis() {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* much better to use a bot account rather than a user account.
|
||||
@@ -225,30 +234,26 @@ class Client extends EventEmitter {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
destroy() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.manager.destroy().then(() => {
|
||||
for (const t of this._timeouts) clearTimeout(t);
|
||||
for (const i of this._intervals) clearInterval(i);
|
||||
this._timeouts = [];
|
||||
this._intervals = [];
|
||||
this.token = null;
|
||||
this.email = null;
|
||||
this.password = null;
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
for (const t of this._timeouts) clearTimeout(t);
|
||||
for (const i of this._intervals) clearInterval(i);
|
||||
this._timeouts = [];
|
||||
this._intervals = [];
|
||||
this.token = null;
|
||||
this.email = null;
|
||||
this.password = null;
|
||||
return this.manager.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however
|
||||
* if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.
|
||||
* @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) {
|
||||
this.ws.send({
|
||||
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.
|
||||
* @param {string} code the invite code.
|
||||
* @param {InviteResolvable} invite An invite code or URL
|
||||
* @returns {Promise<Invite>}
|
||||
*/
|
||||
fetchInvite(code) {
|
||||
fetchInvite(invite) {
|
||||
const code = this.resolver.resolveInviteCode(invite);
|
||||
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.
|
||||
* 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)
|
||||
* will be removed from the caches. The default is based on the client's `message_cache_lifetime` option.
|
||||
* @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 `messageCacheLifetime` option.
|
||||
* @returns {number} Amount of messages that were removed from the caches,
|
||||
* or -1 if the message cache lifetime is unlimited
|
||||
*/
|
||||
sweepMessages(lifetime = this.options.message_cache_lifetime) {
|
||||
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('Lifetime must be a number.');
|
||||
sweepMessages(lifetime = this.options.messageCacheLifetime) {
|
||||
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.');
|
||||
if (lifetime <= 0) {
|
||||
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
|
||||
return -1;
|
||||
@@ -298,7 +313,7 @@ class Client extends EventEmitter {
|
||||
channels++;
|
||||
|
||||
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);
|
||||
messages++;
|
||||
}
|
||||
@@ -333,6 +348,51 @@ class Client extends EventEmitter {
|
||||
clearInterval(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;
|
||||
|
||||
@@ -3,6 +3,7 @@ const cloneObject = require('../util/CloneObject');
|
||||
const Guild = require('../structures/Guild');
|
||||
const User = require('../structures/User');
|
||||
const DMChannel = require('../structures/DMChannel');
|
||||
const Emoji = require('../structures/Emoji');
|
||||
const TextChannel = require('../structures/TextChannel');
|
||||
const VoiceChannel = require('../structures/VoiceChannel');
|
||||
const GuildChannel = require('../structures/GuildChannel');
|
||||
@@ -27,7 +28,7 @@ class ClientDataManager {
|
||||
* @event Client#guildCreate
|
||||
* @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); });
|
||||
} else {
|
||||
this.client.emit(Constants.Events.GUILD_CREATE, guild);
|
||||
@@ -73,6 +74,26 @@ class ClientDataManager {
|
||||
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) {
|
||||
const already = this.client.guilds.has(guild.id);
|
||||
this.client.guilds.delete(guild.id);
|
||||
@@ -97,6 +118,12 @@ class ClientDataManager {
|
||||
updateChannel(currentChannel, 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;
|
||||
|
||||
@@ -99,23 +99,6 @@ class ClientDataResolver {
|
||||
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:
|
||||
* * An instance of a Channel
|
||||
@@ -138,6 +121,26 @@ class ClientDataResolver {
|
||||
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:
|
||||
* * A string
|
||||
@@ -205,6 +208,23 @@ class ClientDataResolver {
|
||||
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:
|
||||
* * A Buffer
|
||||
|
||||
@@ -49,19 +49,20 @@ class ClientManager {
|
||||
*/
|
||||
setupKeepAlive(time) {
|
||||
this.heartbeatInterval = this.client.setInterval(() => {
|
||||
this.client.emit('debug', 'Sending heartbeat');
|
||||
this.client.ws.send({
|
||||
op: Constants.OPCodes.HEARTBEAT,
|
||||
d: Date.now(),
|
||||
d: this.client.ws.sequence,
|
||||
}, true);
|
||||
}, time);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
return new Promise((resolve) => {
|
||||
this.client.ws.destroy();
|
||||
if (!this.client.user.bot) {
|
||||
this.client.rest.methods.logout().then(resolve);
|
||||
} else {
|
||||
this.client.ws.destroy();
|
||||
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('UserUpdate');
|
||||
this.register('GuildSync');
|
||||
this.register('GuildEmojiCreate');
|
||||
this.register('GuildEmojiDelete');
|
||||
this.register('GuildEmojiUpdate');
|
||||
this.register('GuildRolesPositionUpdate');
|
||||
}
|
||||
|
||||
register(name) {
|
||||
|
||||
@@ -24,7 +24,7 @@ class ChannelDeleteAction extends Action {
|
||||
}
|
||||
|
||||
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) {
|
||||
const oldChannel = cloneObject(channel);
|
||||
channel.setup(data);
|
||||
if (!oldChannel.equals(data)) client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel);
|
||||
client.emit(Constants.Events.CHANNEL_UPDATE, oldChannel, channel);
|
||||
return {
|
||||
old: oldChannel,
|
||||
updated: channel,
|
||||
|
||||
@@ -38,7 +38,7 @@ class GuildDeleteAction extends Action {
|
||||
}
|
||||
|
||||
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._removeMember(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);
|
||||
} else {
|
||||
member = this.deleted.get(guild.id + data.user.id) || null;
|
||||
@@ -36,15 +36,14 @@ class GuildMemberRemoveAction extends Action {
|
||||
}
|
||||
|
||||
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.
|
||||
* @event Client#guildMemberRemove
|
||||
* @param {Guild} guild The guild that the member has left.
|
||||
* @param {GuildMember} member The member that has left the guild.
|
||||
* @param {GuildMember} member The member that has left/been kicked from the guild.
|
||||
*/
|
||||
|
||||
module.exports = GuildMemberRemoveAction;
|
||||
|
||||
@@ -11,7 +11,7 @@ class GuildRoleCreate extends Action {
|
||||
const already = guild.roles.has(data.role.id);
|
||||
const role = new Role(guild, data.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 {
|
||||
role,
|
||||
};
|
||||
@@ -24,9 +24,8 @@ class GuildRoleCreate extends Action {
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted whenever a guild role is created.
|
||||
* @event Client#guildRoleCreate
|
||||
* @param {Guild} guild The guild that the role was created in.
|
||||
* Emitted whenever a role is created.
|
||||
* @event Client#roleCreate
|
||||
* @param {Role} role The role that was created.
|
||||
*/
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class GuildRoleDeleteAction extends Action {
|
||||
guild.roles.delete(data.role_id);
|
||||
this.deleted.set(guild.id + data.role_id, role);
|
||||
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 {
|
||||
role = this.deleted.get(guild.id + data.role_id) || null;
|
||||
}
|
||||
@@ -33,14 +33,13 @@ class GuildRoleDeleteAction extends Action {
|
||||
}
|
||||
|
||||
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.
|
||||
* @event Client#guildRoleDelete
|
||||
* @param {Guild} guild The guild that the role was deleted in.
|
||||
* @event Client#roleDelete
|
||||
* @param {Role} role The role that was deleted.
|
||||
*/
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ class GuildRoleUpdateAction extends Action {
|
||||
let oldRole = null;
|
||||
|
||||
const role = guild.roles.get(roleData.id);
|
||||
if (role && !role.equals(roleData)) {
|
||||
if (role) {
|
||||
oldRole = cloneObject(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 {
|
||||
@@ -33,8 +33,7 @@ class GuildRoleUpdateAction extends Action {
|
||||
|
||||
/**
|
||||
* Emitted whenever a guild role is updated.
|
||||
* @event Client#guildRoleUpdate
|
||||
* @param {Guild} guild The guild that the role was updated in.
|
||||
* @event Client#roleUpdate
|
||||
* @param {Role} oldRole The role before 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) {
|
||||
data.presences = data.presences || [];
|
||||
for (const presence of data.presences) {
|
||||
const user = client.users.get(presence.user.id);
|
||||
if (user) {
|
||||
user.status = presence.status;
|
||||
user.game = presence.game;
|
||||
}
|
||||
guild._setPresence(presence.user.id, presence);
|
||||
}
|
||||
|
||||
data.members = data.members || [];
|
||||
|
||||
@@ -10,7 +10,7 @@ class GuildUpdateAction extends Action {
|
||||
if (guild) {
|
||||
const oldGuild = cloneObject(guild);
|
||||
guild.setup(data);
|
||||
if (!oldGuild.equals(data)) client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild);
|
||||
client.emit(Constants.Events.GUILD_UPDATE, oldGuild, guild);
|
||||
return {
|
||||
old: oldGuild,
|
||||
updated: guild,
|
||||
|
||||
@@ -33,7 +33,7 @@ class MessageDeleteAction extends Action {
|
||||
|
||||
scheduleForDeletion(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);
|
||||
if (channel) {
|
||||
const message = channel.messages.get(data.id);
|
||||
if (message && !message.equals(data, true)) {
|
||||
if (message) {
|
||||
const oldMessage = cloneObject(message);
|
||||
message.patch(data);
|
||||
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;
|
||||
|
||||
@@ -26,7 +26,7 @@ class RESTManager {
|
||||
}
|
||||
|
||||
getRequestHandler() {
|
||||
switch (this.client.options.api_request_method) {
|
||||
switch (this.client.options.apiRequestMethod) {
|
||||
case 'sequential':
|
||||
return SequentialRequestHandler;
|
||||
case 'burst':
|
||||
|
||||
@@ -7,13 +7,16 @@ const User = requireStructure('User');
|
||||
const GuildMember = requireStructure('GuildMember');
|
||||
const Role = requireStructure('Role');
|
||||
const Invite = requireStructure('Invite');
|
||||
const Webhook = requireStructure('Webhook');
|
||||
const UserProfile = requireStructure('UserProfile');
|
||||
|
||||
class RESTMethods {
|
||||
constructor(restManager) {
|
||||
this.rest = restManager;
|
||||
}
|
||||
|
||||
loginToken(token) {
|
||||
loginToken(token = this.rest.client.token) {
|
||||
token = token.replace(/^Bot\s*/i, '');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.client.manager.connectToWebSocket(token, resolve, reject);
|
||||
});
|
||||
@@ -26,42 +29,47 @@ class RESTMethods {
|
||||
this.rest.client.password = password;
|
||||
this.rest.makeRequest('post', Constants.Endpoints.login, false, { email, password })
|
||||
.then(data => {
|
||||
this.rest.client.manager.connectToWebSocket(data.token, resolve, reject);
|
||||
resolve(this.loginToken(data.token));
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
return this.rest.makeRequest('post', Constants.Endpoints.logout, true);
|
||||
return this.rest.makeRequest('post', Constants.Endpoints.logout, true, {});
|
||||
}
|
||||
|
||||
getGateway() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rest.makeRequest('get', Constants.Endpoints.gateway, true)
|
||||
.then(res => {
|
||||
this.rest.client.ws.gateway = `${res.url}/?encoding=json&v=${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);
|
||||
})
|
||||
.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) => {
|
||||
if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
|
||||
|
||||
if (disable_everyone || (typeof disable_everyone === 'undefined' && this.rest.client.options.disable_everyone)) {
|
||||
content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
|
||||
}
|
||||
if (content) {
|
||||
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) {
|
||||
this.createDM(channel).then(chan => {
|
||||
this._sendMessageRequest(chan, content, file, tts, nonce, resolve, reject);
|
||||
})
|
||||
.catch(reject);
|
||||
}).catch(reject);
|
||||
} else {
|
||||
this._sendMessageRequest(channel, content, file, tts, nonce, resolve, reject);
|
||||
}
|
||||
@@ -71,22 +79,24 @@ class RESTMethods {
|
||||
_sendMessageRequest(channel, content, file, tts, nonce, resolve, reject) {
|
||||
if (content instanceof Array) {
|
||||
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,
|
||||
}, file).catch(reject);
|
||||
|
||||
for (let i = 1; i <= content.length; i++) {
|
||||
if (i < content.length) {
|
||||
promise.then(data => {
|
||||
const i2 = i;
|
||||
promise = promise.then(data => {
|
||||
datas.push(data);
|
||||
return this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
||||
content: content[i], tts, nonce,
|
||||
content: content[i2], tts, nonce,
|
||||
}, file);
|
||||
});
|
||||
}).catch(reject);
|
||||
} else {
|
||||
promise.then(data => {
|
||||
datas.push(data);
|
||||
resolve(this.rest.client.actions.MessageCreate.handle(datas).messages);
|
||||
});
|
||||
}).catch(reject);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -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
|
||||
deleteGuild(guild) {
|
||||
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) => {
|
||||
const id = this.rest.client.resolver.resolveUserID(member);
|
||||
if (!id) throw new Error('Couldn\'t resolve the user ID to ban.');
|
||||
|
||||
this.rest.makeRequest('put', `${Constants.Endpoints.guildBans(guild.id)}/${id}`, true, {
|
||||
'delete-message-days': deleteDays,
|
||||
}).then(() => {
|
||||
if (member instanceof GuildMember) {
|
||||
resolve(member);
|
||||
return;
|
||||
}
|
||||
const user = this.rest.client.resolver.resolveUser(id);
|
||||
if (user) {
|
||||
member = this.rest.client.resolver.resolveGuildMember(guild, user);
|
||||
resolve(member || user);
|
||||
return;
|
||||
}
|
||||
resolve(id);
|
||||
}).catch(reject);
|
||||
this.rest.makeRequest('put',
|
||||
`${Constants.Endpoints.guildBans(guild.id)}/${id}?delete-message-days=${deleteDays}`, true, {
|
||||
'delete-message-days': deleteDays,
|
||||
}).then(() => {
|
||||
if (member instanceof GuildMember) {
|
||||
resolve(member);
|
||||
return;
|
||||
}
|
||||
const user = this.rest.client.resolver.resolveUser(id);
|
||||
if (user) {
|
||||
member = this.rest.client.resolver.resolveGuildMember(guild, user);
|
||||
resolve(member || user);
|
||||
return;
|
||||
}
|
||||
resolve(id);
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -419,12 +450,13 @@ class RESTMethods {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = {};
|
||||
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;
|
||||
if (typeof data.color === 'string' && data.color.startsWith('#')) {
|
||||
data.color = parseInt(data.color.replace('#', ''), 16);
|
||||
}
|
||||
data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist;
|
||||
data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable;
|
||||
|
||||
if (_data.permissions) {
|
||||
let perms = 0;
|
||||
@@ -516,6 +548,178 @@ class RESTMethods {
|
||||
}).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;
|
||||
|
||||
@@ -2,6 +2,7 @@ const Collection = require('../../util/Collection');
|
||||
const mergeDefault = require('../../util/MergeDefault');
|
||||
const Constants = require('../../util/Constants');
|
||||
const VoiceConnection = require('./VoiceConnection');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
|
||||
/**
|
||||
* Manages all the voice stuff for the Client
|
||||
@@ -26,52 +27,17 @@ class ClientVoiceManager {
|
||||
* @type {Collection<string, VoiceChannel>}
|
||||
*/
|
||||
this.pending = new Collection();
|
||||
|
||||
this.client.on('self.voiceServer', this.onVoiceServer.bind(this));
|
||||
this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a pending request can be processed
|
||||
* @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);
|
||||
});
|
||||
}
|
||||
onVoiceServer(data) {
|
||||
if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setTokenAndEndpoint(data.token, data.endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the Client receives information about this voice server update.
|
||||
* @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);
|
||||
onVoiceStateUpdate(data) {
|
||||
if (this.pending.has(data.guild_id)) this.pending.get(data.guild_id).setSessionID(data.session_id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,13 +45,26 @@ class ClientVoiceManager {
|
||||
* @param {VoiceChannel} channel The channel to join
|
||||
* @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({
|
||||
guild_id: channel.guild.id,
|
||||
channel_id: channel.id,
|
||||
self_mute: false,
|
||||
self_deaf: false,
|
||||
}, options);
|
||||
|
||||
this.client.ws.send({
|
||||
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
||||
d: options,
|
||||
@@ -99,28 +78,171 @@ class ClientVoiceManager {
|
||||
*/
|
||||
joinChannel(channel) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.pending.get(channel.guild.id)) throw new Error(`Already connecting to a channel in guild.`);
|
||||
const existingConn = this.connections.get(channel.guild.id);
|
||||
if (existingConn) {
|
||||
if (existingConn.channel.id !== channel.id) {
|
||||
this._sendWSJoin(channel);
|
||||
if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.');
|
||||
|
||||
if (!channel.permissionsFor(this.client.user).hasPermission('CONNECT')) {
|
||||
throw new Error('You do not have permission to join this voice 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;
|
||||
}
|
||||
resolve(existingConn);
|
||||
resolve(existingConnection);
|
||||
return;
|
||||
}
|
||||
this.pending.set(channel.guild.id, {
|
||||
channel,
|
||||
sessionID: null,
|
||||
token: null,
|
||||
endpoint: null,
|
||||
resolve,
|
||||
reject,
|
||||
|
||||
const pendingConnection = new PendingVoiceConnection(this, channel);
|
||||
this.pending.set(channel.guild.id, pendingConnection);
|
||||
|
||||
pendingConnection.on('fail', reason => {
|
||||
this.pending.delete(channel.guild.id);
|
||||
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;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const VoiceConnectionWebSocket = require('./VoiceConnectionWebSocket');
|
||||
const VoiceConnectionUDPClient = require('./VoiceConnectionUDPClient');
|
||||
const VoiceReceiver = require('./receiver/VoiceReceiver');
|
||||
const VoiceWebSocket = require('./VoiceWebSocket');
|
||||
const VoiceUDP = require('./VoiceUDPClient');
|
||||
const Constants = require('../../util/Constants');
|
||||
const AudioPlayer = require('./player/AudioPlayer');
|
||||
const VoiceReceiver = require('./receiver/VoiceReceiver');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const DefaultPlayer = require('./player/DefaultPlayer');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Represents a connection to a Voice Channel in Discord.
|
||||
@@ -16,89 +17,101 @@ const DefaultPlayer = require('./player/DefaultPlayer');
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class VoiceConnection extends EventEmitter {
|
||||
constructor(manager, channel, token, sessionID, endpoint, resolve, reject) {
|
||||
|
||||
constructor(pendingConnection) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* The voice manager of this connection
|
||||
* The Voice Manager that instantiated this connection
|
||||
* @type {ClientVoiceManager}
|
||||
* @private
|
||||
*/
|
||||
this.manager = manager;
|
||||
this.voiceManager = pendingConnection.voiceManager;
|
||||
|
||||
/**
|
||||
* The player
|
||||
* @type {BasePlayer}
|
||||
*/
|
||||
this.player = new DefaultPlayer(this);
|
||||
|
||||
/**
|
||||
* The endpoint of the connection
|
||||
* @type {string}
|
||||
*/
|
||||
this.endpoint = endpoint;
|
||||
|
||||
/**
|
||||
* The VoiceChannel for this connection
|
||||
* The voice channel this connection is currently serving
|
||||
* @type {VoiceChannel}
|
||||
*/
|
||||
this.channel = channel;
|
||||
this.channel = pendingConnection.channel;
|
||||
|
||||
/**
|
||||
* The WebSocket connection for this voice connection
|
||||
* @type {VoiceConnectionWebSocket}
|
||||
* @private
|
||||
* An array of Voice Receivers that have been created for this connection
|
||||
* @type {VoiceReceiver[]}
|
||||
*/
|
||||
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.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.
|
||||
* @event VoiceConnection#error
|
||||
* @param {Error} error The encountered error
|
||||
* The authentication data needed to connect to the voice server
|
||||
* @type {object}
|
||||
* @private
|
||||
*/
|
||||
this.emit('error', err);
|
||||
this._shutdown(err);
|
||||
this.authentication = pendingConnection.data;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} [reason='user requested'] The reason of the disconnection
|
||||
* Sets whether the voice connection should display as "speaking" or not
|
||||
* @param {boolean} value whether or not to speak
|
||||
* @private
|
||||
*/
|
||||
disconnect(reason = 'user requested') {
|
||||
this.manager.client.ws.send({
|
||||
setSpeaking(value) {
|
||||
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,
|
||||
d: {
|
||||
guild_id: this.channel.guild.id,
|
||||
@@ -107,81 +120,51 @@ class VoiceConnection extends EventEmitter {
|
||||
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.
|
||||
* @event VoiceConnection#disconnected
|
||||
* @param {Error} error The encountered error, if any
|
||||
* Emitted when the voice connection disconnects
|
||||
* @event VoiceConnection#disconnect
|
||||
*/
|
||||
this.emit('disconnected', e);
|
||||
this.emit('disconnect');
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds listeners to the WebSocket and UDP sub-clients.
|
||||
* Connect the voice connection
|
||||
* @private
|
||||
*/
|
||||
bindListeners() {
|
||||
this.websocket.on('error', err => this._onError(err));
|
||||
this.websocket.on('close', err => this._onClose(err));
|
||||
this.websocket.on('ready-for-udp', data => {
|
||||
this.udp = new VoiceConnectionUDPClient(this, data);
|
||||
this.data = data;
|
||||
this.udp.on('error', err => this._onError(err));
|
||||
this.udp.on('close', err => this._onClose(err));
|
||||
});
|
||||
this.websocket.on('ready', secretKey => {
|
||||
this.data.secret = secretKey;
|
||||
this.ready = true;
|
||||
connect() {
|
||||
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
|
||||
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
|
||||
this.sockets.ws = new VoiceWebSocket(this);
|
||||
this.sockets.udp = new VoiceUDP(this);
|
||||
this.sockets.ws.on('error', e => this.emit('error', e));
|
||||
this.sockets.udp.on('error', e => this.emit('error', e));
|
||||
this.sockets.ws.once('ready', d => {
|
||||
this.authentication.port = d.port;
|
||||
this.authentication.ssrc = d.ssrc;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
this._resolve(this);
|
||||
this.emit('ready');
|
||||
});
|
||||
this.once('ready', () => {
|
||||
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 => {
|
||||
this.sockets.ws.on('speaking', data => {
|
||||
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);
|
||||
if (!data.speaking) {
|
||||
for (const receiver of this.receivers) {
|
||||
@@ -206,7 +189,6 @@ class VoiceConnection extends EventEmitter {
|
||||
* @param {boolean} speaking Whether or not the user is 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);
|
||||
});
|
||||
}
|
||||
@@ -232,9 +214,8 @@ class VoiceConnection extends EventEmitter {
|
||||
* })
|
||||
* .catch(console.log);
|
||||
*/
|
||||
playFile(file, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||
const options = { seek, volume, passes };
|
||||
return this.player.playFile(file, options);
|
||||
playFile(file, options) {
|
||||
return this.playStream(fs.createReadStream(file), options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,7 +236,7 @@ class VoiceConnection extends EventEmitter {
|
||||
*/
|
||||
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||
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 } = {}) {
|
||||
const options = { seek, volume, passes };
|
||||
this.player._shutdown();
|
||||
return this.player.playPCMStream(stream, options);
|
||||
}
|
||||
|
||||
@@ -275,9 +255,9 @@ class VoiceConnection extends EventEmitter {
|
||||
* @returns {VoiceReceiver}
|
||||
*/
|
||||
createReceiver() {
|
||||
const rcv = new VoiceReceiver(this);
|
||||
this.receivers.push(rcv);
|
||||
return rcv;
|
||||
const receiver = new VoiceReceiver(this);
|
||||
this.receivers.push(receiver);
|
||||
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._triggered = false;
|
||||
this._volume = streamOptions.volume;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @type {number}
|
||||
*/
|
||||
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
|
||||
* @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
|
||||
* How long the stream dispatcher has been "speaking" for
|
||||
* @type {number}
|
||||
* @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}
|
||||
* @readonly
|
||||
*/
|
||||
@@ -76,176 +67,6 @@ class StreamDispatcher extends EventEmitter {
|
||||
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
|
||||
* @type {number}
|
||||
@@ -292,6 +113,194 @@ class StreamDispatcher extends EventEmitter {
|
||||
resume() {
|
||||
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;
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
const ConverterEngine = require('./ConverterEngine');
|
||||
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 {
|
||||
constructor(player) {
|
||||
@@ -24,10 +62,7 @@ class FfmpegConverterEngine extends ConverterEngine {
|
||||
'-ss', String(seek),
|
||||
'pipe:1',
|
||||
], { stdio: ['pipe', 'pipe', 'ignore'] });
|
||||
encoder.on('error', e => this.handleError(encoder, e));
|
||||
encoder.stdin.on('error', e => this.handleError(encoder, e));
|
||||
encoder.stdout.on('error', e => this.handleError(encoder, e));
|
||||
return encoder;
|
||||
return new PCMConversionProcess(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,
|
||||
delay: 0,
|
||||
},
|
||||
}).catch(e => {
|
||||
this.emit('debug', e);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,19 +25,22 @@ class VoiceReceiver extends EventEmitter {
|
||||
this.queues = new Map();
|
||||
this.pcmStreams = new Map();
|
||||
this.opusStreams = new Map();
|
||||
|
||||
/**
|
||||
* Whether or not this receiver has been destroyed.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.destroyed = false;
|
||||
|
||||
/**
|
||||
* The VoiceConnection that instantiated this
|
||||
* @type {VoiceConnection}
|
||||
*/
|
||||
this.connection = connection;
|
||||
this._listener = (msg => {
|
||||
this.voiceConnection = connection;
|
||||
|
||||
this._listener = msg => {
|
||||
const ssrc = +msg.readUInt32BE(8).toString(10);
|
||||
const user = this.connection.ssrcMap.get(ssrc);
|
||||
const user = this.voiceConnection.ssrcMap.get(ssrc);
|
||||
if (!user) {
|
||||
if (!this.queues.has(ssrc)) this.queues.set(ssrc, []);
|
||||
this.queues.get(ssrc).push(msg);
|
||||
@@ -50,8 +53,8 @@ class VoiceReceiver extends EventEmitter {
|
||||
}
|
||||
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() {
|
||||
if (!this.destroyed) return;
|
||||
this.connection.udp.udpSocket.on('message', this._listener);
|
||||
this.voiceConnection.sockets.udp.socket.on('message', this._listener);
|
||||
this.destroyed = false;
|
||||
return;
|
||||
}
|
||||
@@ -70,7 +73,7 @@ class VoiceReceiver extends EventEmitter {
|
||||
* Destroy this VoiceReceiver, also ending any streams that it may be controlling.
|
||||
*/
|
||||
destroy() {
|
||||
this.connection.udp.udpSocket.removeListener('message', this._listener);
|
||||
this.voiceConnection.sockets.udp.socket.removeListener('message', this._listener);
|
||||
for (const stream of this.pcmStreams) {
|
||||
stream[1]._push(null);
|
||||
this.pcmStreams.delete(stream[0]);
|
||||
@@ -89,7 +92,7 @@ class VoiceReceiver extends EventEmitter {
|
||||
* @returns {ReadableStream}
|
||||
*/
|
||||
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 (this.opusStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
|
||||
const stream = new Readable();
|
||||
@@ -104,7 +107,7 @@ class VoiceReceiver extends EventEmitter {
|
||||
* @returns {ReadableStream}
|
||||
*/
|
||||
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 (this.pcmStreams.get(user.id)) throw new Error('There is already an existing stream for that user.');
|
||||
const stream = new Readable();
|
||||
@@ -114,7 +117,7 @@ class VoiceReceiver extends EventEmitter {
|
||||
|
||||
handlePacket(msg, user) {
|
||||
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) {
|
||||
/**
|
||||
* 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 {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);
|
||||
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}
|
||||
*/
|
||||
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
|
||||
* @param {string} gateway The gateway to connect to
|
||||
*/
|
||||
connect(gateway) {
|
||||
_connect(gateway) {
|
||||
this.client.emit('debug', `Connecting to gateway ${gateway}`);
|
||||
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.onopen = () => this.eventOpen();
|
||||
this.ws.onclose = (d) => this.eventClose(d);
|
||||
@@ -77,6 +86,15 @@ class WebSocketManager extends EventEmitter {
|
||||
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
|
||||
* @param {Object} data An object that can be JSON stringified
|
||||
@@ -99,6 +117,7 @@ class WebSocketManager extends EventEmitter {
|
||||
|
||||
_send(data) {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.emit('send', data);
|
||||
this.ws.send(data);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +144,7 @@ class WebSocketManager extends EventEmitter {
|
||||
*/
|
||||
eventOpen() {
|
||||
this.client.emit('debug', 'Connection to gateway opened');
|
||||
if (this.reconnecting) this._sendResume();
|
||||
if (this.status === Constants.Status.RECONNECTING) this._sendResume();
|
||||
else this._sendNewIdentify();
|
||||
}
|
||||
|
||||
@@ -133,6 +152,11 @@ class WebSocketManager extends EventEmitter {
|
||||
* Sends a gateway resume packet, in cases of unexpected disconnections.
|
||||
*/
|
||||
_sendResume() {
|
||||
if (!this.sessionID) {
|
||||
this._sendNewIdentify();
|
||||
return;
|
||||
}
|
||||
this.client.emit('debug', 'Identifying as resumed session');
|
||||
const payload = {
|
||||
token: this.client.token,
|
||||
session_id: this.sessionID,
|
||||
@@ -152,13 +176,15 @@ class WebSocketManager extends EventEmitter {
|
||||
this.reconnecting = false;
|
||||
const payload = this.client.options.ws;
|
||||
payload.token = this.client.token;
|
||||
if (this.client.options.shard_count > 0) {
|
||||
payload.shard = [Number(this.client.options.shard_id), Number(this.client.options.shard_count)];
|
||||
if (this.client.options.shardCount > 0) {
|
||||
payload.shard = [Number(this.client.options.shardId), Number(this.client.options.shardCount)];
|
||||
}
|
||||
this.client.emit('debug', 'Identifying as new session');
|
||||
this.send({
|
||||
op: Constants.OPCodes.IDENTIFY,
|
||||
d: payload,
|
||||
});
|
||||
this.sequence = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,6 +197,7 @@ class WebSocketManager extends EventEmitter {
|
||||
* Emitted whenever the client websocket is disconnected
|
||||
* @event Client#disconnect
|
||||
*/
|
||||
clearInterval(this.client.manager.heartbeatInterval);
|
||||
if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT);
|
||||
if (event.code === 4004) return;
|
||||
if (event.code === 4010) return;
|
||||
@@ -194,7 +221,7 @@ class WebSocketManager extends EventEmitter {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -235,10 +262,11 @@ class WebSocketManager extends EventEmitter {
|
||||
}
|
||||
if (unavailableCount === 0) {
|
||||
this.status = Constants.Status.NEARLY;
|
||||
if (this.client.options.fetch_all_members) {
|
||||
const promises = this.client.guilds.array().map(g => g.fetchMembers());
|
||||
if (this.client.options.fetchAllMembers) {
|
||||
const promises = this.client.guilds.map(g => g.fetchMembers());
|
||||
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();
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -42,6 +42,8 @@ class WebSocketPacketManager {
|
||||
this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk');
|
||||
this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate');
|
||||
this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync');
|
||||
this.register(Constants.WSEvents.RELATIONSHIP_ADD, 'RelationshipAdd');
|
||||
this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, 'RelationshipRemove');
|
||||
}
|
||||
|
||||
get client() {
|
||||
@@ -72,17 +74,22 @@ class WebSocketPacketManager {
|
||||
}
|
||||
|
||||
if (packet.op === Constants.OPCodes.INVALID_SESSION) {
|
||||
this.ws.sessionID = null;
|
||||
this.ws._sendNewIdentify();
|
||||
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.checkIfReady();
|
||||
}
|
||||
|
||||
this.setSequence(packet.s);
|
||||
|
||||
if (this.ws.disabledEvents[packet.t] !== undefined) return false;
|
||||
|
||||
if (this.ws.status !== Constants.Status.READY) {
|
||||
if (BeforeReadyWhitelist.indexOf(packet.t) === -1) {
|
||||
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();
|
||||
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
|
||||
* @param {Guild} guild The guild that the chunks relate to
|
||||
* @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) {
|
||||
const memberInGuild = guild.members.get(user.id);
|
||||
if (!memberInGuild && data.status !== 'offline') {
|
||||
const member = guild._addMember({
|
||||
let member = guild.members.get(user.id);
|
||||
if (!member && data.status !== 'offline') {
|
||||
member = guild._addMember({
|
||||
user,
|
||||
roles: data.roles,
|
||||
deaf: false,
|
||||
mute: 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
|
||||
* @param {User} oldUser The user before the presence update
|
||||
* @param {User} newUser The user after the presence update
|
||||
* @param {GuildMember} oldMember The member before 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
|
||||
* @event Client#guildMemberAvailable
|
||||
* @param {Guild} guild The guild that the member became available in
|
||||
* @param {GuildMember} member The member that became available
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,13 +10,28 @@ class ReadyHandler extends AbstractHandler {
|
||||
|
||||
const clientUser = new ClientUser(client, data.user);
|
||||
client.user = clientUser;
|
||||
client.readyTime = new Date();
|
||||
client.readyAt = new Date();
|
||||
client.users.set(clientUser.id, clientUser);
|
||||
|
||||
for (const guild of data.guilds) client.dataManager.newGuild(guild);
|
||||
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));
|
||||
|
||||
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) {
|
||||
const client = this.packetManager.client;
|
||||
const data = packet.d;
|
||||
if (client.voice.pending.has(data.guild_id)) {
|
||||
client.voice._receivedVoiceServer(data.guild_id, data.token, data.endpoint);
|
||||
}
|
||||
client.emit('self.voiceServer', data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ class VoiceStateUpdateHandler extends AbstractHandler {
|
||||
// if the member left the voice channel, unset their speaking property
|
||||
if (!data.channel_id) member.speaking = null;
|
||||
|
||||
if (client.voice.pending.has(guild.id) && member.user.id === client.user.id && data.channel_id) {
|
||||
client.voice._receivedVoiceStateUpdate(data.guild_id, data.session_id);
|
||||
if (member.user.id === client.user.id && data.channel_id) {
|
||||
client.emit('self.voiceStateUpdate', data);
|
||||
}
|
||||
|
||||
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 = {
|
||||
Client: require('./client/Client'),
|
||||
WebhookClient: require('./client/WebhookClient'),
|
||||
Shard: require('./sharding/Shard'),
|
||||
ShardClientUtil: require('./sharding/ShardClientUtil'),
|
||||
ShardingManager: require('./sharding/ShardingManager'),
|
||||
|
||||
Collection: require('./util/Collection'),
|
||||
splitMessage: require('./util/SplitMessage'),
|
||||
escapeMarkdown: require('./util/EscapeMarkdown'),
|
||||
fetchRecommendedShards: require('./util/FetchRecommendedShards'),
|
||||
|
||||
Channel: require('./structures/Channel'),
|
||||
ClientUser: require('./structures/ClientUser'),
|
||||
DMChannel: require('./structures/DMChannel'),
|
||||
Emoji: require('./structures/Emoji'),
|
||||
EvaluatedPermissions: require('./structures/EvaluatedPermissions'),
|
||||
Game: require('./structures/Presence').Game,
|
||||
GroupDMChannel: require('./structures/GroupDMChannel'),
|
||||
Guild: require('./structures/Guild'),
|
||||
GuildChannel: require('./structures/GuildChannel'),
|
||||
@@ -23,10 +28,12 @@ module.exports = {
|
||||
PartialGuild: require('./structures/PartialGuild'),
|
||||
PartialGuildChannel: require('./structures/PartialGuildChannel'),
|
||||
PermissionOverwrites: require('./structures/PermissionOverwrites'),
|
||||
Presence: require('./structures/Presence').Presence,
|
||||
Role: require('./structures/Role'),
|
||||
TextChannel: require('./structures/TextChannel'),
|
||||
User: require('./structures/User'),
|
||||
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 path = require('path');
|
||||
const makeError = require('../util/MakeError');
|
||||
const makePlainError = require('../util/MakePlainError');
|
||||
|
||||
/**
|
||||
* Represents a Shard spawned by the ShardingManager.
|
||||
@@ -8,35 +10,39 @@ class Shard {
|
||||
/**
|
||||
* @param {ShardingManager} manager The sharding manager
|
||||
* @param {number} id The ID of this shard
|
||||
* @param {array} [args=[]] Command line arguments to pass to the script
|
||||
*/
|
||||
constructor(manager, id) {
|
||||
constructor(manager, id, args = []) {
|
||||
/**
|
||||
* The manager of the spawned shard
|
||||
* Manager that created the shard
|
||||
* @type {ShardingManager}
|
||||
*/
|
||||
this.manager = manager;
|
||||
|
||||
/**
|
||||
* The shard ID
|
||||
* ID of the shard
|
||||
* @type {number}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
this.process = childProcess.fork(path.resolve(this.manager.file), [], {
|
||||
env: {
|
||||
SHARD_ID: this.id,
|
||||
SHARD_COUNT: this.manager.totalShards,
|
||||
},
|
||||
this.process = childProcess.fork(path.resolve(this.manager.file), args, {
|
||||
env: this.env,
|
||||
});
|
||||
|
||||
this.process.on('message', message => {
|
||||
this.manager.emit('message', this, message);
|
||||
});
|
||||
|
||||
this.process.on('message', this._handleMessage.bind(this));
|
||||
this.process.once('exit', () => {
|
||||
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.
|
||||
* @param {string} script JavaScript to run on the shard
|
||||
@@ -69,20 +107,10 @@ class Shard {
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const listener = message => {
|
||||
if (!message) return;
|
||||
if (message._evalResult) {
|
||||
this.process.removeListener('message', listener);
|
||||
this._evals.delete(script);
|
||||
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);
|
||||
}
|
||||
if (!message || message._eval !== script) return;
|
||||
this.process.removeListener('message', listener);
|
||||
this._evals.delete(script);
|
||||
if (!message._error) resolve(message._result); else reject(makeError(message._error));
|
||||
};
|
||||
this.process.on('message', listener);
|
||||
|
||||
@@ -98,35 +126,30 @@ 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);
|
||||
* Handles an IPC message
|
||||
* @param {*} message Message received
|
||||
* @private
|
||||
*/
|
||||
fetchClientValue(prop) {
|
||||
if (this._fetches.has(prop)) return this._fetches.get(prop);
|
||||
_handleMessage(message) {
|
||||
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) => {
|
||||
const listener = message => {
|
||||
if (typeof message !== 'object' || message._fetchProp !== prop) return;
|
||||
this.process.removeListener('message', listener);
|
||||
this._fetches.delete(prop);
|
||||
resolve(message._fetchPropValue);
|
||||
};
|
||||
this.process.on('message', listener);
|
||||
// Shard is requesting an eval broadcast
|
||||
if (message._sEval) {
|
||||
this.manager.broadcastEval(message._sEval)
|
||||
.then(results => this.send({ _sEval: message._sEval, _result: results }))
|
||||
.catch(err => this.send({ _sEval: message._sEval, _error: makePlainError(err) }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.send({ _fetchProp: prop }).catch(err => {
|
||||
this.process.removeListener('message', listener);
|
||||
this._fetches.delete(prop);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
this._fetches.set(prop, promise);
|
||||
return promise;
|
||||
this.manager.emit('message', this, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 fs = require('fs');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
|
||||
const mergeDefault = require('../util/MergeDefault');
|
||||
const Shard = require('./Shard');
|
||||
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
|
||||
* 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>
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class ShardingManager extends EventEmitter {
|
||||
/**
|
||||
* @param {string} file Path to your shard script file
|
||||
* @param {number} [totalShards=1] Number of shards to default to spawning
|
||||
* @param {boolean} [respawn=true] Respawn a shard when it dies
|
||||
* @param {Object} [options] Options for the sharding manager
|
||||
* @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();
|
||||
options = mergeDefault({
|
||||
totalShards: 'auto',
|
||||
respawn: true,
|
||||
shardArgs: [],
|
||||
token: null,
|
||||
}, options);
|
||||
|
||||
/**
|
||||
* Path to the shard script file
|
||||
@@ -32,16 +43,36 @@ class ShardingManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Amount of shards that this manager is going to spawn
|
||||
* @type {number}
|
||||
* @type {number|string}
|
||||
*/
|
||||
this.totalShards = totalShards;
|
||||
if (typeof totalShards !== 'number' || isNaN(totalShards)) {
|
||||
throw new TypeError('Amount of shards must be a number.');
|
||||
this.totalShards = options.totalShards;
|
||||
if (this.totalShards !== 'auto') {
|
||||
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
|
||||
@@ -52,11 +83,11 @@ class ShardingManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* 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>}
|
||||
*/
|
||||
createShard(id = this.shards.size) {
|
||||
const shard = new Shard(this, id);
|
||||
const shard = new Shard(this, id, this.shardArgs);
|
||||
this.shards.set(id, shard);
|
||||
/**
|
||||
* Emitted upon launching a shard
|
||||
@@ -74,10 +105,29 @@ class ShardingManager extends EventEmitter {
|
||||
* @returns {Promise<Collection<number, Shard>>}
|
||||
*/
|
||||
spawn(amount = this.totalShards, delay = 5500) {
|
||||
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 RangeError('Amount of shards must be an integer.');
|
||||
return new Promise((resolve, reject) => {
|
||||
if (amount === 'auto') {
|
||||
fetchRecommendedShards(this.token).then(count => {
|
||||
this.totalShards = count;
|
||||
resolve(this._spawn(count, delay));
|
||||
}).catch(reject);
|
||||
} else {
|
||||
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
|
||||
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||
if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
|
||||
resolve(this._spawn(amount, delay));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
|
||||
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
|
||||
* @type {Date}
|
||||
*/
|
||||
get creationDate() {
|
||||
return new Date((this.id / 4194304) + 1420070400000);
|
||||
get createdTimestamp() {
|
||||
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
|
||||
* channel.delete()
|
||||
* .then() // success
|
||||
* .catch(console.log); // log error
|
||||
* .catch(console.error); // log error
|
||||
*/
|
||||
delete() {
|
||||
return this.client.rest.methods.deleteChannel(this);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const User = require('./User');
|
||||
const Collection = require('../util/Collection');
|
||||
|
||||
/**
|
||||
* Represents the logged in client's Discord User
|
||||
@@ -19,8 +20,22 @@ class ClientUser extends User {
|
||||
* @type {string}
|
||||
*/
|
||||
this.email = data.email;
|
||||
|
||||
this.localPresence = {};
|
||||
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) {
|
||||
@@ -37,7 +52,7 @@ class ClientUser extends User {
|
||||
* // set username
|
||||
* client.user.setUsername('discordjs')
|
||||
* .then(user => console.log(`My new username is ${user.username}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setUsername(username) {
|
||||
return this.client.rest.methods.updateCurrentUser({ username });
|
||||
@@ -52,7 +67,7 @@ class ClientUser extends User {
|
||||
* // set email
|
||||
* client.user.setEmail('bob@gmail.com')
|
||||
* .then(user => console.log(`My new email is ${user.email}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setEmail(email) {
|
||||
return this.client.rest.methods.updateCurrentUser({ email });
|
||||
@@ -65,9 +80,9 @@ class ClientUser extends User {
|
||||
* @returns {Promise<ClientUser>}
|
||||
* @example
|
||||
* // set password
|
||||
* client.user.setPassword('password')
|
||||
* client.user.setPassword('password123')
|
||||
* .then(user => console.log('New password set!'))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setPassword(password) {
|
||||
return this.client.rest.methods.updateCurrentUser({ password });
|
||||
@@ -75,68 +90,144 @@ class ClientUser extends User {
|
||||
|
||||
/**
|
||||
* Set the avatar of the logged in Client.
|
||||
* @param {Base64Resolvable} avatar The new avatar
|
||||
* @param {FileResolvable|Base64Resolveable} avatar The new avatar
|
||||
* @returns {Promise<ClientUser>}
|
||||
* @example
|
||||
* // set avatar
|
||||
* client.user.setAvatar(fs.readFileSync('./avatar.png'))
|
||||
* client.user.setAvatar('./avatar.png')
|
||||
* .then(user => console.log(`New avatar set!`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
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.
|
||||
* @param {string} [status] The status, can be `online` or `idle`
|
||||
* @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)
|
||||
* Set the status of the logged in user.
|
||||
* @param {string} status can be `online`, `idle`, `invisible` or `dnd` (do not disturb)
|
||||
* @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 => {
|
||||
if (status === 'online' || status === 'here' || status === 'available') {
|
||||
this.idleStatus = null;
|
||||
} else if (status === 'idle' || status === 'away') {
|
||||
this.idleStatus = Date.now();
|
||||
if (!icon) resolve(this.client.rest.methods.createGuild({ name, icon, region }));
|
||||
if (icon.startsWith('data:')) {
|
||||
resolve(this.client.rest.methods.createGuild({ name, icon, region }));
|
||||
} else {
|
||||
this.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 (game === null) {
|
||||
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 (data.status) {
|
||||
if (typeof data.status !== 'string') throw new TypeError('Status must be a string');
|
||||
status = data.status;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
this.userGame.url = url;
|
||||
this.userGame.type = 1;
|
||||
if (data.game) {
|
||||
game = data.game;
|
||||
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({
|
||||
op: 3,
|
||||
d: {
|
||||
idle_since: this.idleStatus,
|
||||
game: this.userGame,
|
||||
},
|
||||
d: this.localPresence,
|
||||
});
|
||||
|
||||
this.status = this.idleStatus ? 'idle' : 'online';
|
||||
this.game = this.userGame;
|
||||
this.client._setPresence(this.id, this.localPresence);
|
||||
|
||||
resolve(this);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,12 +51,21 @@ class Emoji {
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the emoji was created
|
||||
* The timestamp the emoji was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
* @type {Date}
|
||||
*/
|
||||
get creationDate() {
|
||||
return new Date((this.id / 4194304) + 1420070400000);
|
||||
get createdTimestamp() {
|
||||
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}
|
||||
* @example
|
||||
* // send an emoji:
|
||||
* const emoji = guild.emojis.array()[0];
|
||||
* const emoji = guild.emojis.first();
|
||||
* msg.reply(`Hello! ${emoji}`);
|
||||
*/
|
||||
toString() {
|
||||
|
||||
@@ -50,7 +50,17 @@ class EvaluatedPermissions {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
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.
|
||||
* @type {User}
|
||||
* @readonly
|
||||
*/
|
||||
get owner() {
|
||||
return this.client.users.get(this.ownerID);
|
||||
@@ -100,8 +101,8 @@ class GroupDMChannel extends Channel {
|
||||
this.ownerID === channel.ownerID;
|
||||
|
||||
if (equal) {
|
||||
const thisIDs = this.recipients.array().map(r => r.id);
|
||||
const otherIDs = channel.recipients.map(r => r.id);
|
||||
const thisIDs = this.recipients.keyArray();
|
||||
const otherIDs = channel.recipients.keyArray();
|
||||
return arraysEqual(thisIDs, otherIDs);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const User = require('./User');
|
||||
const Role = require('./Role');
|
||||
const Emoji = require('./Emoji');
|
||||
const Presence = require('./Presence').Presence;
|
||||
const GuildMember = require('./GuildMember');
|
||||
const Constants = require('../util/Constants');
|
||||
const Collection = require('../util/Collection');
|
||||
@@ -100,6 +101,12 @@ class Guild {
|
||||
*/
|
||||
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.
|
||||
* @type {Object[]}
|
||||
@@ -137,10 +144,15 @@ class Guild {
|
||||
*/
|
||||
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.available = !data.unavailable;
|
||||
this.features = data.features || this.features || [];
|
||||
this._joinedTimestamp = data.joined_at ? new Date(data.joined_at).getTime() : this._joinedTimestamp;
|
||||
|
||||
if (data.members) {
|
||||
this.members.clear();
|
||||
@@ -170,11 +182,7 @@ class Guild {
|
||||
|
||||
if (data.presences) {
|
||||
for (const presence of data.presences) {
|
||||
const user = this.client.users.get(presence.user.id);
|
||||
if (user) {
|
||||
user.status = presence.status;
|
||||
user.game = presence.game;
|
||||
}
|
||||
this._setPresence(presence.user.id, presence);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,20 +205,30 @@ class Guild {
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the guild was created
|
||||
* The timestamp the guild was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
* @type {Date}
|
||||
*/
|
||||
get creationDate() {
|
||||
return new Date((this.id / 4194304) + 1420070400000);
|
||||
get createdTimestamp() {
|
||||
return (this.id / 4194304) + 1420070400000;
|
||||
}
|
||||
|
||||
/**
|
||||
* The date at which the logged-in client joined the guild.
|
||||
* The time the guild was created
|
||||
* @type {Date}
|
||||
* @readonly
|
||||
*/
|
||||
get joinDate() {
|
||||
return new Date(this._joinedTimestamp);
|
||||
get createdAt() {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {UserResolvable} user The user to fetch the member for
|
||||
@@ -329,7 +355,7 @@ class Guild {
|
||||
* region: 'london',
|
||||
* })
|
||||
* .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
edit(data) {
|
||||
return this.client.rest.methods.updateGuild(this, data);
|
||||
@@ -343,7 +369,7 @@ class Guild {
|
||||
* // edit the guild name
|
||||
* guild.setName('Discord Guild')
|
||||
* .then(updated => console.log(`Updated guild name to ${guild.name}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setName(name) {
|
||||
return this.edit({ name });
|
||||
@@ -357,7 +383,7 @@ class Guild {
|
||||
* // edit the guild region
|
||||
* guild.setRegion('london')
|
||||
* .then(updated => console.log(`Updated guild region to ${guild.region}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setRegion(region) {
|
||||
return this.edit({ region });
|
||||
@@ -371,7 +397,7 @@ class Guild {
|
||||
* // edit the guild verification level
|
||||
* guild.setVerificationLevel(1)
|
||||
* .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setVerificationLevel(verificationLevel) {
|
||||
return this.edit({ verificationLevel });
|
||||
@@ -385,7 +411,7 @@ class Guild {
|
||||
* // edit the guild AFK channel
|
||||
* guild.setAFKChannel(channel)
|
||||
* .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setAFKChannel(afkChannel) {
|
||||
return this.edit({ afkChannel });
|
||||
@@ -399,7 +425,7 @@ class Guild {
|
||||
* // edit the guild AFK channel
|
||||
* guild.setAFKTimeout(60)
|
||||
* .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setAFKTimeout(afkTimeout) {
|
||||
return this.edit({ afkTimeout });
|
||||
@@ -413,7 +439,7 @@ class Guild {
|
||||
* // edit the guild icon
|
||||
* guild.setIcon(fs.readFileSync('./icon.png'))
|
||||
* .then(updated => console.log('Updated the guild icon'))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setIcon(icon) {
|
||||
return this.edit({ icon });
|
||||
@@ -427,7 +453,7 @@ class Guild {
|
||||
* // edit the guild owner
|
||||
* guild.setOwner(guilds.members[0])
|
||||
* .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setOwner(owner) {
|
||||
return this.edit({ owner });
|
||||
@@ -441,7 +467,7 @@ class Guild {
|
||||
* // edit the guild splash
|
||||
* guild.setIcon(fs.readFileSync('./splash.png'))
|
||||
* .then(updated => console.log('Updated the guild splash'))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setSplash(splash) {
|
||||
return this.edit({ splash });
|
||||
@@ -514,7 +540,7 @@ class Guild {
|
||||
* // create a new text channel
|
||||
* guild.createChannel('new-general', 'text')
|
||||
* .then(channel => console.log(`Created new channel ${channel}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
createChannel(name, type) {
|
||||
return this.client.rest.methods.createChannel(this, name, type);
|
||||
@@ -528,12 +554,12 @@ class Guild {
|
||||
* // create a new role
|
||||
* guild.createRole()
|
||||
* .then(role => console.log(`Created role ${role}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
* @example
|
||||
* // create a new role with data
|
||||
* guild.createRole({ name: 'Super Cool People' })
|
||||
* .then(role => console.log(`Created role ${role}`))
|
||||
* .catch(console.log)
|
||||
* .catch(console.error)
|
||||
*/
|
||||
createRole(data) {
|
||||
const create = this.client.rest.methods.createGuildRole(this);
|
||||
@@ -541,6 +567,43 @@ class Guild {
|
||||
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.
|
||||
* @returns {Promise<Guild>}
|
||||
@@ -548,7 +611,7 @@ class Guild {
|
||||
* // leave a guild
|
||||
* guild.leave()
|
||||
* .then(g => console.log(`Left the guild ${g}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
leave() {
|
||||
return this.client.rest.methods.leaveGuild(this);
|
||||
@@ -561,12 +624,38 @@ class Guild {
|
||||
* // delete a guild
|
||||
* guild.delete()
|
||||
* .then(g => console.log(`Deleted the guild ${g}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
delete() {
|
||||
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
|
||||
* 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) {
|
||||
const existing = this.members.has(guildUser.user.id);
|
||||
if (!(guildUser.user instanceof User)) guildUser.user = this.client.dataManager.newUser(guildUser.user);
|
||||
|
||||
guildUser.joined_at = guildUser.joined_at || 0;
|
||||
@@ -636,11 +726,10 @@ class Guild {
|
||||
/**
|
||||
* Emitted whenever a user joins a guild.
|
||||
* @event Client#guildMemberAdd
|
||||
* @param {Guild} guild The guild that the user has joined
|
||||
* @param {GuildMember} member The member that has joined
|
||||
* @param {GuildMember} member The member that has joined a guild
|
||||
*/
|
||||
if (this.client.ws.status === Constants.Status.READY && emitEvent) {
|
||||
this.client.emit(Constants.Events.GUILD_MEMBER_ADD, this, member);
|
||||
if (this.client.ws.status === Constants.Status.READY && emitEvent && !existing) {
|
||||
this.client.emit(Constants.Events.GUILD_MEMBER_ADD, member);
|
||||
}
|
||||
|
||||
this._checkChunks();
|
||||
@@ -659,11 +748,10 @@ class Guild {
|
||||
/**
|
||||
* Emitted whenever a Guild Member changes - i.e. new role, removed role, nickname
|
||||
* @event Client#guildMemberUpdate
|
||||
* @param {Guild} guild The guild that the update affects
|
||||
* @param {GuildMember} oldMember The member before 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 {
|
||||
@@ -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() {
|
||||
if (this._fetchWaiter) {
|
||||
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.
|
||||
* @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
|
||||
* @returns {Promise}
|
||||
* @example
|
||||
@@ -120,7 +120,7 @@ class GuildChannel extends Channel {
|
||||
* SEND_MESSAGES: false
|
||||
* })
|
||||
* .then(() => console.log('Done!'))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
overwritePermissions(userOrRole, options) {
|
||||
const payload = {
|
||||
@@ -130,6 +130,9 @@ class GuildChannel extends Channel {
|
||||
|
||||
if (userOrRole instanceof Role) {
|
||||
payload.type = 'role';
|
||||
} else if (this.guild.roles.has(userOrRole)) {
|
||||
userOrRole = this.guild.roles.get(userOrRole);
|
||||
payload.type = 'role';
|
||||
} else {
|
||||
userOrRole = this.client.resolver.resolveUser(userOrRole);
|
||||
payload.type = 'member';
|
||||
@@ -141,8 +144,8 @@ class GuildChannel extends Channel {
|
||||
const prevOverwrite = this.permissionOverwrites.get(userOrRole.id);
|
||||
|
||||
if (prevOverwrite) {
|
||||
payload.allow = prevOverwrite.allow;
|
||||
payload.deny = prevOverwrite.deny;
|
||||
payload.allow = prevOverwrite.allowData;
|
||||
payload.deny = prevOverwrite.denyData;
|
||||
}
|
||||
|
||||
for (const perm in options) {
|
||||
@@ -170,7 +173,7 @@ class GuildChannel extends Channel {
|
||||
* // set a new channel name
|
||||
* channel.setName('not_general')
|
||||
* .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setName(name) {
|
||||
return this.client.rest.methods.updateChannel(this, { name });
|
||||
@@ -184,7 +187,7 @@ class GuildChannel extends Channel {
|
||||
* // set a new channel position
|
||||
* channel.setPosition(2)
|
||||
* .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setPosition(position) {
|
||||
return this.client.rest.methods.updateChannel(this, { position });
|
||||
@@ -198,7 +201,7 @@ class GuildChannel extends Channel {
|
||||
* // set a new channel topic
|
||||
* channel.setTopic('needs more rate limiting')
|
||||
* .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setTopic(topic) {
|
||||
return this.client.rest.methods.updateChannel(this, { topic });
|
||||
@@ -236,12 +239,12 @@ class GuildChannel extends Channel {
|
||||
this.name === channel.name;
|
||||
|
||||
if (equal) {
|
||||
if (channel.permission_overwrites) {
|
||||
const thisIDSet = Array.from(this.permissionOverwrites.keys());
|
||||
const otherIDSet = channel.permission_overwrites.map(overwrite => overwrite.id);
|
||||
if (this.permissionOverwrites && channel.permissionOverwrites) {
|
||||
const thisIDSet = this.permissionOverwrites.keyArray();
|
||||
const otherIDSet = channel.permissionOverwrites.keyArray();
|
||||
equal = arraysEqual(thisIDSet, otherIDSet);
|
||||
} else {
|
||||
equal = false;
|
||||
equal = !this.permissionOverwrites && !channel.permissionOverwrites;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,17 +82,32 @@ class GuildMember {
|
||||
*/
|
||||
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._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}
|
||||
* @readonly
|
||||
*/
|
||||
get joinDate() {
|
||||
return new Date(this._joinDate);
|
||||
get joinedAt() {
|
||||
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.
|
||||
* @type {Role}
|
||||
* @readonly
|
||||
*/
|
||||
get highestRole() {
|
||||
return this.roles.reduce((prev, role) =>
|
||||
!prev || role.position > prev.position || (role.position === prev.position && role.id < prev.id) ? role : prev
|
||||
);
|
||||
return this.roles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,6 +177,7 @@ class GuildMember {
|
||||
/**
|
||||
* The overall set of permissions for the guild member, taking only roles into account
|
||||
* @type {EvaluatedPermissions}
|
||||
* @readonly
|
||||
*/
|
||||
get 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.
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get kickable() {
|
||||
if (this.user.id === this.guild.ownerID) return false;
|
||||
if (this.user.id === this.client.user.id) return false;
|
||||
const clientMember = this.member(this.client.member);
|
||||
const clientMember = this.guild.member(this.client.user);
|
||||
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.
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get bannable() {
|
||||
if (this.user.id === this.guild.ownerID) return false;
|
||||
if (this.user.id === this.client.user.id) return false;
|
||||
const clientMember = this.member(this.client.member);
|
||||
const clientMember = this.guild.member(this.client.user);
|
||||
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) {
|
||||
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}!`);
|
||||
*/
|
||||
toString() {
|
||||
return String(this.user);
|
||||
return `<@${this.nickname ? '!' : ''}${this.user.id}>`;
|
||||
}
|
||||
|
||||
// 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._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}
|
||||
* @readonly
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this._createdAt);
|
||||
return new Date(this.createdTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* The creation date of the invite
|
||||
* @type {Date}
|
||||
* The timestamp the invite will expire at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get creationDate() {
|
||||
return new Date(this._createdAt);
|
||||
get expiresTimestamp() {
|
||||
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 Collection = require('../util/Collection');
|
||||
const Constants = require('../util/Constants');
|
||||
const escapeMarkdown = require('../util/EscapeMarkdown');
|
||||
|
||||
/**
|
||||
* Represents a Message on Discord
|
||||
@@ -31,6 +32,12 @@ class Message {
|
||||
*/
|
||||
this.id = data.id;
|
||||
|
||||
/**
|
||||
* The type of the message
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = Constants.MessageTypes[data.type];
|
||||
|
||||
/**
|
||||
* The content of the message
|
||||
* @type {string}
|
||||
@@ -87,6 +94,18 @@ class Message {
|
||||
this.attachments = new Collection();
|
||||
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
|
||||
* @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 = [];
|
||||
}
|
||||
|
||||
@@ -139,9 +156,9 @@ class Message {
|
||||
if (this.guild) this.member = this.guild.member(this.author);
|
||||
}
|
||||
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) {
|
||||
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 ('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}
|
||||
* @readonly
|
||||
*/
|
||||
get timestamp() {
|
||||
return new Date(this._timestamp);
|
||||
get createdAt() {
|
||||
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}
|
||||
* @readonly
|
||||
*/
|
||||
get editedTimestamp() {
|
||||
return new Date(this._editedTimestamp);
|
||||
get editedAt() {
|
||||
return this.editedTimestamp ? new Date(this.editedTimestamp) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The guild the message was sent in (if in a guild channel)
|
||||
* @type {?Guild}
|
||||
* @readonly
|
||||
*/
|
||||
get guild() {
|
||||
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 relevant mention in the message content will not be converted.
|
||||
* @type {string}
|
||||
* @readonly
|
||||
*/
|
||||
get cleanContent() {
|
||||
return this.content
|
||||
.replace(/@everyone/g, '@\u200Beveryone')
|
||||
.replace(/@here/g, '@\u200Bhere')
|
||||
.replace(/@(everyone|here)/g, '@\u200b$1')
|
||||
.replace(/<@!?[0-9]+>/g, (input) => {
|
||||
const id = input.replace(/<|!|>|@/g, '');
|
||||
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.
|
||||
* Sorted from latest (first) to oldest (last).
|
||||
* @type {Message[]}
|
||||
* @readonly
|
||||
*/
|
||||
get edits() {
|
||||
return this._edits.slice().unshift(this);
|
||||
@@ -258,6 +279,7 @@ class Message {
|
||||
/**
|
||||
* Whether the message is editable by the client user.
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get editable() {
|
||||
return this.author.id === this.client.user.id;
|
||||
@@ -266,6 +288,7 @@ class Message {
|
||||
/**
|
||||
* Whether the message is deletable by the client user.
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get deletable() {
|
||||
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.
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get pinnable() {
|
||||
return !this.guild ||
|
||||
@@ -301,7 +325,7 @@ class Message {
|
||||
* // update the content of a message
|
||||
* message.edit('This is my new content!')
|
||||
* .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
edit(content) {
|
||||
return this.client.rest.methods.updateMessage(this, content);
|
||||
@@ -314,8 +338,8 @@ class Message {
|
||||
* @returns {Promise<Message>}
|
||||
*/
|
||||
editCode(lang, content) {
|
||||
content = this.client.resolver.resolveString(content).replace(/```/g, '`\u200b``');
|
||||
return this.edit(`\`\`\`${lang ? lang : ''}\n${content}\n\`\`\``);
|
||||
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||
return this.edit(`\`\`\`${lang || ''}\n${content}\n\`\`\``);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,7 +366,7 @@ class Message {
|
||||
* // delete a message
|
||||
* message.delete()
|
||||
* .then(msg => console.log(`Deleted message from ${msg.author}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
delete(timeout = 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -363,7 +387,7 @@ class Message {
|
||||
* // reply to a message
|
||||
* message.reply('Hey, I'm a reply!')
|
||||
* .then(msg => console.log(`Sent a reply to ${msg.author}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
reply(content, options = {}) {
|
||||
content = this.client.resolver.resolveString(content);
|
||||
@@ -401,8 +425,8 @@ class Message {
|
||||
|
||||
if (equal && rawData) {
|
||||
equal = this.mentions.everyone === message.mentions.everyone &&
|
||||
this._timestamp === new Date(rawData.timestamp).getTime() &&
|
||||
this._editedTimestamp === new Date(rawData.edited_timestamp).getTime();
|
||||
this.createdTimestamp === new Date(rawData.timestamp).getTime() &&
|
||||
this.editedTimestamp === new Date(rawData.edited_timestamp).getTime();
|
||||
}
|
||||
|
||||
return equal;
|
||||
|
||||
@@ -24,6 +24,7 @@ class MessageCollector extends EventEmitter {
|
||||
* @typedef {Object} CollectorOptions
|
||||
* @property {number} [time] Duration for the collector in milliseconds
|
||||
* @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
|
||||
*/
|
||||
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 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`.
|
||||
* @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
|
||||
* @type {Date}
|
||||
*/
|
||||
get creationDate() {
|
||||
return new Date((this.id / 4194304) + 1420070400000);
|
||||
get createdTimestamp() {
|
||||
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.
|
||||
* @type {Collection<string, GuildMember>}
|
||||
* @readonly
|
||||
*/
|
||||
get members() {
|
||||
return this.guild.members.filter(m => m.roles.has(this.id));
|
||||
@@ -140,7 +150,17 @@ class Role {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
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
|
||||
* role.edit({name: 'new role'})
|
||||
* .then(r => console.log(`Edited role ${r}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
edit(data) {
|
||||
return this.client.rest.methods.updateGuildRole(this, data);
|
||||
@@ -165,7 +185,7 @@ class Role {
|
||||
* // set the name of the role
|
||||
* role.setName('new role')
|
||||
* .then(r => console.log(`Edited name of role ${r}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setName(name) {
|
||||
return this.client.rest.methods.updateGuildRole(this, { name });
|
||||
@@ -179,7 +199,7 @@ class Role {
|
||||
* // set the color of a role
|
||||
* role.setColor('#FF0000')
|
||||
* .then(r => console.log(`Set color of role ${r}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setColor(color) {
|
||||
return this.client.rest.methods.updateGuildRole(this, { color });
|
||||
@@ -193,7 +213,7 @@ class Role {
|
||||
* // set the hoist of the role
|
||||
* role.setHoist(true)
|
||||
* .then(r => console.log(`Role hoisted: ${r.hoist}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setHoist(hoist) {
|
||||
return this.client.rest.methods.updateGuildRole(this, { hoist });
|
||||
@@ -207,10 +227,10 @@ class Role {
|
||||
* // set the position of the role
|
||||
* role.setPosition(1)
|
||||
* .then(r => console.log(`Role position: ${r.position}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
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
|
||||
* role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS'])
|
||||
* .then(r => console.log(`Role updated ${r}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setPermissions(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
|
||||
* @returns {Promise<Role>}
|
||||
@@ -234,7 +268,7 @@ class Role {
|
||||
* // delete a role
|
||||
* role.delete()
|
||||
* .then(r => console.log(`Deleted role ${r}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
delete() {
|
||||
return this.client.rest.methods.deleteGuildRole(this);
|
||||
@@ -265,6 +299,18 @@ class Role {
|
||||
toString() {
|
||||
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;
|
||||
|
||||
@@ -42,6 +42,38 @@ class TextChannel extends GuildChannel {
|
||||
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
|
||||
sendMessage() { return; }
|
||||
sendTTSMessage() { return; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const TextBasedChannel = require('./interface/TextBasedChannel');
|
||||
const Constants = require('../util/Constants');
|
||||
const Presence = require('./Presence').Presence;
|
||||
|
||||
/**
|
||||
* Represents a User on Discord.
|
||||
@@ -47,45 +48,43 @@ class User {
|
||||
* @type {boolean}
|
||||
*/
|
||||
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) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The time the user was created
|
||||
* The timestamp the user was created at
|
||||
* @type {number}
|
||||
* @readonly
|
||||
* @type {Date}
|
||||
*/
|
||||
get creationDate() {
|
||||
return new Date((this.id / 4194304) + 1420070400000);
|
||||
get createdTimestamp() {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.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;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {number} bitrate The new bitrate
|
||||
@@ -53,7 +69,7 @@ class VoiceChannel extends GuildChannel {
|
||||
* // set the bitrate of a voice channel
|
||||
* voiceChannel.setBitrate(48000)
|
||||
* .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
setBitrate(bitrate) {
|
||||
return this.rest.client.rest.methods.updateChannel(this, { bitrate });
|
||||
@@ -66,7 +82,7 @@ class VoiceChannel extends GuildChannel {
|
||||
* // join a voice channel
|
||||
* voiceChannel.join()
|
||||
* .then(connection => console.log('Connected!'))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
join() {
|
||||
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 MessageCollector = require('../MessageCollector');
|
||||
const Collection = require('../../util/Collection');
|
||||
const escapeMarkdown = require('../../util/EscapeMarkdown');
|
||||
|
||||
/**
|
||||
* Interface for classes that have text-channel-like features
|
||||
@@ -27,7 +28,7 @@ class TextBasedChannel {
|
||||
* @typedef {Object} MessageOptions
|
||||
* @property {boolean} [tts=false] Whether or not the message should be spoken aloud
|
||||
* @property {string} [nonce=''] The nonce for the message
|
||||
* @property {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
|
||||
* @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.
|
||||
@@ -38,8 +39,8 @@ class TextBasedChannel {
|
||||
* @typedef {Object} SplitOptions
|
||||
* @property {number} [maxLength=1950] Maximum character length per message piece
|
||||
* @property {string} [char='\n'] Character to split the message with
|
||||
* @property {string} [prepend=''] Text to prepend to each middle piece
|
||||
* @property {string} [append=''] Text to append to each middle piece
|
||||
* @property {string} [prepend=''] Text to prepend to every piece except the first
|
||||
* @property {string} [append=''] Text to append to every piece except the last
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -51,7 +52,7 @@ class TextBasedChannel {
|
||||
* // send a message
|
||||
* channel.sendMessage('hello!')
|
||||
* .then(message => console.log(`Sent message: ${message.content}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
sendMessage(content, options = {}) {
|
||||
return this.client.rest.methods.sendMessage(this, content, options);
|
||||
@@ -66,7 +67,7 @@ class TextBasedChannel {
|
||||
* // send a TTS message
|
||||
* channel.sendTTSMessage('hello!')
|
||||
* .then(message => console.log(`Sent tts message: ${message.content}`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
sendTTSMessage(content, options = {}) {
|
||||
Object.assign(options, { tts: true });
|
||||
@@ -111,15 +112,16 @@ class TextBasedChannel {
|
||||
sendCode(lang, content, options = {}) {
|
||||
if (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```';
|
||||
}
|
||||
content = this.client.resolver.resolveString(content).replace(/```/g, '`\u200b``');
|
||||
return this.sendMessage(`\`\`\`${lang ? lang : ''}\n${content}\n\`\`\``, options);
|
||||
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||
return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {Promise<Message>}
|
||||
* @example
|
||||
@@ -158,7 +160,7 @@ class TextBasedChannel {
|
||||
* // get messages
|
||||
* channel.fetchMessages({limit: 10})
|
||||
* .then(messages => console.log(`Received ${messages.size} messages`))
|
||||
* .catch(console.log);
|
||||
* .catch(console.error);
|
||||
*/
|
||||
fetchMessages(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -241,6 +243,7 @@ class TextBasedChannel {
|
||||
/**
|
||||
* Whether or not the typing indicator is being shown in the channel.
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get typing() {
|
||||
return this.client.user._typing.has(this.id);
|
||||
@@ -249,6 +252,7 @@ class TextBasedChannel {
|
||||
/**
|
||||
* Number of times `startTyping` has been called.
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get typingCount() {
|
||||
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.
|
||||
* @param {Collection<string, Message>|Message[]} messages The messages to delete
|
||||
* @returns {Collection<string, Message>}
|
||||
* @param {Collection<string, Message>|Message[]|number} messages Messages to delete, or number of messages to delete
|
||||
* @returns {Promise<Collection<string, Message>>} Deleted messages
|
||||
*/
|
||||
bulkDelete(messages) {
|
||||
if (messages instanceof Collection) messages = messages.array();
|
||||
if (!(messages instanceof Array)) return Promise.reject(new TypeError('Messages must be an Array or Collection.'));
|
||||
const messageIDs = messages.map(m => m.id);
|
||||
return this.client.rest.methods.bulkDeleteMessages(this, messageIDs);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!isNaN(messages)) {
|
||||
this.fetchMessages({ limit: messages }).then(msgs => resolve(this.bulkDelete(msgs)));
|
||||
} else if (messages instanceof Array || messages instanceof Collection) {
|
||||
const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id);
|
||||
resolve(this.client.rest.methods.bulkDeleteMessages(this, messageIDs));
|
||||
} else {
|
||||
reject(new TypeError('Messages must be an Array, Collection, or number.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_cacheMessage(message) {
|
||||
const maxSize = this.client.options.max_message_cache;
|
||||
const maxSize = this.client.options.messageCacheMaxSize;
|
||||
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);
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -3,26 +3,48 @@
|
||||
* @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}
|
||||
* @example
|
||||
* // identical to:
|
||||
* Array.from(collection.values());
|
||||
*/
|
||||
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}
|
||||
* @example
|
||||
* // identical to:
|
||||
* Array.from(collection.keys());
|
||||
*/
|
||||
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) {
|
||||
if (thisArg) fn = fn.bind(thisArg);
|
||||
const collection = new Collection();
|
||||
const results = new Collection();
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* the delete method. Returns an array of promises
|
||||
|
||||
@@ -1,35 +1,44 @@
|
||||
exports.Package = require('../../package.json');
|
||||
|
||||
/**
|
||||
* Options for a Client.
|
||||
* @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.
|
||||
* @property {number} [shard_id=0] The ID of this shard
|
||||
* @property {number} [shard_count=0] The number of shards
|
||||
* @property {number} [max_message_cache=200] 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 {number} [shardId=0] The ID of this shard
|
||||
* @property {number} [shardCount=0] The number of shards
|
||||
* @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel
|
||||
* @property {boolean} [sync=false] Whether to periodically sync guilds
|
||||
* (-1 for unlimited - don't do this without message sweeping, otherwise memory usage will climb indefinitely)
|
||||
* @property {number} [messageCacheLifetime=0] How long until a message should be uncached by the message sweeping
|
||||
* (in seconds, 0 for forever)
|
||||
* @property {number} [message_sweep_interval=0] How frequently to remove messages from the cache that are older than
|
||||
* the max message lifetime (in seconds, 0 for never)
|
||||
* @property {boolean} [fetch_all_members=false] Whether to cache all guild members and users upon startup
|
||||
* @property {boolean} [disable_everyone=false] Default value for MessageOptions.disable_everyone
|
||||
* @property {number} [rest_ws_bridge_timeout=5000] Maximum time permitted between REST responses and their
|
||||
* @property {number} [messageSweepInterval=0] How frequently to remove messages from the cache that are older than
|
||||
* the message cache lifetime (in seconds, 0 for never)
|
||||
* @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as
|
||||
* upon joining a guild
|
||||
* @property {boolean} [disableEveryone=false] Default value for MessageOptions.disableEveryone
|
||||
* @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their
|
||||
* corresponding websocket events
|
||||
* @property {string[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be
|
||||
* processed. Disabling useless events such as 'TYPING_START' can result in significant performance increases on
|
||||
* large-scale bots.
|
||||
* @property {WebsocketOptions} [ws] Options for the websocket
|
||||
*/
|
||||
exports.DefaultOptions = {
|
||||
api_request_method: 'sequential',
|
||||
shard_id: 0,
|
||||
shard_count: 0,
|
||||
max_message_cache: 200,
|
||||
message_cache_lifetime: 0,
|
||||
message_sweep_interval: 0,
|
||||
fetch_all_members: false,
|
||||
disable_everyone: false,
|
||||
rest_ws_bridge_timeout: 5000,
|
||||
protocol_version: 6,
|
||||
apiRequestMethod: 'sequential',
|
||||
shardId: 0,
|
||||
shardCount: 0,
|
||||
messageCacheMaxSize: 200,
|
||||
messageCacheLifetime: 0,
|
||||
messageSweepInterval: 0,
|
||||
fetchAllMembers: false,
|
||||
disableEveryone: false,
|
||||
restWsBridgeTimeout: 5000,
|
||||
disabledEvents: [],
|
||||
sync: false,
|
||||
|
||||
/**
|
||||
* Websocket options.
|
||||
* Websocket options. These are left as snake_case to match the API.
|
||||
* @typedef {Object} WebsocketOptions
|
||||
* @property {number} [large_threshold=250] Number of members in a guild to be considered large
|
||||
* @property {boolean} [compress=true] Whether to compress data sent on the connection
|
||||
@@ -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 = {
|
||||
NO_TOKEN: 'Request to use token, but token was unavailable to the client.',
|
||||
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.',
|
||||
INVALID_RATE_LIMIT_METHOD: 'Unknown rate limiting method.',
|
||||
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 = {
|
||||
// general endpoints
|
||||
// general
|
||||
login: `${API}/auth/login`,
|
||||
logout: `${API}/auth/logout`,
|
||||
gateway: `${API}/gateway`,
|
||||
botGateway: `${API}/gateway/bot`,
|
||||
invite: (id) => `${API}/invite/${id}`,
|
||||
inviteLink: (id) => `https://discord.gg/${id}`,
|
||||
CDN: 'https://cdn.discordapp.com',
|
||||
@@ -89,9 +82,11 @@ const Endpoints = exports.Endpoints = {
|
||||
// users
|
||||
user: (userID) => `${API}/users/${userID}`,
|
||||
userChannels: (userID) => `${Endpoints.user(userID)}/channels`,
|
||||
userProfile: (userID) => `${Endpoints.user(userID)}/profile`,
|
||||
avatar: (userID, avatar) => userID === '1' ? avatar : `${Endpoints.user(userID)}/avatars/${avatar}.jpg`,
|
||||
me: `${API}/users/@me`,
|
||||
meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`,
|
||||
relationships: (userID) => `${Endpoints.user(userID)}/relationships`,
|
||||
|
||||
// guilds
|
||||
guilds: `${API}/guilds`,
|
||||
@@ -108,6 +103,7 @@ const Endpoints = exports.Endpoints = {
|
||||
guildMember: (guildID, memberID) => `${Endpoints.guildMembers(guildID)}/${memberID}`,
|
||||
stupidInconsistentGuildEndpoint: (guildID) => `${Endpoints.guildMember(guildID, '@me')}/nick`,
|
||||
guildChannels: (guildID) => `${Endpoints.guild(guildID)}/channels`,
|
||||
guildEmojis: (guildID) => `${Endpoints.guild(guildID)}/emojis`,
|
||||
|
||||
// channels
|
||||
channels: `${API}/channels`,
|
||||
@@ -117,6 +113,25 @@ const Endpoints = exports.Endpoints = {
|
||||
channelTyping: (channelID) => `${Endpoints.channel(channelID)}/typing`,
|
||||
channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`,
|
||||
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 = {
|
||||
@@ -130,6 +145,8 @@ exports.OPCodes = {
|
||||
RECONNECT: 7,
|
||||
REQUEST_GUILD_MEMBERS: 8,
|
||||
INVALID_SESSION: 9,
|
||||
HELLO: 10,
|
||||
HEARTBEAT_ACK: 11,
|
||||
};
|
||||
|
||||
exports.VoiceOPCodes = {
|
||||
@@ -145,52 +162,49 @@ exports.Events = {
|
||||
READY: 'ready',
|
||||
GUILD_CREATE: 'guildCreate',
|
||||
GUILD_DELETE: 'guildDelete',
|
||||
GUILD_UPDATE: 'guildUpdate',
|
||||
GUILD_UNAVAILABLE: 'guildUnavailable',
|
||||
GUILD_AVAILABLE: 'guildAvailable',
|
||||
GUILD_UPDATE: 'guildUpdate',
|
||||
GUILD_BAN_ADD: 'guildBanAdd',
|
||||
GUILD_BAN_REMOVE: 'guildBanRemove',
|
||||
GUILD_MEMBER_ADD: 'guildMemberAdd',
|
||||
GUILD_MEMBER_REMOVE: 'guildMemberRemove',
|
||||
GUILD_MEMBER_UPDATE: 'guildMemberUpdate',
|
||||
GUILD_ROLE_CREATE: 'guildRoleCreate',
|
||||
GUILD_ROLE_DELETE: 'guildRoleDelete',
|
||||
GUILD_ROLE_UPDATE: 'guildRoleUpdate',
|
||||
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_DELETE: 'channelDelete',
|
||||
CHANNEL_UPDATE: 'channelUpdate',
|
||||
PRESENCE_UPDATE: 'presenceUpdate',
|
||||
USER_UPDATE: 'userUpdate',
|
||||
VOICE_STATE_UPDATE: 'voiceStateUpdate',
|
||||
TYPING_START: 'typingStart',
|
||||
TYPING_STOP: 'typingStop',
|
||||
WARN: 'warn',
|
||||
GUILD_MEMBERS_CHUNK: 'guildMembersChunk',
|
||||
CHANNEL_PINS_UPDATE: 'channelPinsUpdate',
|
||||
MESSAGE_CREATE: 'message',
|
||||
MESSAGE_DELETE: 'messageDelete',
|
||||
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',
|
||||
RECONNECTING: 'reconnecting',
|
||||
GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking',
|
||||
MESSAGE_BULK_DELETE: 'messageDeleteBulk',
|
||||
CHANNEL_PINS_UPDATE: 'channelPinsUpdate',
|
||||
ERROR: 'error',
|
||||
WARN: 'warn',
|
||||
DEBUG: 'debug',
|
||||
};
|
||||
|
||||
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',
|
||||
GUILD_BAN_ADD: 'GUILD_BAN_ADD',
|
||||
GUILD_BAN_REMOVE: 'GUILD_BAN_REMOVE',
|
||||
GUILD_SYNC: 'GUILD_SYNC',
|
||||
GUILD_CREATE: 'GUILD_CREATE',
|
||||
GUILD_DELETE: 'GUILD_DELETE',
|
||||
GUILD_UPDATE: 'GUILD_UPDATE',
|
||||
GUILD_MEMBER_ADD: 'GUILD_MEMBER_ADD',
|
||||
GUILD_MEMBER_REMOVE: 'GUILD_MEMBER_REMOVE',
|
||||
GUILD_MEMBER_UPDATE: 'GUILD_MEMBER_UPDATE',
|
||||
@@ -198,16 +212,35 @@ exports.WSEvents = {
|
||||
GUILD_ROLE_CREATE: 'GUILD_ROLE_CREATE',
|
||||
GUILD_ROLE_DELETE: 'GUILD_ROLE_DELETE',
|
||||
GUILD_ROLE_UPDATE: 'GUILD_ROLE_UPDATE',
|
||||
GUILD_UPDATE: 'GUILD_UPDATE',
|
||||
TYPING_START: 'TYPING_START',
|
||||
GUILD_BAN_ADD: 'GUILD_BAN_ADD',
|
||||
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',
|
||||
PRESENCE_UPDATE: 'PRESENCE_UPDATE',
|
||||
VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE',
|
||||
TYPING_START: 'TYPING_START',
|
||||
FRIEND_ADD: 'RELATIONSHIP_ADD',
|
||||
FRIEND_REMOVE: 'RELATIONSHIP_REMOVE',
|
||||
VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE',
|
||||
MESSAGE_DELETE_BULK: 'MESSAGE_DELETE_BULK',
|
||||
CHANNEL_PINS_UPDATE: 'CHANNEL_PINS_UPDATE',
|
||||
GUILD_SYNC: 'GUILD_SYNC',
|
||||
RELATIONSHIP_ADD: 'RELATIONSHIP_ADD',
|
||||
RELATIONSHIP_REMOVE: 'RELATIONSHIP_REMOVE',
|
||||
};
|
||||
|
||||
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 = {
|
||||
@@ -238,11 +271,11 @@ const PermissionFlags = exports.PermissionFlags = {
|
||||
CHANGE_NICKNAME: 1 << 26,
|
||||
MANAGE_NICKNAMES: 1 << 27,
|
||||
MANAGE_ROLES_OR_PERMISSIONS: 1 << 28,
|
||||
MANAGE_WEBHOOKS: 1 << 29,
|
||||
MANAGE_EMOJIS: 1 << 30,
|
||||
};
|
||||
|
||||
let _ALL_PERMISSIONS = 0;
|
||||
for (const key in PermissionFlags) _ALL_PERMISSIONS |= PermissionFlags[key];
|
||||
|
||||
exports.ALL_PERMISSIONS = _ALL_PERMISSIONS;
|
||||
|
||||
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 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', () => {
|
||||
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 => {
|
||||
console.log(`made ${channel.name}`);
|
||||
});
|
||||
|
||||
client.on('guildMemberAdd', (g, m) => {
|
||||
console.log(`${m.user.username} joined ${g.name}`);
|
||||
})
|
||||
|
||||
client.on('guildMemberUpdate', (g, o, n) => {
|
||||
console.log(o.nickname, n.nickname);
|
||||
});
|
||||
|
||||
client.on('debug', console.log);
|
||||
client.on('error', m => console.log('debug', m));
|
||||
client.on('reconnecting', m => console.log('debug', m));
|
||||
|
||||
client.on('message', message => {
|
||||
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?') {
|
||||
message.channel.sendMessage('Your permissions are:\n' +
|
||||
JSON.stringify(message.channel.permissionsFor(message.author).serialize(), null, 4));
|
||||
@@ -57,7 +75,7 @@ client.on('message', message => {
|
||||
request
|
||||
.get('url')
|
||||
.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!'));
|
||||
});
|
||||
}
|
||||
@@ -65,11 +83,11 @@ client.on('message', message => {
|
||||
if (message.content.startsWith('gn')) {
|
||||
message.guild.setName(message.content.substr(3))
|
||||
.then(guild => console.log('guild updated to', guild.name))
|
||||
.catch(console.log);
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
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') {
|
||||
@@ -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.guilds.size} guilds 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!') {
|
||||
@@ -94,7 +112,7 @@ client.on('message', message => {
|
||||
message.guild.member(message.mentions[0]).kick().then(member => {
|
||||
console.log(member);
|
||||
message.channel.sendMessage('Kicked!' + member.user.username);
|
||||
}).catch(console.log);
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
if (message.content === 'ratelimittest') {
|
||||
@@ -108,17 +126,17 @@ client.on('message', message => {
|
||||
if (message.content === 'makerole') {
|
||||
message.guild.createRole().then(role => {
|
||||
message.channel.sendMessage(`Made role ${role.name}`);
|
||||
}).catch(console.log);
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
channel.setName(channel.name + 'a').then(chanLoop).catch(console.log);
|
||||
channel.setName(channel.name + 'a').then(chanLoop).catch(console.error);
|
||||
}
|
||||
|
||||
client.on('message', msg => {
|
||||
@@ -142,6 +160,7 @@ let disp, con;
|
||||
|
||||
client.on('message', msg => {
|
||||
if (msg.content.startsWith('/play')) {
|
||||
console.log('I am now going to play', msg.content);
|
||||
const chan = msg.content.split(' ').slice(1).join(' ');
|
||||
con.playStream(ytdl(chan, {filter : 'audioonly'}), { passes : 4 });
|
||||
}
|
||||
@@ -151,11 +170,10 @@ client.on('message', msg => {
|
||||
.then(conn => {
|
||||
con = conn;
|
||||
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('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 client = new Discord.Client({
|
||||
shard_id: process.argv[2],
|
||||
shard_count: process.argv[3],
|
||||
shardId: process.argv[2],
|
||||
shardCount: process.argv[3],
|
||||
});
|
||||
|
||||
client.on('message', msg => {
|
||||
@@ -20,7 +20,12 @@ client.on('message', msg => {
|
||||
process.send(123);
|
||||
|
||||
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 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}`));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user