mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-14 18:43:31 +01:00
Merge remote-tracking branch 'origin/indev' into indev-voice
This commit is contained in:
@@ -7,8 +7,8 @@ is a great boon to your coding process.
|
|||||||
To get ready to work on the codebase, please do the following:
|
To get ready to work on the codebase, please do the following:
|
||||||
|
|
||||||
1. Fork & clone the repository
|
1. Fork & clone the repository
|
||||||
2. Run `npm install`, or `npm install --no-optional` if you're not working on voice
|
2. Run `npm install`
|
||||||
3. Code your heart out!
|
3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript`
|
||||||
4. Run `npm test` to run ESLint
|
4. Code your heart out!
|
||||||
5. Run `npm run docs` to build any documentation changes
|
5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid
|
||||||
6. [Submit a pull request](https://github.com/hydrabolt/discord.js/compare)
|
6. [Submit a pull request](https://github.com/hydrabolt/discord.js/compare)
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -16,12 +16,14 @@ discord.js is a powerful node.js module that allows you to interact with the [Di
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
**Node.js 6.0.0 or newer is required.**
|
**Node.js 6.0.0 or newer is required.**
|
||||||
With voice support: `npm install --save discord.js --production`
|
Without voice support: `npm install discord.js --save`
|
||||||
Without voice support: `npm install --save discord.js --production --no-optional`
|
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||||
|
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||||
|
If both audio packages are installed, discord.js will automatically choose node-opus.
|
||||||
|
|
||||||
By default, discord.js uses [opusscript](https://www.npmjs.com/package/opusscript) when playing audio over voice connections.
|
The preferred audio engine is node-opus, as it performs significantly better than opusscript.
|
||||||
If you're looking to play over multiple voice connections, it might be better to install [node-opus](https://www.npmjs.com/package/node-opus).
|
Using opusscript is only recommended for development on Windows, since getting node-opus to build there can be a bit of a challenge.
|
||||||
discord.js will automatically prefer node-opus over opusscript.
|
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
```js
|
```js
|
||||||
@@ -41,17 +43,24 @@ client.on('message', message => {
|
|||||||
client.login('your token');
|
client.login('your token');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A bot template using discord.js can be generated using [generator-discordbot](https://www.npmjs.com/package/generator-discordbot).
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
* [Website](http://hydrabolt.github.io/discord.js/)
|
* [Website](http://hydrabolt.github.io/discord.js/)
|
||||||
* [Discord.js Server](https://discord.gg/bRCvFy9)
|
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||||
* [Discord API Server](https://discord.gg/rV4BwdK)
|
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||||
* [Documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master)
|
* [Documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master)
|
||||||
* [Legacy Documentation](http://discordjs.readthedocs.io/en/8.1.0/docs_client.html)
|
* [Legacy (v8) documentation](http://discordjs.readthedocs.io/en/8.2.0/docs_client.html)
|
||||||
|
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
|
||||||
* [GitHub](https://github.com/hydrabolt/discord.js)
|
* [GitHub](https://github.com/hydrabolt/discord.js)
|
||||||
* [NPM](https://www.npmjs.com/package/discord.js)
|
* [NPM](https://www.npmjs.com/package/discord.js)
|
||||||
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
|
* [Related libraries](https://discordapi.com/unofficial/libs.html)
|
||||||
* [Related Libraries](https://discordapi.com/unofficial/libs.html)
|
|
||||||
|
|
||||||
## Contact
|
## Contributing
|
||||||
Before reporting an issue, please read the [documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master).
|
Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the
|
||||||
If you can't find help there, you can ask in the official [Discord.js Server](https://discord.gg/bRCvFy9).
|
[documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master).
|
||||||
|
See [the contributing guide](CONTRIBUTING.md) if you'd like to submit a PR.
|
||||||
|
|
||||||
|
## Help
|
||||||
|
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||||
|
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ stable and performant than previous versions.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
**Node.js 6.0.0 or newer is required.**
|
**Node.js 6.0.0 or newer is required.**
|
||||||
With voice support: `npm install --save discord.js --production`
|
Without voice support: `npm install discord.js --save`
|
||||||
Without voice support: `npm install --save discord.js --production --no-optional`
|
With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save`
|
||||||
|
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save`
|
||||||
|
If both audio packages are installed, discord.js will automatically prefer node-opus.
|
||||||
|
|
||||||
By default, discord.js uses [opusscript](https://www.npmjs.com/package/opusscript) when playing audio over voice connections.
|
The preferred audio engine is node-opus, as it performs significantly better than opusscript.
|
||||||
If you're looking to play over multiple voice connections, it might be better to install [node-opus](https://www.npmjs.com/package/node-opus).
|
Using opusscript is only recommended for development on Windows, since getting node-opus to build there can be a bit of a challenge.
|
||||||
discord.js will automatically prefer node-opus over opusscript.
|
For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers.
|
||||||
|
|
||||||
## Guides
|
## Guides
|
||||||
* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/)
|
* [LuckyEvie's general guide](https://eslachance.gitbooks.io/discord-js-bot-guide/content/)
|
||||||
@@ -33,15 +35,15 @@ discord.js will automatically prefer node-opus over opusscript.
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
* [Website](http://hydrabolt.github.io/discord.js/)
|
* [Website](http://hydrabolt.github.io/discord.js/)
|
||||||
* [Discord.js Server](https://discord.gg/bRCvFy9)
|
* [Discord.js server](https://discord.gg/bRCvFy9)
|
||||||
* [Discord API Server](https://discord.gg/rV4BwdK)
|
* [Discord API server](https://discord.gg/rV4BwdK)
|
||||||
* [Documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master)
|
* [Documentation](http://hydrabolt.github.io/discord.js/#!/docs/tag/master)
|
||||||
* [Legacy Documentation](http://discordjs.readthedocs.io/en/8.1.0/docs_client.html)
|
* [Legacy (v8) documentation](http://discordjs.readthedocs.io/en/8.2.0/docs_client.html)
|
||||||
|
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
|
||||||
* [GitHub](https://github.com/hydrabolt/discord.js)
|
* [GitHub](https://github.com/hydrabolt/discord.js)
|
||||||
* [NPM](https://www.npmjs.com/package/discord.js)
|
* [NPM](https://www.npmjs.com/package/discord.js)
|
||||||
* [Examples](https://github.com/hydrabolt/discord.js/tree/master/docs/custom/examples)
|
* [Related libraries](https://discordapi.com/unofficial/libs.html)
|
||||||
* [Related Libraries](https://discordapi.com/unofficial/libs.html)
|
|
||||||
|
|
||||||
## Help
|
## Help
|
||||||
If you don't understand something in this documentation, you are experiencing problems, or you just need a gentle
|
If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle
|
||||||
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
nudge in the right direction, please don't hesitate to join our official [Discord.js Server](https://discord.gg/bRCvFy9).
|
||||||
|
|||||||
12
docs/custom/examples/webhook.js
Normal file
12
docs/custom/examples/webhook.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Send a message using a webhook
|
||||||
|
*/
|
||||||
|
|
||||||
|
// import the discord.js module
|
||||||
|
const Discord = require('discord.js');
|
||||||
|
|
||||||
|
// create a new webhook
|
||||||
|
const hook = new Discord.WebhookClient('webhook id', 'webhook token');
|
||||||
|
|
||||||
|
// send a message using the webhook
|
||||||
|
hook.sendMessage('I am now alive!');
|
||||||
10
docs/custom/webhook.js
Normal file
10
docs/custom/webhook.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
category: 'Examples',
|
||||||
|
name: 'Webhooks',
|
||||||
|
data:
|
||||||
|
`\`\`\`js
|
||||||
|
${fs.readFileSync('./docs/custom/examples/webhook.js').toString('utf-8')}
|
||||||
|
\`\`\``,
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
@@ -28,7 +28,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"superagent": "^2.2.0",
|
"superagent": "^2.2.0",
|
||||||
"tweetnacl": "^0.14.3",
|
"tweetnacl": "^0.14.3",
|
||||||
"ws": "^1.1.1",
|
"ws": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"node-opus": "^0.2.1",
|
||||||
"opusscript": "^0.0.1"
|
"opusscript": "^0.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -36,9 +39,6 @@
|
|||||||
"jsdoc-parse": "^1.2.0",
|
"jsdoc-parse": "^1.2.0",
|
||||||
"eslint": "^3.4.0"
|
"eslint": "^3.4.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
|
||||||
"node-opus": "^0.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,22 +20,19 @@ class Client extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* @param {ClientOptions} [options] Options for the client
|
* @param {ClientOptions} [options] Options for the client
|
||||||
*/
|
*/
|
||||||
constructor(options) {
|
constructor(options = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
// Obtain shard details from environment
|
||||||
|
if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID);
|
||||||
|
if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options the client was instantiated with
|
* The options the client was instantiated with
|
||||||
* @type {ClientOptions}
|
* @type {ClientOptions}
|
||||||
*/
|
*/
|
||||||
this.options = mergeDefault(Constants.DefaultOptions, options);
|
this.options = mergeDefault(Constants.DefaultOptions, options);
|
||||||
|
this._validateOptions();
|
||||||
if (!this.options.shardId && 'SHARD_ID' in process.env) {
|
|
||||||
this.options.shardId = Number(process.env.SHARD_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.options.shardCount && 'SHARD_COUNT' in process.env) {
|
|
||||||
this.options.shardCount = Number(process.env.SHARD_COUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The REST manager of the client
|
* The REST manager of the client
|
||||||
@@ -117,11 +114,15 @@ class Client extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
this.presences = new Collection();
|
this.presences = new Collection();
|
||||||
|
|
||||||
/**
|
if (!this.token && 'CLIENT_TOKEN' in process.env) {
|
||||||
* The authorization token for the logged in user/bot.
|
/**
|
||||||
* @type {?string}
|
* The authorization token for the logged in user/bot.
|
||||||
*/
|
* @type {?string}
|
||||||
this.token = null;
|
*/
|
||||||
|
this.token = process.env.CLIENT_TOKEN;
|
||||||
|
} else {
|
||||||
|
this.token = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The email, if there is one, for the logged in Client
|
* The email, if there is one, for the logged in Client
|
||||||
@@ -145,7 +146,7 @@ class Client extends EventEmitter {
|
|||||||
* The date at which the Client was regarded as being in the `READY` state.
|
* The date at which the Client was regarded as being in the `READY` state.
|
||||||
* @type {?Date}
|
* @type {?Date}
|
||||||
*/
|
*/
|
||||||
this.readyTime = null;
|
this.readyAt = null;
|
||||||
|
|
||||||
this._timeouts = new Set();
|
this._timeouts = new Set();
|
||||||
this._intervals = new Set();
|
this._intervals = new Set();
|
||||||
@@ -170,7 +171,7 @@ class Client extends EventEmitter {
|
|||||||
* @readonly
|
* @readonly
|
||||||
*/
|
*/
|
||||||
get uptime() {
|
get uptime() {
|
||||||
return this.readyTime ? Date.now() - this.readyTime : null;
|
return this.readyAt ? Date.now() - this.readyAt : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,6 +196,15 @@ class Client extends EventEmitter {
|
|||||||
return emojis;
|
return emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp that the client was last ready at
|
||||||
|
* @type {?number}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
get readyTimestamp() {
|
||||||
|
return this.readyAt ? this.readyAt.getTime() : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs the client in. If successful, resolves with the account's token. <warn>If you're making a bot, it's
|
* Logs the client in. If successful, resolves with the account's token. <warn>If you're making a bot, it's
|
||||||
* much better to use a bot account rather than a user account.
|
* much better to use a bot account rather than a user account.
|
||||||
@@ -241,13 +251,13 @@ class Client extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however
|
* This shouldn't really be necessary to most developers as it is automatically invoked every 30 seconds, however
|
||||||
* if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.
|
* if you wish to force a sync of Guild data, you can use this. Only applicable to user accounts.
|
||||||
* @param {Guild[]} [guilds=this.guilds.array()] An array of guilds to sync
|
* @param {Guild[]|Collection<string, Guild>} [guilds=this.guilds] An array or collection of guilds to sync
|
||||||
*/
|
*/
|
||||||
syncGuilds(guilds = this.guilds.array()) {
|
syncGuilds(guilds = this.guilds) {
|
||||||
if (!this.user.bot) {
|
if (!this.user.bot) {
|
||||||
this.ws.send({
|
this.ws.send({
|
||||||
op: 12,
|
op: 12,
|
||||||
d: guilds.map(g => g.id),
|
d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,13 +275,23 @@ class Client extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches an invite object from an invite code.
|
* Fetches an invite object from an invite code.
|
||||||
* @param {string} code the invite code.
|
* @param {InviteResolvable} invite An invite code or URL
|
||||||
* @returns {Promise<Invite>}
|
* @returns {Promise<Invite>}
|
||||||
*/
|
*/
|
||||||
fetchInvite(code) {
|
fetchInvite(invite) {
|
||||||
|
const code = this.resolver.resolveInviteCode(invite);
|
||||||
return this.rest.methods.getInvite(code);
|
return this.rest.methods.getInvite(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a webhook by ID.
|
||||||
|
* @param {string} id ID of the webhook
|
||||||
|
* @returns {Promise<Webhook>}
|
||||||
|
*/
|
||||||
|
fetchWebhook(id) {
|
||||||
|
return this.rest.methods.getWebhook(id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sweeps all channels' messages and removes the ones older than the max message lifetime.
|
* Sweeps all channels' messages and removes the ones older than the max message lifetime.
|
||||||
* If the message has been edited, the time of the edit is used rather than the time of the original message.
|
* If the message has been edited, the time of the edit is used rather than the time of the original message.
|
||||||
@@ -281,7 +301,7 @@ class Client extends EventEmitter {
|
|||||||
* or -1 if the message cache lifetime is unlimited
|
* or -1 if the message cache lifetime is unlimited
|
||||||
*/
|
*/
|
||||||
sweepMessages(lifetime = this.options.messageCacheLifetime) {
|
sweepMessages(lifetime = this.options.messageCacheLifetime) {
|
||||||
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('Lifetime must be a number.');
|
if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.');
|
||||||
if (lifetime <= 0) {
|
if (lifetime <= 0) {
|
||||||
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
|
this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited');
|
||||||
return -1;
|
return -1;
|
||||||
@@ -344,6 +364,39 @@ class Client extends EventEmitter {
|
|||||||
_eval(script) {
|
_eval(script) {
|
||||||
return eval(script);
|
return eval(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_validateOptions(options = this.options) {
|
||||||
|
if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) {
|
||||||
|
throw new TypeError('The shardCount option must be a number.');
|
||||||
|
}
|
||||||
|
if (typeof options.shardId !== 'number' || isNaN(options.shardId)) {
|
||||||
|
throw new TypeError('The shardId option must be a number.');
|
||||||
|
}
|
||||||
|
if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.');
|
||||||
|
if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.');
|
||||||
|
if (options.shardId !== 0 && options.shardId >= options.shardCount) {
|
||||||
|
throw new RangeError('The shardId option must be less than shardCount.');
|
||||||
|
}
|
||||||
|
if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) {
|
||||||
|
throw new TypeError('The messageCacheMaxSize option must be a number.');
|
||||||
|
}
|
||||||
|
if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) {
|
||||||
|
throw new TypeError('The messageCacheLifetime option must be a number.');
|
||||||
|
}
|
||||||
|
if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) {
|
||||||
|
throw new TypeError('The messageSweepInterval option must be a number.');
|
||||||
|
}
|
||||||
|
if (typeof options.fetchAllMembers !== 'boolean') {
|
||||||
|
throw new TypeError('The fetchAllMembers option must be a boolean.');
|
||||||
|
}
|
||||||
|
if (typeof options.disableEveryone !== 'boolean') {
|
||||||
|
throw new TypeError('The disableEveryone option must be a boolean.');
|
||||||
|
}
|
||||||
|
if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) {
|
||||||
|
throw new TypeError('The restWsBridgeTimeout option must be a number.');
|
||||||
|
}
|
||||||
|
if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Client;
|
module.exports = Client;
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ class ClientDataManager {
|
|||||||
updateChannel(currentChannel, newData) {
|
updateChannel(currentChannel, newData) {
|
||||||
currentChannel.setup(newData);
|
currentChannel.setup(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateEmoji(currentEmoji, newData) {
|
||||||
|
const oldEmoji = cloneObject(currentEmoji);
|
||||||
|
currentEmoji.setup(newData);
|
||||||
|
this.client.emit(Constants.Events.GUILD_EMOJI_UPDATE, oldEmoji, currentEmoji);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ClientDataManager;
|
module.exports = ClientDataManager;
|
||||||
|
|||||||
@@ -99,23 +99,6 @@ class ClientDataResolver {
|
|||||||
return guild.members.get(user.id) || null;
|
return guild.members.get(user.id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
|
|
||||||
* * A Buffer
|
|
||||||
* * A Base64 string
|
|
||||||
* @typedef {Buffer|string} Base64Resolvable
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a Base64Resolvable to a Base 64 image
|
|
||||||
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
|
|
||||||
* @returns {?string}
|
|
||||||
*/
|
|
||||||
resolveBase64(data) {
|
|
||||||
if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data that can be resolved to give a Channel. This can be:
|
* Data that can be resolved to give a Channel. This can be:
|
||||||
* * An instance of a Channel
|
* * An instance of a Channel
|
||||||
@@ -138,6 +121,26 @@ class ClientDataResolver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data that can be resolved to give an invite code. This can be:
|
||||||
|
* * An invite code
|
||||||
|
* * An invite URL
|
||||||
|
* @typedef {string} InviteResolvable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves InviteResolvable to an invite code
|
||||||
|
* @param {InviteResolvable} data The invite resolvable to resolve
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
resolveInviteCode(data) {
|
||||||
|
const inviteRegex = /discord(?:app)?\.(?:gg|com\/invite)\/([a-z0-9]{5})/i;
|
||||||
|
const match = inviteRegex.exec(data);
|
||||||
|
|
||||||
|
if (match && match[1]) return match[1];
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data that can be resolved to give a permission number. This can be:
|
* Data that can be resolved to give a permission number. This can be:
|
||||||
* * A string
|
* * A string
|
||||||
@@ -205,6 +208,23 @@ class ClientDataResolver {
|
|||||||
return String(data);
|
return String(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data that resolves to give a Base64 string, typically for image uploading. This can be:
|
||||||
|
* * A Buffer
|
||||||
|
* * A Base64 string
|
||||||
|
* @typedef {Buffer|string} Base64Resolvable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a Base64Resolvable to a Base 64 image
|
||||||
|
* @param {Base64Resolvable} data The base 64 resolvable you want to resolve
|
||||||
|
* @returns {?string}
|
||||||
|
*/
|
||||||
|
resolveBase64(data) {
|
||||||
|
if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data that can be resolved to give a Buffer. This can be:
|
* Data that can be resolved to give a Buffer. This can be:
|
||||||
* * A Buffer
|
* * A Buffer
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class ClientManager {
|
|||||||
*/
|
*/
|
||||||
setupKeepAlive(time) {
|
setupKeepAlive(time) {
|
||||||
this.heartbeatInterval = this.client.setInterval(() => {
|
this.heartbeatInterval = this.client.setInterval(() => {
|
||||||
|
this.client.emit('debug', 'Sending heartbeat');
|
||||||
this.client.ws.send({
|
this.client.ws.send({
|
||||||
op: Constants.OPCodes.HEARTBEAT,
|
op: Constants.OPCodes.HEARTBEAT,
|
||||||
d: this.client.ws.sequence,
|
d: this.client.ws.sequence,
|
||||||
|
|||||||
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;
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
const Action = require('./Action');
|
const Action = require('./Action');
|
||||||
const Constants = require('../../util/Constants');
|
|
||||||
|
|
||||||
class GuildEmojiUpdateAction extends Action {
|
class GuildEmojiUpdateAction extends Action {
|
||||||
handle(data, guild) {
|
handle(data, guild) {
|
||||||
const client = this.client;
|
const client = this.client;
|
||||||
for (let emoji of data.emojis) {
|
for (let emoji of data.emojis) {
|
||||||
const already = guild.emojis.has(emoji.id);
|
const already = guild.emojis.has(emoji.id);
|
||||||
emoji = client.dataManager.newEmoji(emoji, guild);
|
if (already) {
|
||||||
if (already) client.emit(Constants.Events.GUILD_EMOJI_UPDATE, guild, emoji);
|
client.dataManager.updateEmoji(guild.emojis.get(emoji.id), emoji);
|
||||||
|
} else {
|
||||||
|
emoji = client.dataManager.newEmoji(emoji, guild);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (let emoji of guild.emojis) {
|
for (let emoji of guild.emojis) {
|
||||||
if (!data.emoijs.has(emoji.id)) client.dataManager.killEmoji(emoji);
|
if (!data.emoijs.has(emoji.id)) client.dataManager.killEmoji(emoji);
|
||||||
@@ -21,7 +23,7 @@ class GuildEmojiUpdateAction extends Action {
|
|||||||
/**
|
/**
|
||||||
* Emitted whenever an emoji is updated
|
* Emitted whenever an emoji is updated
|
||||||
* @event Client#guildEmojiUpdate
|
* @event Client#guildEmojiUpdate
|
||||||
* @param {Guild} guild The guild that the emoji was updated in.
|
* @param {Emoji} oldEmoji The old emoji
|
||||||
* @param {Emoji} emoji The emoji that was updated.
|
* @param {Emoji} newEmoji The new emoji
|
||||||
*/
|
*/
|
||||||
module.exports = GuildEmojiUpdateAction;
|
module.exports = GuildEmojiUpdateAction;
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ const User = requireStructure('User');
|
|||||||
const GuildMember = requireStructure('GuildMember');
|
const GuildMember = requireStructure('GuildMember');
|
||||||
const Role = requireStructure('Role');
|
const Role = requireStructure('Role');
|
||||||
const Invite = requireStructure('Invite');
|
const Invite = requireStructure('Invite');
|
||||||
|
const Webhook = requireStructure('Webhook');
|
||||||
|
|
||||||
class RESTMethods {
|
class RESTMethods {
|
||||||
constructor(restManager) {
|
constructor(restManager) {
|
||||||
this.rest = restManager;
|
this.rest = restManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
loginToken(token) {
|
loginToken(token = this.rest.client.token) {
|
||||||
|
token = token.replace(/^Bot\s*/i, '');
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.rest.client.manager.connectToWebSocket(token, resolve, reject);
|
this.rest.client.manager.connectToWebSocket(token, resolve, reject);
|
||||||
});
|
});
|
||||||
@@ -26,7 +28,7 @@ class RESTMethods {
|
|||||||
this.rest.client.password = password;
|
this.rest.client.password = password;
|
||||||
this.rest.makeRequest('post', Constants.Endpoints.login, false, { email, password })
|
this.rest.makeRequest('post', Constants.Endpoints.login, false, { email, password })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.rest.client.manager.connectToWebSocket(data.token, resolve, reject);
|
resolve(this.loginToken(data.token));
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
@@ -47,21 +49,26 @@ class RESTMethods {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBotGateway() {
|
||||||
|
return this.rest.makeRequest('get', Constants.Endpoints.botGateway, true);
|
||||||
|
}
|
||||||
|
|
||||||
sendMessage(channel, content, { tts, nonce, disableEveryone, split } = {}, file = null) {
|
sendMessage(channel, content, { tts, nonce, disableEveryone, split } = {}, file = null) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
|
if (typeof content !== 'undefined') content = this.rest.client.resolver.resolveString(content);
|
||||||
|
|
||||||
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
|
if (content) {
|
||||||
content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
|
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.rest.client.options.disableEveryone)) {
|
||||||
}
|
content = content.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere');
|
||||||
|
}
|
||||||
|
|
||||||
if (split) content = splitMessage(content, typeof split === 'object' ? split : {});
|
if (split) content = splitMessage(content, typeof split === 'object' ? split : {});
|
||||||
|
}
|
||||||
|
|
||||||
if (channel instanceof User || channel instanceof GuildMember) {
|
if (channel instanceof User || channel instanceof GuildMember) {
|
||||||
this.createDM(channel).then(chan => {
|
this.createDM(channel).then(chan => {
|
||||||
this._sendMessageRequest(chan, content, file, tts, nonce, resolve, reject);
|
this._sendMessageRequest(chan, content, file, tts, nonce, resolve, reject);
|
||||||
})
|
}).catch(reject);
|
||||||
.catch(reject);
|
|
||||||
} else {
|
} else {
|
||||||
this._sendMessageRequest(channel, content, file, tts, nonce, resolve, reject);
|
this._sendMessageRequest(channel, content, file, tts, nonce, resolve, reject);
|
||||||
}
|
}
|
||||||
@@ -71,22 +78,24 @@ class RESTMethods {
|
|||||||
_sendMessageRequest(channel, content, file, tts, nonce, resolve, reject) {
|
_sendMessageRequest(channel, content, file, tts, nonce, resolve, reject) {
|
||||||
if (content instanceof Array) {
|
if (content instanceof Array) {
|
||||||
const datas = [];
|
const datas = [];
|
||||||
const promise = this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
let promise = this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
||||||
content: content[0], tts, nonce,
|
content: content[0], tts, nonce,
|
||||||
}, file).catch(reject);
|
}, file).catch(reject);
|
||||||
|
|
||||||
for (let i = 1; i <= content.length; i++) {
|
for (let i = 1; i <= content.length; i++) {
|
||||||
if (i < content.length) {
|
if (i < content.length) {
|
||||||
promise.then(data => {
|
const i2 = i;
|
||||||
|
promise = promise.then(data => {
|
||||||
datas.push(data);
|
datas.push(data);
|
||||||
return this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
return this.rest.makeRequest('post', Constants.Endpoints.channelMessages(channel.id), true, {
|
||||||
content: content[i], tts, nonce,
|
content: content[i2], tts, nonce,
|
||||||
}, file);
|
}, file);
|
||||||
});
|
}).catch(reject);
|
||||||
} else {
|
} else {
|
||||||
promise.then(data => {
|
promise.then(data => {
|
||||||
datas.push(data);
|
datas.push(data);
|
||||||
resolve(this.rest.client.actions.MessageCreate.handle(datas).messages);
|
resolve(this.rest.client.actions.MessageCreate.handle(datas).messages);
|
||||||
});
|
}).catch(reject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -535,6 +544,138 @@ class RESTMethods {
|
|||||||
}).catch(reject);
|
}).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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = RESTMethods;
|
module.exports = RESTMethods;
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ class WebSocketManager extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object with keys that are websocket event names that should be ignored
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
this.disabledEvents = {};
|
||||||
|
for (const event in client.options.disabledEvents) this.disabledEvents[event] = true;
|
||||||
|
|
||||||
this.first = true;
|
this.first = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +76,7 @@ class WebSocketManager extends EventEmitter {
|
|||||||
_connect(gateway) {
|
_connect(gateway) {
|
||||||
this.client.emit('debug', `Connecting to gateway ${gateway}`);
|
this.client.emit('debug', `Connecting to gateway ${gateway}`);
|
||||||
this.normalReady = false;
|
this.normalReady = false;
|
||||||
if (this.status !== Constants.Status.RECONNECTING) {
|
if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING;
|
||||||
this.status = Constants.Status.CONNECTING;
|
|
||||||
}
|
|
||||||
this.ws = new WebSocket(gateway);
|
this.ws = new WebSocket(gateway);
|
||||||
this.ws.onopen = () => this.eventOpen();
|
this.ws.onopen = () => this.eventOpen();
|
||||||
this.ws.onclose = (d) => this.eventClose(d);
|
this.ws.onclose = (d) => this.eventClose(d);
|
||||||
@@ -216,7 +221,7 @@ class WebSocketManager extends EventEmitter {
|
|||||||
|
|
||||||
this.client.emit('raw', packet);
|
this.client.emit('raw', packet);
|
||||||
|
|
||||||
if (packet.op === 10) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
|
if (packet.op === Constants.OPCodes.HELLO) this.client.manager.setupKeepAlive(packet.d.heartbeat_interval);
|
||||||
return this.packetManager.handle(packet);
|
return this.packetManager.handle(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,9 +263,10 @@ class WebSocketManager extends EventEmitter {
|
|||||||
if (unavailableCount === 0) {
|
if (unavailableCount === 0) {
|
||||||
this.status = Constants.Status.NEARLY;
|
this.status = Constants.Status.NEARLY;
|
||||||
if (this.client.options.fetchAllMembers) {
|
if (this.client.options.fetchAllMembers) {
|
||||||
const promises = this.client.guilds.array().map(g => g.fetchMembers());
|
const promises = this.client.guilds.map(g => g.fetchMembers());
|
||||||
Promise.all(promises).then(() => this._emitReady()).catch(e => {
|
Promise.all(promises).then(() => this._emitReady()).catch(e => {
|
||||||
this.client.emit(Constants.Event.WARN, `Error on pre-ready guild member fetching - ${e}`);
|
this.client.emit(Constants.Events.WARN, 'Error in pre-ready guild member fetching');
|
||||||
|
this.client.emit(Constants.Events.ERROR, e);
|
||||||
this._emitReady();
|
this._emitReady();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ class WebSocketPacketManager {
|
|||||||
this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk');
|
this.register(Constants.WSEvents.MESSAGE_DELETE_BULK, 'MessageDeleteBulk');
|
||||||
this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate');
|
this.register(Constants.WSEvents.CHANNEL_PINS_UPDATE, 'ChannelPinsUpdate');
|
||||||
this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync');
|
this.register(Constants.WSEvents.GUILD_SYNC, 'GuildSync');
|
||||||
|
this.register(Constants.WSEvents.RELATIONSHIP_ADD, 'RelationshipAdd');
|
||||||
|
this.register(Constants.WSEvents.RELATIONSHIP_REMOVE, 'RelationshipRemove');
|
||||||
}
|
}
|
||||||
|
|
||||||
get client() {
|
get client() {
|
||||||
@@ -77,6 +79,8 @@ class WebSocketPacketManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (packet.op === Constants.OPCodes.HEARTBEAT_ACK) this.ws.client.emit('debug', 'Heartbeat acknowledged');
|
||||||
|
|
||||||
if (this.ws.status === Constants.Status.RECONNECTING) {
|
if (this.ws.status === Constants.Status.RECONNECTING) {
|
||||||
this.ws.reconnecting = false;
|
this.ws.reconnecting = false;
|
||||||
this.ws.checkIfReady();
|
this.ws.checkIfReady();
|
||||||
@@ -84,6 +88,8 @@ class WebSocketPacketManager {
|
|||||||
|
|
||||||
this.setSequence(packet.s);
|
this.setSequence(packet.s);
|
||||||
|
|
||||||
|
if (this.ws.disabledEvents[packet.t] !== undefined) return false;
|
||||||
|
|
||||||
if (this.ws.status !== Constants.Status.READY) {
|
if (this.ws.status !== Constants.Status.READY) {
|
||||||
if (BeforeReadyWhitelist.indexOf(packet.t) === -1) {
|
if (BeforeReadyWhitelist.indexOf(packet.t) === -1) {
|
||||||
this.queue.push(packet);
|
this.queue.push(packet);
|
||||||
|
|||||||
@@ -10,12 +10,21 @@ class ReadyHandler extends AbstractHandler {
|
|||||||
|
|
||||||
const clientUser = new ClientUser(client, data.user);
|
const clientUser = new ClientUser(client, data.user);
|
||||||
client.user = clientUser;
|
client.user = clientUser;
|
||||||
client.readyTime = new Date();
|
client.readyAt = new Date();
|
||||||
client.users.set(clientUser.id, clientUser);
|
client.users.set(clientUser.id, clientUser);
|
||||||
|
|
||||||
for (const guild of data.guilds) client.dataManager.newGuild(guild);
|
for (const guild of data.guilds) client.dataManager.newGuild(guild);
|
||||||
for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM);
|
for (const privateDM of data.private_channels) client.dataManager.newChannel(privateDM);
|
||||||
|
|
||||||
|
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 || [];
|
data.presences = data.presences || [];
|
||||||
for (const presence of data.presences) {
|
for (const presence of data.presences) {
|
||||||
client.dataManager.newUser(presence.user);
|
client.dataManager.newUser(presence.user);
|
||||||
|
|||||||
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;
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
Client: require('./client/Client'),
|
Client: require('./client/Client'),
|
||||||
|
WebhookClient: require('./client/WebhookClient'),
|
||||||
Shard: require('./sharding/Shard'),
|
Shard: require('./sharding/Shard'),
|
||||||
ShardClientUtil: require('./sharding/ShardClientUtil'),
|
ShardClientUtil: require('./sharding/ShardClientUtil'),
|
||||||
ShardingManager: require('./sharding/ShardingManager'),
|
ShardingManager: require('./sharding/ShardingManager'),
|
||||||
|
|
||||||
Collection: require('./util/Collection'),
|
Collection: require('./util/Collection'),
|
||||||
|
splitMessage: require('./util/SplitMessage'),
|
||||||
|
escapeMarkdown: require('./util/EscapeMarkdown'),
|
||||||
|
getRecommendedShards: require('./util/GetRecommendedShards'),
|
||||||
|
|
||||||
Channel: require('./structures/Channel'),
|
Channel: require('./structures/Channel'),
|
||||||
ClientUser: require('./structures/ClientUser'),
|
ClientUser: require('./structures/ClientUser'),
|
||||||
@@ -28,6 +33,7 @@ module.exports = {
|
|||||||
TextChannel: require('./structures/TextChannel'),
|
TextChannel: require('./structures/TextChannel'),
|
||||||
User: require('./structures/User'),
|
User: require('./structures/User'),
|
||||||
VoiceChannel: require('./structures/VoiceChannel'),
|
VoiceChannel: require('./structures/VoiceChannel'),
|
||||||
|
Webhook: require('./structures/Webhook'),
|
||||||
|
|
||||||
version: require('../package').version,
|
version: require('../package').version,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ class Shard {
|
|||||||
/**
|
/**
|
||||||
* @param {ShardingManager} manager The sharding manager
|
* @param {ShardingManager} manager The sharding manager
|
||||||
* @param {number} id The ID of this shard
|
* @param {number} id The ID of this shard
|
||||||
|
* @param {array} [args=[]] Command line arguments to pass to the script
|
||||||
*/
|
*/
|
||||||
constructor(manager, id) {
|
constructor(manager, id, args = []) {
|
||||||
/**
|
/**
|
||||||
* Manager that created the shard
|
* Manager that created the shard
|
||||||
* @type {ShardingManager}
|
* @type {ShardingManager}
|
||||||
@@ -24,15 +25,22 @@ class Shard {
|
|||||||
*/
|
*/
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The environment variables for the shard
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
this.env = {
|
||||||
|
SHARD_ID: this.id,
|
||||||
|
SHARD_COUNT: this.manager.totalShards,
|
||||||
|
};
|
||||||
|
if (this.manager.token) this.env.CLIENT_TOKEN = this.manager.token;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process of the shard
|
* Process of the shard
|
||||||
* @type {ChildProcess}
|
* @type {ChildProcess}
|
||||||
*/
|
*/
|
||||||
this.process = childProcess.fork(path.resolve(this.manager.file), [], {
|
this.process = childProcess.fork(path.resolve(this.manager.file), args, {
|
||||||
env: {
|
env: this.env,
|
||||||
SHARD_ID: this.id,
|
|
||||||
SHARD_COUNT: this.manager.totalShards,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
this.process.on('message', this._handleMessage.bind(this));
|
this.process.on('message', this._handleMessage.bind(this));
|
||||||
this.process.once('exit', () => {
|
this.process.once('exit', () => {
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const EventEmitter = require('events').EventEmitter;
|
const EventEmitter = require('events').EventEmitter;
|
||||||
|
const mergeDefault = require('../util/MergeDefault');
|
||||||
const Shard = require('./Shard');
|
const Shard = require('./Shard');
|
||||||
const Collection = require('../util/Collection');
|
const Collection = require('../util/Collection');
|
||||||
|
const getRecommendedShards = require('../util/GetRecommendedShards');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a utility class that can be used to help you spawn shards of your Client. Each shard is completely separate
|
* This is a utility class that can be used to help you spawn shards of your Client. Each shard is completely separate
|
||||||
* from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.
|
* from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.
|
||||||
|
* If you do not select an amount of shards, the manager will automatically decide the best amount.
|
||||||
* <warn>The Sharding Manager is still experimental</warn>
|
* <warn>The Sharding Manager is still experimental</warn>
|
||||||
* @extends {EventEmitter}
|
* @extends {EventEmitter}
|
||||||
*/
|
*/
|
||||||
class ShardingManager extends EventEmitter {
|
class ShardingManager extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @param {string} file Path to your shard script file
|
* @param {string} file Path to your shard script file
|
||||||
* @param {number} [totalShards=1] Number of shards to default to spawning
|
* @param {Object} [options] Options for the sharding manager
|
||||||
* @param {boolean} [respawn=true] Respawn a shard when it dies
|
* @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto"
|
||||||
|
* @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting
|
||||||
|
* @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning
|
||||||
|
* @param {string} [options.token] Token to use for automatic shard count and passing to shards
|
||||||
*/
|
*/
|
||||||
constructor(file, totalShards = 1, respawn = true) {
|
constructor(file, options = {}) {
|
||||||
super();
|
super();
|
||||||
|
options = mergeDefault({
|
||||||
|
totalShards: 'auto',
|
||||||
|
respawn: true,
|
||||||
|
shardArgs: [],
|
||||||
|
token: null,
|
||||||
|
}, options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the shard script file
|
* Path to the shard script file
|
||||||
@@ -32,20 +43,36 @@ class ShardingManager extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount of shards that this manager is going to spawn
|
* Amount of shards that this manager is going to spawn
|
||||||
* @type {number}
|
* @type {number|string}
|
||||||
*/
|
*/
|
||||||
this.totalShards = totalShards;
|
this.totalShards = options.totalShards;
|
||||||
if (typeof totalShards !== 'number' || isNaN(totalShards)) {
|
if (this.totalShards !== 'auto') {
|
||||||
throw new TypeError('Amount of shards must be a number.');
|
if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) {
|
||||||
|
throw new TypeError('Amount of shards must be a number.');
|
||||||
|
}
|
||||||
|
if (this.totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||||
|
if (this.totalShards !== Math.floor(this.totalShards)) {
|
||||||
|
throw new RangeError('Amount of shards must be an integer.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (totalShards < 1) throw new RangeError('Amount of shards must be at least 1.');
|
|
||||||
if (totalShards !== Math.floor(totalShards)) throw new RangeError('Amount of shards must be an integer.');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether shards should automatically respawn upon exiting
|
* Whether shards should automatically respawn upon exiting
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.respawn = respawn;
|
this.respawn = options.respawn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of arguments to pass to shards.
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
this.shardArgs = options.shardArgs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token to use for obtaining the automatic shard count, and passing to shards
|
||||||
|
* @type {?string}
|
||||||
|
*/
|
||||||
|
this.token = options.token ? options.token.replace(/^Bot\s*/i, '') : null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A collection of shards that this manager has spawned
|
* A collection of shards that this manager has spawned
|
||||||
@@ -60,7 +87,7 @@ class ShardingManager extends EventEmitter {
|
|||||||
* @returns {Promise<Shard>}
|
* @returns {Promise<Shard>}
|
||||||
*/
|
*/
|
||||||
createShard(id = this.shards.size) {
|
createShard(id = this.shards.size) {
|
||||||
const shard = new Shard(this, id);
|
const shard = new Shard(this, id, this.shardArgs);
|
||||||
this.shards.set(id, shard);
|
this.shards.set(id, shard);
|
||||||
/**
|
/**
|
||||||
* Emitted upon launching a shard
|
* Emitted upon launching a shard
|
||||||
@@ -78,10 +105,29 @@ class ShardingManager extends EventEmitter {
|
|||||||
* @returns {Promise<Collection<number, Shard>>}
|
* @returns {Promise<Collection<number, Shard>>}
|
||||||
*/
|
*/
|
||||||
spawn(amount = this.totalShards, delay = 5500) {
|
spawn(amount = this.totalShards, delay = 5500) {
|
||||||
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
|
return new Promise((resolve, reject) => {
|
||||||
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
|
if (amount === 'auto') {
|
||||||
if (amount !== Math.floor(amount)) throw new RangeError('Amount of shards must be an integer.');
|
getRecommendedShards(this.token).then(count => {
|
||||||
|
this.totalShards = count;
|
||||||
|
resolve(this._spawn(count, delay));
|
||||||
|
}).catch(reject);
|
||||||
|
} else {
|
||||||
|
if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.');
|
||||||
|
if (amount < 1) throw new RangeError('Amount of shards must be at least 1.');
|
||||||
|
if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.');
|
||||||
|
resolve(this._spawn(amount, delay));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually spawns shards, unlike that poser above >:(
|
||||||
|
* @param {number} amount Number of shards to spawn
|
||||||
|
* @param {number} delay How long to wait in between spawning each shard (in milliseconds)
|
||||||
|
* @returns {Promise<Collection<number, Shard>>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_spawn(amount, delay) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
|
if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`);
|
||||||
this.totalShards = amount;
|
this.totalShards = amount;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
|
const Collection = require('../util/Collection');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the logged in client's Discord User
|
* Represents the logged in client's Discord User
|
||||||
@@ -21,6 +22,20 @@ class ClientUser extends User {
|
|||||||
this.email = data.email;
|
this.email = data.email;
|
||||||
this.localPresence = {};
|
this.localPresence = {};
|
||||||
this._typing = new Map();
|
this._typing = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Collection of friends for the logged in user.
|
||||||
|
* <warn>This is only filled for user accounts, not bot accounts!</warn>
|
||||||
|
* @type {Collection<string, User>}
|
||||||
|
*/
|
||||||
|
this.friends = new Collection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Collection of blocked users for the logged in user.
|
||||||
|
* <warn>This is only filled for user accounts, not bot accounts!</warn>
|
||||||
|
* @type {Collection<string, User>}
|
||||||
|
*/
|
||||||
|
this.blocked = new Collection();
|
||||||
}
|
}
|
||||||
|
|
||||||
edit(data) {
|
edit(data) {
|
||||||
@@ -118,6 +133,28 @@ class ClientUser extends User {
|
|||||||
return this.setPresence({ 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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the full presence of the current user.
|
* Set the full presence of the current user.
|
||||||
* @param {Object} data the data to provide
|
* @param {Object} data the data to provide
|
||||||
@@ -125,12 +162,12 @@ class ClientUser extends User {
|
|||||||
*/
|
*/
|
||||||
setPresence(data) {
|
setPresence(data) {
|
||||||
// {"op":3,"d":{"status":"dnd","since":0,"game":null,"afk":false}}
|
// {"op":3,"d":{"status":"dnd","since":0,"game":null,"afk":false}}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
let status = this.localPresence.status || this.presence.status;
|
let status = this.localPresence.status || this.presence.status;
|
||||||
let game = this.localPresence.game;
|
let game = this.localPresence.game;
|
||||||
let afk = this.localPresence.afk || this.presence.afk;
|
let afk = this.localPresence.afk || this.presence.afk;
|
||||||
|
|
||||||
if (!game) {
|
if (!game && this.presence.game) {
|
||||||
game = {
|
game = {
|
||||||
name: this.presence.game.name,
|
name: this.presence.game.name,
|
||||||
type: this.presence.game.type,
|
type: this.presence.game.type,
|
||||||
@@ -139,10 +176,7 @@ class ClientUser extends User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.status) {
|
if (data.status) {
|
||||||
if (typeof data.status !== 'string') {
|
if (typeof data.status !== 'string') throw new TypeError('Status must be a string');
|
||||||
reject(new TypeError('status must be a string'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
status = data.status;
|
status = data.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class Emoji {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
* @example
|
* @example
|
||||||
* // send an emoji:
|
* // send an emoji:
|
||||||
* const emoji = guild.emojis.array()[0];
|
* const emoji = guild.emojis.first();
|
||||||
* msg.reply(`Hello! ${emoji}`);
|
* msg.reply(`Hello! ${emoji}`);
|
||||||
*/
|
*/
|
||||||
toString() {
|
toString() {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class EvaluatedPermissions {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
hasPermissions(permissions, explicit = false) {
|
hasPermissions(permissions, explicit = false) {
|
||||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
return permissions.every(p => this.hasPermission(p, explicit));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ class GroupDMChannel extends Channel {
|
|||||||
this.ownerID === channel.ownerID;
|
this.ownerID === channel.ownerID;
|
||||||
|
|
||||||
if (equal) {
|
if (equal) {
|
||||||
const thisIDs = this.recipients.array().map(r => r.id);
|
const thisIDs = this.recipients.keyArray();
|
||||||
const otherIDs = channel.recipients.map(r => r.id);
|
const otherIDs = channel.recipients.keyArray();
|
||||||
return arraysEqual(thisIDs, otherIDs);
|
return arraysEqual(thisIDs, otherIDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -296,6 +296,14 @@ class Guild {
|
|||||||
return this.client.rest.methods.getGuildInvites(this);
|
return this.client.rest.methods.getGuildInvites(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all webhooks for the guild.
|
||||||
|
* @returns {Collection<Webhook>}
|
||||||
|
*/
|
||||||
|
fetchWebhooks() {
|
||||||
|
return this.client.rest.methods.getGuildWebhooks(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a single guild member from a user.
|
* Fetch a single guild member from a user.
|
||||||
* @param {UserResolvable} user The user to fetch the member for
|
* @param {UserResolvable} user The user to fetch the member for
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class GuildChannel extends Channel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Overwrites the permissions for a user or role in this channel.
|
* Overwrites the permissions for a user or role in this channel.
|
||||||
* @param {Role|UserResolvable} userOrRole The user or role to update
|
* @param {RoleResolvable|UserResolvable} userOrRole The user or role to update
|
||||||
* @param {PermissionOverwriteOptions} options The configuration for the update
|
* @param {PermissionOverwriteOptions} options The configuration for the update
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
* @example
|
* @example
|
||||||
@@ -130,6 +130,9 @@ class GuildChannel extends Channel {
|
|||||||
|
|
||||||
if (userOrRole instanceof Role) {
|
if (userOrRole instanceof Role) {
|
||||||
payload.type = 'role';
|
payload.type = 'role';
|
||||||
|
} else if (this.guild.roles.has(userOrRole)) {
|
||||||
|
userOrRole = this.guild.roles.get(userOrRole);
|
||||||
|
payload.type = 'role';
|
||||||
} else {
|
} else {
|
||||||
userOrRole = this.client.resolver.resolveUser(userOrRole);
|
userOrRole = this.client.resolver.resolveUser(userOrRole);
|
||||||
payload.type = 'member';
|
payload.type = 'member';
|
||||||
@@ -236,12 +239,12 @@ class GuildChannel extends Channel {
|
|||||||
this.name === channel.name;
|
this.name === channel.name;
|
||||||
|
|
||||||
if (equal) {
|
if (equal) {
|
||||||
if (channel.permission_overwrites) {
|
if (this.permissionOverwrites && channel.permissionOverwrites) {
|
||||||
const thisIDSet = Array.from(this.permissionOverwrites.keys());
|
const thisIDSet = this.permissionOverwrites.keyArray();
|
||||||
const otherIDSet = channel.permission_overwrites.map(overwrite => overwrite.id);
|
const otherIDSet = channel.permissionOverwrites.keyArray();
|
||||||
equal = arraysEqual(thisIDSet, otherIDSet);
|
equal = arraysEqual(thisIDSet, otherIDSet);
|
||||||
} else {
|
} else {
|
||||||
equal = false;
|
equal = !this.permissionOverwrites && !channel.permissionOverwrites;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ class GuildMember {
|
|||||||
*/
|
*/
|
||||||
hasPermissions(permissions, explicit = false) {
|
hasPermissions(permissions, explicit = false) {
|
||||||
if (!explicit && this.user.id === this.guild.ownerID) return true;
|
if (!explicit && this.user.id === this.guild.ownerID) return true;
|
||||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
return permissions.every(p => this.hasPermission(p, explicit));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -407,7 +407,7 @@ class GuildMember {
|
|||||||
* console.log(`Hello from ${member}!`);
|
* console.log(`Hello from ${member}!`);
|
||||||
*/
|
*/
|
||||||
toString() {
|
toString() {
|
||||||
return String(this.user);
|
return `<@${this.nickname ? '!' : ''}${this.user.id}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const Attachment = require('./MessageAttachment');
|
|||||||
const Embed = require('./MessageEmbed');
|
const Embed = require('./MessageEmbed');
|
||||||
const Collection = require('../util/Collection');
|
const Collection = require('../util/Collection');
|
||||||
const Constants = require('../util/Constants');
|
const Constants = require('../util/Constants');
|
||||||
|
const escapeMarkdown = require('../util/EscapeMarkdown');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Message on Discord
|
* Represents a Message on Discord
|
||||||
@@ -31,6 +32,12 @@ class Message {
|
|||||||
*/
|
*/
|
||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the message
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.type = Constants.MessageTypes[data.type];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The content of the message
|
* The content of the message
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -332,8 +339,8 @@ class Message {
|
|||||||
* @returns {Promise<Message>}
|
* @returns {Promise<Message>}
|
||||||
*/
|
*/
|
||||||
editCode(lang, content) {
|
editCode(lang, content) {
|
||||||
content = this.client.resolver.resolveString(content).replace(/```/g, '`\u200b``');
|
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||||
return this.edit(`\`\`\`${lang ? lang : ''}\n${content}\n\`\`\``);
|
return this.edit(`\`\`\`${lang || ''}\n${content}\n\`\`\``);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class MessageAttachment {
|
|||||||
* The Proxy URL to this attachment
|
* The Proxy URL to this attachment
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
this.proxyURL = data.url;
|
this.proxyURL = data.proxy_url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The height of this attachment (if an image)
|
* The height of this attachment (if an image)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class MessageCollector extends EventEmitter {
|
|||||||
* @typedef {Object} CollectorOptions
|
* @typedef {Object} CollectorOptions
|
||||||
* @property {number} [time] Duration for the collector in milliseconds
|
* @property {number} [time] Duration for the collector in milliseconds
|
||||||
* @property {number} [max] Maximum number of messages to handle
|
* @property {number} [max] Maximum number of messages to handle
|
||||||
|
* @property {number} [maxMatches] Maximum number of successfully filtered messages to obtain
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,7 +87,8 @@ class MessageCollector extends EventEmitter {
|
|||||||
* @event MessageCollector#message
|
* @event MessageCollector#message
|
||||||
*/
|
*/
|
||||||
this.emit('message', message, this);
|
this.emit('message', message, this);
|
||||||
if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
|
if (this.collected.size >= this.options.maxMatches) this.stop('matchesLimit');
|
||||||
|
else if (this.options.max && this.collected.size === this.options.max) this.stop('limit');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -100,9 +102,14 @@ class MessageCollector extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
get next() {
|
get next() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.ended) {
|
||||||
|
reject(this.collected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
this.removeListener(onMessage);
|
this.removeListener('message', onMessage);
|
||||||
this.removeListener(onEnd);
|
this.removeListener('end', onEnd);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMessage = (...args) => {
|
const onMessage = (...args) => {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class Role {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
hasPermissions(permissions, explicit = false) {
|
hasPermissions(permissions, explicit = false) {
|
||||||
return permissions.map(p => this.hasPermission(p, explicit)).every(v => v);
|
return permissions.every(p => this.hasPermission(p, explicit));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -42,6 +42,38 @@ class TextChannel extends GuildChannel {
|
|||||||
return members;
|
return members;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all webhooks for the channel.
|
||||||
|
* @returns {Promise<Collection<string, Webhook>>}
|
||||||
|
*/
|
||||||
|
fetchWebhooks() {
|
||||||
|
return this.client.rest.methods.getChannelWebhooks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a webhook for the channel.
|
||||||
|
* @param {string} name The name of the webhook.
|
||||||
|
* @param {FileResolvable} avatar The avatar for the webhook.
|
||||||
|
* @returns {Promise<Webhook>} webhook The created webhook.
|
||||||
|
* @example
|
||||||
|
* channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png')
|
||||||
|
* .then(webhook => console.log(`Created Webhook ${webhook}`))
|
||||||
|
* .catch(console.log)
|
||||||
|
*/
|
||||||
|
createWebhook(name, avatar) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (avatar) {
|
||||||
|
this.client.resolver.resolveFile(avatar).then(file => {
|
||||||
|
let base64 = new Buffer(file, 'binary').toString('base64');
|
||||||
|
let dataURI = `data:;base64,${base64}`;
|
||||||
|
this.client.rest.methods.createWebhook(this, name, dataURI).then(resolve).catch(reject);
|
||||||
|
}).catch(reject);
|
||||||
|
} else {
|
||||||
|
this.client.rest.methods.createWebhook(this, name).then(resolve).catch(reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
// These are here only for documentation purposes - they are implemented by TextBasedChannel
|
||||||
sendMessage() { return; }
|
sendMessage() { return; }
|
||||||
sendTTSMessage() { return; }
|
sendTTSMessage() { return; }
|
||||||
|
|||||||
@@ -135,6 +135,38 @@ class User {
|
|||||||
return this.client.rest.methods.deleteChannel(this);
|
return this.client.rest.methods.deleteChannel(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a friend request to the user
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
addFriend() {
|
||||||
|
return this.client.rest.methods.addFriend(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the user from your friends
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
removeFriend() {
|
||||||
|
return this.client.rest.methods.removeFriend(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks the user
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
block() {
|
||||||
|
return this.client.rest.methods.blockUser(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unblocks the user
|
||||||
|
* @returns {Promise<User>}
|
||||||
|
*/
|
||||||
|
unblock() {
|
||||||
|
return this.client.rest.methods.unblockUser(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
|
* Checks if the user is equal to another. It compares username, ID, discriminator, status and the game being played.
|
||||||
* It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
|
* It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties.
|
||||||
|
|||||||
@@ -45,6 +45,22 @@ class VoiceChannel extends GuildChannel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the client has permission join the voice channel
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get joinable() {
|
||||||
|
return this.permissionsFor(this.client.user).hasPermission('CONNECT');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the client has permission to send audio to the voice channel
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get speakable() {
|
||||||
|
return this.permissionsFor(this.client.user).hasPermission('SPEAK');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the bitrate of the channel
|
* Sets the bitrate of the channel
|
||||||
* @param {number} bitrate The new bitrate
|
* @param {number} bitrate The new bitrate
|
||||||
|
|||||||
206
src/structures/Webhook.js
Normal file
206
src/structures/Webhook.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
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, 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.editWebhook(this, name, dataURI)
|
||||||
|
.then(resolve).catch(reject);
|
||||||
|
}).catch(reject);
|
||||||
|
} else {
|
||||||
|
this.client.rest.methods.editWebhook(this, name)
|
||||||
|
.then(data => {
|
||||||
|
this.setup(data);
|
||||||
|
}).catch(reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the Webhook
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
delete() {
|
||||||
|
return this.client.rest.methods.deleteWebhook(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Webhook;
|
||||||
@@ -2,6 +2,7 @@ const path = require('path');
|
|||||||
const Message = require('../Message');
|
const Message = require('../Message');
|
||||||
const MessageCollector = require('../MessageCollector');
|
const MessageCollector = require('../MessageCollector');
|
||||||
const Collection = require('../../util/Collection');
|
const Collection = require('../../util/Collection');
|
||||||
|
const escapeMarkdown = require('../../util/EscapeMarkdown');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for classes that have text-channel-like features
|
* Interface for classes that have text-channel-like features
|
||||||
@@ -111,11 +112,11 @@ class TextBasedChannel {
|
|||||||
sendCode(lang, content, options = {}) {
|
sendCode(lang, content, options = {}) {
|
||||||
if (options.split) {
|
if (options.split) {
|
||||||
if (typeof options.split !== 'object') options.split = {};
|
if (typeof options.split !== 'object') options.split = {};
|
||||||
if (!options.split.prepend) options.split.prepend = `\`\`\`${lang ? lang : ''}\n`;
|
if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`;
|
||||||
if (!options.split.append) options.split.append = '\n```';
|
if (!options.split.append) options.split.append = '\n```';
|
||||||
}
|
}
|
||||||
content = this.client.resolver.resolveString(content).replace(/```/g, '`\u200b``');
|
content = escapeMarkdown(this.client.resolver.resolveString(content), true);
|
||||||
return this.sendMessage(`\`\`\`${lang ? lang : ''}\n${content}\n\`\`\``, options);
|
return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -315,17 +316,17 @@ class TextBasedChannel {
|
|||||||
* @returns {Collection<string, Message>}
|
* @returns {Collection<string, Message>}
|
||||||
*/
|
*/
|
||||||
bulkDelete(messages) {
|
bulkDelete(messages) {
|
||||||
if (messages instanceof Collection) messages = messages.array();
|
if (!(messages instanceof Array || messages instanceof Collection)) {
|
||||||
if (!(messages instanceof Array)) return Promise.reject(new TypeError('Messages must be an Array or Collection.'));
|
return Promise.reject(new TypeError('Messages must be an Array or Collection.'));
|
||||||
const messageIDs = messages.map(m => m.id);
|
}
|
||||||
|
const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id);
|
||||||
return this.client.rest.methods.bulkDeleteMessages(this, messageIDs);
|
return this.client.rest.methods.bulkDeleteMessages(this, messageIDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
_cacheMessage(message) {
|
_cacheMessage(message) {
|
||||||
const maxSize = this.client.options.maxMessageCache;
|
const maxSize = this.client.options.messageCacheMaxSize;
|
||||||
if (maxSize === 0) return null;
|
if (maxSize === 0) return null;
|
||||||
if (this.messages.size >= maxSize) this.messages.delete(this.messages.keys().next().value);
|
if (this.messages.size >= maxSize && maxSize > 0) this.messages.delete(this.messages.firstKey());
|
||||||
|
|
||||||
this.messages.set(message.id, message);
|
this.messages.set(message.id, message);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,48 @@
|
|||||||
* @extends {Map}
|
* @extends {Map}
|
||||||
*/
|
*/
|
||||||
class Collection extends Map {
|
class Collection extends Map {
|
||||||
|
constructor(iterable) {
|
||||||
|
super(iterable);
|
||||||
|
this._array = null;
|
||||||
|
this._keyArray = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, val) {
|
||||||
|
super.set(key, val);
|
||||||
|
this._array = null;
|
||||||
|
this._keyArray = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key) {
|
||||||
|
super.delete(key);
|
||||||
|
this._array = null;
|
||||||
|
this._keyArray = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an ordered array of the values of this collection.
|
* Creates an ordered array of the values of this collection, and caches it internally. The array will only be
|
||||||
|
* reconstructed if an item is added to or removed from the collection, or if you add/remove elements on the array.
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
* @example
|
* @example
|
||||||
* // identical to:
|
* // identical to:
|
||||||
* Array.from(collection.values());
|
* Array.from(collection.values());
|
||||||
*/
|
*/
|
||||||
array() {
|
array() {
|
||||||
return Array.from(this.values());
|
if (!this._array || this._array.length !== this.size) this._array = Array.from(this.values());
|
||||||
|
return this._array;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an ordered array of the keys of this collection.
|
* Creates an ordered array of the keys of this collection, and caches it internally. The array will only be
|
||||||
|
* reconstructed if an item is added to or removed from the collection, or if you add/remove elements on the array.
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
* @example
|
* @example
|
||||||
* // identical to:
|
* // identical to:
|
||||||
* Array.from(collection.keys());
|
* Array.from(collection.keys());
|
||||||
*/
|
*/
|
||||||
keyArray() {
|
keyArray() {
|
||||||
return Array.from(this.keys());
|
if (!this._keyArray || this._keyArray.length !== this.size) this._keyArray = Array.from(this.keys());
|
||||||
|
return this._keyArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,11 +205,27 @@ class Collection extends Map {
|
|||||||
*/
|
*/
|
||||||
filter(fn, thisArg) {
|
filter(fn, thisArg) {
|
||||||
if (thisArg) fn = fn.bind(thisArg);
|
if (thisArg) fn = fn.bind(thisArg);
|
||||||
const collection = new Collection();
|
const results = new Collection();
|
||||||
for (const [key, val] of this) {
|
for (const [key, val] of this) {
|
||||||
if (fn(val, key, this)) collection.set(key, val);
|
if (fn(val, key, this)) results.set(key, val);
|
||||||
}
|
}
|
||||||
return collection;
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identical to
|
||||||
|
* [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).
|
||||||
|
* @param {function} fn Function used to test (should return a boolean)
|
||||||
|
* @param {Object} [thisArg] Value to use as `this` when executing function
|
||||||
|
* @returns {Collection}
|
||||||
|
*/
|
||||||
|
filterArray(fn, thisArg) {
|
||||||
|
if (thisArg) fn = fn.bind(thisArg);
|
||||||
|
const results = [];
|
||||||
|
for (const [key, val] of this) {
|
||||||
|
if (fn(val, key, this)) results.push(val);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,28 +7,33 @@ exports.Package = require('../../package.json');
|
|||||||
* the order they are triggered, whereas burst runs multiple at a time, and doesn't guarantee a particular order.
|
* the order they are triggered, whereas burst runs multiple at a time, and doesn't guarantee a particular order.
|
||||||
* @property {number} [shardId=0] The ID of this shard
|
* @property {number} [shardId=0] The ID of this shard
|
||||||
* @property {number} [shardCount=0] The number of shards
|
* @property {number} [shardCount=0] The number of shards
|
||||||
* @property {number} [maxMessageCache=200] Number of messages to cache per channel
|
* @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel
|
||||||
|
* (-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
|
* @property {number} [messageCacheLifetime=0] How long until a message should be uncached by the message sweeping
|
||||||
* (in seconds, 0 for forever)
|
* (in seconds, 0 for forever)
|
||||||
* @property {number} [messageSweepInterval=0] How frequently to remove messages from the cache that are older than
|
* @property {number} [messageSweepInterval=0] How frequently to remove messages from the cache that are older than
|
||||||
* the max message lifetime (in seconds, 0 for never)
|
* the message cache lifetime (in seconds, 0 for never)
|
||||||
* @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as
|
* @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as
|
||||||
* upon joining a guild
|
* upon joining a guild
|
||||||
* @property {boolean} [disableEveryone=false] Default value for MessageOptions.disableEveryone
|
* @property {boolean} [disableEveryone=false] Default value for MessageOptions.disableEveryone
|
||||||
* @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their
|
* @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their
|
||||||
* corresponding websocket events
|
* corresponding websocket events
|
||||||
|
* @property {string[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be
|
||||||
|
* processed. Disabling useless events such as 'TYPING_START' can result in significant performance increases on
|
||||||
|
* large-scale bots.
|
||||||
* @property {WebsocketOptions} [ws] Options for the websocket
|
* @property {WebsocketOptions} [ws] Options for the websocket
|
||||||
*/
|
*/
|
||||||
exports.DefaultOptions = {
|
exports.DefaultOptions = {
|
||||||
apiRequestMethod: 'sequential',
|
apiRequestMethod: 'sequential',
|
||||||
shardId: 0,
|
shardId: 0,
|
||||||
shardCount: 0,
|
shardCount: 0,
|
||||||
maxMessageCache: 200,
|
messageCacheMaxSize: 200,
|
||||||
messageCacheLifetime: 0,
|
messageCacheLifetime: 0,
|
||||||
messageSweepInterval: 0,
|
messageSweepInterval: 0,
|
||||||
fetchAllMembers: false,
|
fetchAllMembers: false,
|
||||||
disableEveryone: false,
|
disableEveryone: false,
|
||||||
restWsBridgeTimeout: 5000,
|
restWsBridgeTimeout: 5000,
|
||||||
|
disabledEvents: [],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Websocket options. These are left as snake_case to match the API.
|
* Websocket options. These are left as snake_case to match the API.
|
||||||
@@ -67,6 +72,7 @@ const Endpoints = exports.Endpoints = {
|
|||||||
login: `${API}/auth/login`,
|
login: `${API}/auth/login`,
|
||||||
logout: `${API}/auth/logout`,
|
logout: `${API}/auth/logout`,
|
||||||
gateway: `${API}/gateway`,
|
gateway: `${API}/gateway`,
|
||||||
|
botGateway: `${API}/gateway/bot`,
|
||||||
invite: (id) => `${API}/invite/${id}`,
|
invite: (id) => `${API}/invite/${id}`,
|
||||||
inviteLink: (id) => `https://discord.gg/${id}`,
|
inviteLink: (id) => `https://discord.gg/${id}`,
|
||||||
CDN: 'https://cdn.discordapp.com',
|
CDN: 'https://cdn.discordapp.com',
|
||||||
@@ -77,6 +83,7 @@ const Endpoints = exports.Endpoints = {
|
|||||||
avatar: (userID, avatar) => userID === '1' ? avatar : `${Endpoints.user(userID)}/avatars/${avatar}.jpg`,
|
avatar: (userID, avatar) => userID === '1' ? avatar : `${Endpoints.user(userID)}/avatars/${avatar}.jpg`,
|
||||||
me: `${API}/users/@me`,
|
me: `${API}/users/@me`,
|
||||||
meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`,
|
meGuild: (guildID) => `${Endpoints.me}/guilds/${guildID}`,
|
||||||
|
relationships: (userID) => `${Endpoints.user(userID)}/relationships`,
|
||||||
|
|
||||||
// guilds
|
// guilds
|
||||||
guilds: `${API}/guilds`,
|
guilds: `${API}/guilds`,
|
||||||
@@ -103,6 +110,10 @@ const Endpoints = exports.Endpoints = {
|
|||||||
channelTyping: (channelID) => `${Endpoints.channel(channelID)}/typing`,
|
channelTyping: (channelID) => `${Endpoints.channel(channelID)}/typing`,
|
||||||
channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`,
|
channelPermissions: (channelID) => `${Endpoints.channel(channelID)}/permissions`,
|
||||||
channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`,
|
channelMessage: (channelID, messageID) => `${Endpoints.channelMessages(channelID)}/${messageID}`,
|
||||||
|
channelWebhooks: (channelID) => `${Endpoints.channel(channelID)}/webhooks`,
|
||||||
|
|
||||||
|
// webhooks
|
||||||
|
webhook: (webhookID, token) => `${API}/webhooks/${webhookID}${token ? `/${token}` : ''}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Status = {
|
exports.Status = {
|
||||||
@@ -131,6 +142,8 @@ exports.OPCodes = {
|
|||||||
RECONNECT: 7,
|
RECONNECT: 7,
|
||||||
REQUEST_GUILD_MEMBERS: 8,
|
REQUEST_GUILD_MEMBERS: 8,
|
||||||
INVALID_SESSION: 9,
|
INVALID_SESSION: 9,
|
||||||
|
HELLO: 10,
|
||||||
|
HEARTBEAT_ACK: 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.VoiceOPCodes = {
|
exports.VoiceOPCodes = {
|
||||||
@@ -178,6 +191,7 @@ exports.Events = {
|
|||||||
TYPING_STOP: 'typingStop',
|
TYPING_STOP: 'typingStop',
|
||||||
DISCONNECT: 'disconnect',
|
DISCONNECT: 'disconnect',
|
||||||
RECONNECTING: 'reconnecting',
|
RECONNECTING: 'reconnecting',
|
||||||
|
ERROR: 'error',
|
||||||
WARN: 'warn',
|
WARN: 'warn',
|
||||||
DEBUG: 'debug',
|
DEBUG: 'debug',
|
||||||
};
|
};
|
||||||
@@ -212,6 +226,18 @@ exports.WSEvents = {
|
|||||||
FRIEND_ADD: 'RELATIONSHIP_ADD',
|
FRIEND_ADD: 'RELATIONSHIP_ADD',
|
||||||
FRIEND_REMOVE: 'RELATIONSHIP_REMOVE',
|
FRIEND_REMOVE: 'RELATIONSHIP_REMOVE',
|
||||||
VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE',
|
VOICE_SERVER_UPDATE: 'VOICE_SERVER_UPDATE',
|
||||||
|
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 = {
|
const PermissionFlags = exports.PermissionFlags = {
|
||||||
@@ -242,7 +268,7 @@ const PermissionFlags = exports.PermissionFlags = {
|
|||||||
CHANGE_NICKNAME: 1 << 26,
|
CHANGE_NICKNAME: 1 << 26,
|
||||||
MANAGE_NICKNAMES: 1 << 27,
|
MANAGE_NICKNAMES: 1 << 27,
|
||||||
MANAGE_ROLES_OR_PERMISSIONS: 1 << 28,
|
MANAGE_ROLES_OR_PERMISSIONS: 1 << 28,
|
||||||
|
MANAGE_WEBHOOKS: 1 << 29,
|
||||||
MANAGE_EMOJIS: 1 << 30,
|
MANAGE_EMOJIS: 1 << 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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/GetRecommendedShards.js
Normal file
19
src/util/GetRecommendedShards.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 getRecommendedShards(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user