mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
Merge master into voice-rewrite
This commit is contained in:
@@ -127,7 +127,11 @@
|
||||
"semi-spacing": "error",
|
||||
"semi": "error",
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}],
|
||||
"space-in-parens": "error",
|
||||
"space-infix-ops": "error",
|
||||
"space-unary-ops": "error",
|
||||
|
||||
24
.travis.yml
24
.travis.yml
@@ -1,19 +1,19 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "8"
|
||||
- 8
|
||||
- 9
|
||||
install: npm install
|
||||
script: bash ./travis/test.sh
|
||||
jobs:
|
||||
include:
|
||||
- stage: deploy
|
||||
node_js: 9
|
||||
script: bash ./travis/deploy.sh
|
||||
env:
|
||||
- ENCRYPTION_LABEL="af862fa96d3e"
|
||||
- COMMIT_AUTHOR_EMAIL="amishshah.2k@gmail.com"
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
install: npm install
|
||||
jobs:
|
||||
include:
|
||||
- stage: test
|
||||
script: bash ./travis/test.sh
|
||||
- stage: deploy
|
||||
script: bash ./travis/deploy.sh
|
||||
env:
|
||||
global:
|
||||
- ENCRYPTION_LABEL: "af862fa96d3e"
|
||||
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
|
||||
dist: trusty
|
||||
sudo: false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
<br />
|
||||
<p>
|
||||
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" /></a>
|
||||
<a href="https://discord.js.org"><img src="https://discord.js.org/static/logo.svg" width="546" alt="discord.js" id="djs-logo" /></a>
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
|
||||
11
package.json
11
package.json
@@ -32,12 +32,11 @@
|
||||
"homepage": "https://github.com/hydrabolt/discord.js#readme",
|
||||
"runkitExampleFilename": "./docs/examples/ping.js",
|
||||
"dependencies": {
|
||||
"long": "^3.0.0",
|
||||
"pako": "^1.0.0",
|
||||
"prism-media": "github:hydrabolt/prism-media#indev",
|
||||
"snekfetch": "^3.0.0",
|
||||
"snekfetch": "^3.5.0",
|
||||
"tweetnacl": "^1.0.0",
|
||||
"ws": "^3.0.0"
|
||||
"ws": "^3.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^3.0.0",
|
||||
@@ -50,12 +49,12 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^8.0.0",
|
||||
"discord.js-docgen": "hydrabolt/discord.js-docgen",
|
||||
"eslint": "^4.0.0",
|
||||
"eslint": "^4.11.0",
|
||||
"jsdoc-strip-async-await": "^0.1.0",
|
||||
"json-filter-loader": "^1.0.0",
|
||||
"parallel-webpack": "^2.0.0",
|
||||
"parallel-webpack": "^2.2.0",
|
||||
"uglifyjs-webpack-plugin": "^1.0.0-beta.2",
|
||||
"webpack": "^3.0.0"
|
||||
"webpack": "^3.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
|
||||
@@ -42,6 +42,7 @@ class BaseClient extends EventEmitter {
|
||||
/**
|
||||
* API shortcut
|
||||
* @type {Object}
|
||||
* @readonly
|
||||
* @private
|
||||
*/
|
||||
get api() {
|
||||
|
||||
@@ -163,6 +163,7 @@ class Client extends BaseClient {
|
||||
/**
|
||||
* Timestamp of the latest ping's start time
|
||||
* @type {number}
|
||||
* @readonly
|
||||
* @private
|
||||
*/
|
||||
get _pingTimestamp() {
|
||||
|
||||
@@ -3,7 +3,7 @@ const BaseClient = require('./BaseClient');
|
||||
|
||||
/**
|
||||
* The webhook client.
|
||||
* @extends {Webhook}
|
||||
* @implements {Webhook}
|
||||
* @extends {BaseClient}
|
||||
*/
|
||||
class WebhookClient extends BaseClient {
|
||||
|
||||
@@ -15,7 +15,7 @@ const libs = {
|
||||
|
||||
exports.methods = {};
|
||||
|
||||
(async() => {
|
||||
(async () => {
|
||||
for (const libName of Object.keys(libs)) {
|
||||
try {
|
||||
const lib = require(libName);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const AbstractHandler = require('./AbstractHandler');
|
||||
const { Events } = require('../../../../util/Constants');
|
||||
const ClientUser = require('../../../../structures/ClientUser');
|
||||
let ClientUser;
|
||||
|
||||
class ReadyHandler extends AbstractHandler {
|
||||
handle(packet) {
|
||||
@@ -12,6 +12,7 @@ class ReadyHandler extends AbstractHandler {
|
||||
data.user.user_settings = data.user_settings;
|
||||
data.user.user_guild_settings = data.user_guild_settings;
|
||||
|
||||
if (!ClientUser) ClientUser = require('../../../../structures/ClientUser');
|
||||
const clientUser = new ClientUser(client, data.user);
|
||||
client.user = clientUser;
|
||||
client.readyAt = new Date();
|
||||
|
||||
@@ -20,9 +20,13 @@ const Messages = {
|
||||
SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.',
|
||||
SHARDING_CHILD_CONNECTION: 'Failed to send message to shard\'s process.',
|
||||
SHARDING_PARENT_CONNECTION: 'Failed to send message to master process.',
|
||||
SHARDING_NO_SHARDS: 'No shards have been spawned',
|
||||
SHARDING_IN_PROCESS: 'Shards are still being spawned',
|
||||
SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards`,
|
||||
SHARDING_NO_SHARDS: 'No shards have been spawned.',
|
||||
SHARDING_IN_PROCESS: 'Shards are still being spawned.',
|
||||
SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`,
|
||||
SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`,
|
||||
SHARDING_READY_TIMEOUT: id => `Shard ${id}'s Client took too long to become ready.`,
|
||||
SHARDING_READY_DISCONNECTED: id => `Shard ${id}'s Client disconnected before becoming ready.`,
|
||||
SHARDING_READY_DIED: id => `Shard ${id}'s process exited before its Client became ready.`,
|
||||
|
||||
COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).',
|
||||
COLOR_CONVERT: 'Unable to convert color to a number.',
|
||||
|
||||
10
src/index.js
10
src/index.js
@@ -18,6 +18,7 @@ module.exports = {
|
||||
Permissions: require('./util/Permissions'),
|
||||
Snowflake: require('./util/Snowflake'),
|
||||
SnowflakeUtil: require('./util/Snowflake'),
|
||||
Structures: require('./util/Structures'),
|
||||
Util: Util,
|
||||
util: Util,
|
||||
version: require('../package.json').version,
|
||||
@@ -29,14 +30,18 @@ module.exports = {
|
||||
GuildChannelStore: require('./stores/GuildChannelStore'),
|
||||
GuildMemberStore: require('./stores/GuildMemberStore'),
|
||||
GuildStore: require('./stores/GuildStore'),
|
||||
ReactionUserStore: require('./stores/ReactionUserStore'),
|
||||
MessageStore: require('./stores/MessageStore'),
|
||||
PresenceStore: require('./stores/PresenceStore'),
|
||||
RoleStore: require('./stores/RoleStore'),
|
||||
UserStore: require('./stores/UserStore'),
|
||||
|
||||
// Shortcuts to Util methods
|
||||
discordSort: Util.discordSort,
|
||||
escapeMarkdown: Util.escapeMarkdown,
|
||||
fetchRecommendedShards: Util.fetchRecommendedShards,
|
||||
resolveColor: Util.resolveColor,
|
||||
resolveString: Util.resolveString,
|
||||
splitMessage: Util.splitMessage,
|
||||
|
||||
// Structures
|
||||
@@ -45,7 +50,10 @@ module.exports = {
|
||||
CategoryChannel: require('./structures/CategoryChannel'),
|
||||
Channel: require('./structures/Channel'),
|
||||
ClientApplication: require('./structures/ClientApplication'),
|
||||
ClientUser: require('./structures/ClientUser'),
|
||||
get ClientUser() {
|
||||
// This is a getter so that it properly extends any custom User class
|
||||
return require('./structures/ClientUser');
|
||||
},
|
||||
ClientUserChannelOverride: require('./structures/ClientUserChannelOverride'),
|
||||
ClientUserGuildSettings: require('./structures/ClientUserGuildSettings'),
|
||||
ClientUserSettings: require('./structures/ClientUserSettings'),
|
||||
|
||||
@@ -39,12 +39,13 @@ class RequestHandler {
|
||||
/**
|
||||
* Emitted when the client hits a rate limit while making a request
|
||||
* @event Client#rateLimit
|
||||
* @prop {number} timeout Timeout in ms
|
||||
* @prop {number} limit Number of requests that can be made to this endpoint
|
||||
* @prop {number} timeDifference Delta-T in ms between your system and Discord servers
|
||||
* @prop {string} method HTTP method used for request that triggered this event
|
||||
* @prop {string} path Path used for request that triggered this event
|
||||
* @prop {string} route Route used for request that triggered this event
|
||||
* @param {Object} rateLimitInfo Object containing the rate limit info
|
||||
* @param {number} rateLimitInfo.timeout Timeout in ms
|
||||
* @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint
|
||||
* @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers
|
||||
* @param {string} rateLimitInfo.method HTTP method used for request that triggered this event
|
||||
* @param {string} rateLimitInfo.path Path used for request that triggered this event
|
||||
* @param {string} rateLimitInfo.route Route used for request that triggered this event
|
||||
*/
|
||||
this.client.emit(RATE_LIMIT, {
|
||||
timeout,
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
const childProcess = require('child_process');
|
||||
const EventEmitter = require('events');
|
||||
const path = require('path');
|
||||
const Util = require('../util/Util');
|
||||
const { Error } = require('../errors');
|
||||
|
||||
/**
|
||||
* Represents a Shard spawned by the ShardingManager.
|
||||
* A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains
|
||||
* an instance of the bot and its {@link Client}. When its child process exits for any reason, the shard will spawn a
|
||||
* new one to replace it as necessary.
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
class Shard {
|
||||
class Shard extends EventEmitter {
|
||||
/**
|
||||
* @param {ShardingManager} manager The sharding manager
|
||||
* @param {number} id The ID of this shard
|
||||
* @param {Array} [args=[]] Command line arguments to pass to the script
|
||||
* @param {ShardingManager} manager Manager that is spawning this shard
|
||||
* @param {number} id ID of this shard
|
||||
* @param {string[]} [args=[]] Command line arguments to pass to the script
|
||||
*/
|
||||
constructor(manager, id, args = []) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* Manager that created the shard
|
||||
* @type {ShardingManager}
|
||||
@@ -26,7 +32,13 @@ class Shard {
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* The environment variables for the shard
|
||||
* Arguments for the shard's process
|
||||
* @type {string[]}
|
||||
*/
|
||||
this.args = args;
|
||||
|
||||
/**
|
||||
* Environment variables for the shard's process
|
||||
* @type {Object}
|
||||
*/
|
||||
this.env = Object.assign({}, process.env, {
|
||||
@@ -36,19 +48,81 @@ class Shard {
|
||||
});
|
||||
|
||||
/**
|
||||
* Process of the shard
|
||||
* @type {ChildProcess}
|
||||
* Whether the shard's {@link Client} is ready
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.process = childProcess.fork(path.resolve(this.manager.file), args, {
|
||||
env: this.env,
|
||||
});
|
||||
this.process.on('message', this._handleMessage.bind(this));
|
||||
this.process.once('exit', () => {
|
||||
if (this.manager.respawn) this.manager.createShard(this.id);
|
||||
});
|
||||
this.ready = false;
|
||||
|
||||
/**
|
||||
* Process of the shard
|
||||
* @type {?ChildProcess}
|
||||
*/
|
||||
this.process = null;
|
||||
|
||||
/**
|
||||
* Ongoing promises for calls to {@link Shard#eval}, mapped by the `script` they were called with
|
||||
* @type {Map<string, Promise>}
|
||||
* @private
|
||||
*/
|
||||
this._evals = new Map();
|
||||
|
||||
/**
|
||||
* Ongoing promises for calls to {@link Shard#fetchClientValue}, mapped by the `prop` they were called with
|
||||
* @type {Map<string, Promise>}
|
||||
* @private
|
||||
*/
|
||||
this._fetches = new Map();
|
||||
|
||||
/**
|
||||
* Listener function for the {@link ChildProcess}' `exit` event
|
||||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
this._exitListener = this._handleExit.bind(this, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forks a child process for the shard.
|
||||
* <warn>You should not need to call this manually.</warn>
|
||||
* @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving
|
||||
* @returns {Promise<ChildProcess>}
|
||||
*/
|
||||
async spawn(waitForReady = true) {
|
||||
if (this.process) throw new Error('SHARDING_PROCESS_EXISTS', this.id);
|
||||
|
||||
this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { env: this.env })
|
||||
.on('message', this._handleMessage.bind(this))
|
||||
.on('exit', this._exitListener);
|
||||
|
||||
/**
|
||||
* Emitted upon the creation of the shard's child process.
|
||||
* @event Shard#spawn
|
||||
* @param {ChildProcess} process Child process that was created
|
||||
*/
|
||||
this.emit('spawn', this.process);
|
||||
|
||||
if (!waitForReady) return this.process;
|
||||
await new Promise((resolve, reject) => {
|
||||
this.once('ready', resolve);
|
||||
this.once('disconnect', () => reject(new Error('SHARDING_READY_DISCONNECTED', this.id)));
|
||||
this.once('death', () => reject(new Error('SHARDING_READY_DIED', this.id)));
|
||||
setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), 30000);
|
||||
});
|
||||
return this.process;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills and restarts the shard's process.
|
||||
* @param {number} [delay=500] How long to wait between killing the process and restarting it (in milliseconds)
|
||||
* @param {boolean} [waitForReady=true] Whether to wait the {@link Client} has become ready before resolving
|
||||
* @returns {Promise<ChildProcess>}
|
||||
*/
|
||||
async respawn(delay = 500, waitForReady = true) {
|
||||
this.process.removeListener('exit', this._exitListener);
|
||||
this.process.kill();
|
||||
this._handleExit(false);
|
||||
if (delay > 0) await Util.delayFor(delay);
|
||||
return this.spawn(waitForReady);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +174,7 @@ class Shard {
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a script on the shard, in the context of the client.
|
||||
* Evaluates a script on the shard, in the context of the {@link Client}.
|
||||
* @param {string} script JavaScript to run on the shard
|
||||
* @returns {Promise<*>} Result of the script execution
|
||||
*/
|
||||
@@ -134,6 +208,39 @@ class Shard {
|
||||
*/
|
||||
_handleMessage(message) {
|
||||
if (message) {
|
||||
// Shard is ready
|
||||
if (message._ready) {
|
||||
this.ready = true;
|
||||
/**
|
||||
* Emitted upon the shard's {@link Client#ready} event.
|
||||
* @event Shard#ready
|
||||
*/
|
||||
this.emit('ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// Shard has disconnected
|
||||
if (message._disconnect) {
|
||||
this.ready = false;
|
||||
/**
|
||||
* Emitted upon the shard's {@link Client#disconnect} event.
|
||||
* @event Shard#disconnect
|
||||
*/
|
||||
this.emit('disconnect');
|
||||
return;
|
||||
}
|
||||
|
||||
// Shard is attempting to reconnect
|
||||
if (message._reconnecting) {
|
||||
this.ready = false;
|
||||
/**
|
||||
* Emitted upon the shard's {@link Client#reconnecting} event.
|
||||
* @event Shard#reconnecting
|
||||
*/
|
||||
this.emit('reconnecting');
|
||||
return;
|
||||
}
|
||||
|
||||
// Shard is requesting a property fetch
|
||||
if (message._sFetchProp) {
|
||||
this.manager.fetchClientValues(message._sFetchProp).then(
|
||||
@@ -151,15 +258,44 @@ class Shard {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Shard is requesting a respawn of all shards
|
||||
if (message._sRespawnAll) {
|
||||
const { shardDelay, respawnDelay, waitForReady } = message._sRespawnAll;
|
||||
this.manager.respawnAll(shardDelay, respawnDelay, waitForReady).catch(() => {
|
||||
// Do nothing
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted upon recieving a message from a shard.
|
||||
* @event ShardingManager#message
|
||||
* @param {Shard} shard Shard that sent the message
|
||||
* Emitted upon recieving a message from the child process.
|
||||
* @event Shard#message
|
||||
* @param {*} message Message that was received
|
||||
*/
|
||||
this.manager.emit('message', this, message);
|
||||
this.emit('message', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the shard's process exiting.
|
||||
* @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again
|
||||
* @private
|
||||
*/
|
||||
_handleExit(respawn = this.manager.respawn) {
|
||||
/**
|
||||
* Emitted upon the shard's child process exiting.
|
||||
* @event Shard#death
|
||||
* @param {ChildProcess} process Child process that exited
|
||||
*/
|
||||
this.emit('death', this.process);
|
||||
|
||||
this.ready = false;
|
||||
this.process = null;
|
||||
this._evals.clear();
|
||||
this._fetches.clear();
|
||||
|
||||
if (respawn) this.spawn().catch(err => this.emit('error', err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,19 @@ const { Events } = require('../util/Constants');
|
||||
const { Error } = require('../errors');
|
||||
|
||||
/**
|
||||
* Helper class for sharded clients spawned as a child process, such as from a ShardingManager.
|
||||
* Helper class for sharded clients spawned as a child process, such as from a {@link ShardingManager}.
|
||||
* Utilises IPC to send and receive data to/from the master process and other shards.
|
||||
*/
|
||||
class ShardClientUtil {
|
||||
/**
|
||||
* @param {Client} client The client of the current shard
|
||||
* @param {Client} client Client of the current shard
|
||||
*/
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
process.on('message', this._handleMessage.bind(this));
|
||||
client.on('ready', () => { process.send({ _ready: true }); });
|
||||
client.on('disconnect', () => { process.send({ _disconnect: true }); });
|
||||
client.on('reconnecting', () => { process.send({ _reconnecting: true }); });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,13 +53,14 @@ class ShardClientUtil {
|
||||
/**
|
||||
* Fetches a client property value of each shard.
|
||||
* @param {string} prop Name of the client property to get, using periods for nesting
|
||||
* @returns {Promise<Array>}
|
||||
* @returns {Promise<Array<*>>}
|
||||
* @example
|
||||
* client.shard.fetchClientValues('guilds.size')
|
||||
* .then(results => {
|
||||
* console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`);
|
||||
* })
|
||||
* .catch(console.error);
|
||||
* @see {@link ShardingManager#fetchClientValues}
|
||||
*/
|
||||
fetchClientValues(prop) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -74,9 +79,10 @@ class ShardClientUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a script on all shards, in the context of the Clients.
|
||||
* Evaluates a script on all shards, in the context of the {@link Clients}.
|
||||
* @param {string} script JavaScript to run on each shard
|
||||
* @returns {Promise<Array>} Results of the script execution
|
||||
* @returns {Promise<Array<*>>} Results of the script execution
|
||||
* @see {@link ShardingManager#broadcastEval}
|
||||
*/
|
||||
broadcastEval(script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -94,6 +100,19 @@ class ShardClientUtil {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a respawn of all shards.
|
||||
* @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds)
|
||||
* @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it
|
||||
* (in milliseconds)
|
||||
* @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
|
||||
* @returns {Promise<void>} Resolves upon the message being sent
|
||||
* @see {@link ShardingManager#respawnAll}
|
||||
*/
|
||||
respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) {
|
||||
return this.send({ _sRespawnAll: { shardDelay, respawnDelay, waitForReady } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an IPC message.
|
||||
* @param {*} message Message received
|
||||
|
||||
@@ -7,9 +7,12 @@ const Util = require('../util/Util');
|
||||
const { Error, TypeError, RangeError } = require('../errors');
|
||||
|
||||
/**
|
||||
* This is a utility class that can be used to help you spawn shards of your client. Each shard is completely separate
|
||||
* from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely.
|
||||
* If you do not select an amount of shards, the manager will automatically decide the best amount.
|
||||
* This is a utility class that makes multi-process sharding of a bot an easy and painless experience.
|
||||
* It works by spawning a self-contained {@link ChildProcess} for each individual shard, each containing its own
|
||||
* instance of your bot's {@link Client}. They all have a line of communication with the master process, and there are
|
||||
* several useful methods that utilise it in order to simplify tasks that are normally difficult with sharding. It can
|
||||
* spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a path to your main bot
|
||||
* script to launch for each one.
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class ShardingManager extends EventEmitter {
|
||||
@@ -82,33 +85,33 @@ class ShardingManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Spawns a single shard.
|
||||
* @param {number} id The ID of the shard to spawn. **This is usually not necessary**
|
||||
* @returns {Promise<Shard>}
|
||||
* @param {number} [id=this.shards.size] ID of the shard to spawn -
|
||||
* **This is usually not necessary to manually specify.**
|
||||
* @returns {Shard}
|
||||
*/
|
||||
createShard(id = this.shards.size) {
|
||||
const shard = new Shard(this, id, this.shardArgs);
|
||||
this.shards.set(id, shard);
|
||||
/**
|
||||
* Emitted upon launching a shard.
|
||||
* @event ShardingManager#launch
|
||||
* @param {Shard} shard Shard that was launched
|
||||
* Emitted upon creating a shard.
|
||||
* @event ShardingManager#shardCreate
|
||||
* @param {Shard} shard Shard that was created
|
||||
*/
|
||||
this.emit('launch', shard);
|
||||
return Promise.resolve(shard);
|
||||
this.emit('shardCreate', shard);
|
||||
return shard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns multiple shards.
|
||||
* @param {number} [amount=this.totalShards] Number of shards to spawn
|
||||
* @param {number} [delay=7500] How long to wait in between spawning each shard (in milliseconds)
|
||||
* @param {number} [delay=5500] How long to wait in between spawning each shard (in milliseconds)
|
||||
* @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
|
||||
* @returns {Promise<Collection<number, Shard>>}
|
||||
*/
|
||||
spawn(amount = this.totalShards, delay = 7500) {
|
||||
async spawn(amount = this.totalShards, delay = 5500, waitForReady = true) {
|
||||
// Obtain/verify the number of shards to spawn
|
||||
if (amount === 'auto') {
|
||||
return Util.fetchRecommendedShards(this.token).then(count => {
|
||||
this.totalShards = count;
|
||||
return this._spawn(count, delay);
|
||||
});
|
||||
amount = await Util.fetchRecommendedShards(this.token);
|
||||
} else {
|
||||
if (typeof amount !== 'number' || isNaN(amount)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.');
|
||||
@@ -117,41 +120,22 @@ class ShardingManager extends EventEmitter {
|
||||
if (amount !== Math.floor(amount)) {
|
||||
throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.');
|
||||
}
|
||||
return this._spawn(amount, delay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually spawns shards, unlike that poser above >:(
|
||||
* @param {number} amount Number of shards to spawn
|
||||
* @param {number} delay How long to wait in between spawning each shard (in milliseconds)
|
||||
* @returns {Promise<Collection<number, Shard>>}
|
||||
* @private
|
||||
*/
|
||||
_spawn(amount, delay) {
|
||||
return new Promise(resolve => {
|
||||
if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size);
|
||||
this.totalShards = amount;
|
||||
// Make sure this many shards haven't already been spawned
|
||||
if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size);
|
||||
this.totalShards = amount;
|
||||
|
||||
this.createShard();
|
||||
if (this.shards.size >= this.totalShards) {
|
||||
resolve(this.shards);
|
||||
return;
|
||||
}
|
||||
// Spawn the shards
|
||||
for (let s = 1; s <= amount; s++) {
|
||||
const promises = [];
|
||||
const shard = this.createShard();
|
||||
promises.push(shard.spawn(waitForReady));
|
||||
if (delay > 0 && s !== amount) promises.push(Util.delayFor(delay));
|
||||
await Promise.all(promises); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
|
||||
if (delay <= 0) {
|
||||
while (this.shards.size < this.totalShards) this.createShard();
|
||||
resolve(this.shards);
|
||||
} else {
|
||||
const interval = setInterval(() => {
|
||||
this.createShard();
|
||||
if (this.shards.size >= this.totalShards) {
|
||||
clearInterval(interval);
|
||||
resolve(this.shards);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
});
|
||||
return this.shards;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,9 +150,9 @@ class ShardingManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a script on all shards, in the context of the Clients.
|
||||
* Evaluates a script on all shards, in the context of the {@link Client}s.
|
||||
* @param {string} script JavaScript to run on each shard
|
||||
* @returns {Promise<Array>} Results of the script execution
|
||||
* @returns {Promise<Array<*>>} Results of the script execution
|
||||
*/
|
||||
broadcastEval(script) {
|
||||
const promises = [];
|
||||
@@ -179,7 +163,7 @@ class ShardingManager extends EventEmitter {
|
||||
/**
|
||||
* Fetches a client property value of each shard.
|
||||
* @param {string} prop Name of the client property to get, using periods for nesting
|
||||
* @returns {Promise<Array>}
|
||||
* @returns {Promise<Array<*>>}
|
||||
* @example
|
||||
* manager.fetchClientValues('guilds.size')
|
||||
* .then(results => {
|
||||
@@ -194,6 +178,24 @@ class ShardingManager extends EventEmitter {
|
||||
for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills all running shards and respawns them.
|
||||
* @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds)
|
||||
* @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it
|
||||
* (in milliseconds)
|
||||
* @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another
|
||||
* @returns {Promise<Collection<string, Shard>>}
|
||||
*/
|
||||
async respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) {
|
||||
let s = 0;
|
||||
for (const shard of this.shards) {
|
||||
const promises = [shard.respawn(respawnDelay, waitForReady)];
|
||||
if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay));
|
||||
await Promise.all(promises); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
return this.shards;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ShardingManager;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Collection = require('../util/Collection');
|
||||
let Structures;
|
||||
|
||||
/**
|
||||
* Manages the creation, retrieval and deletion of a specific data model.
|
||||
@@ -7,8 +8,9 @@ const Collection = require('../util/Collection');
|
||||
class DataStore extends Collection {
|
||||
constructor(client, iterable, holds) {
|
||||
super();
|
||||
if (!Structures) Structures = require('../util/Structures');
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
Object.defineProperty(this, 'holds', { value: holds });
|
||||
Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) || holds });
|
||||
if (iterable) for (const item of iterable) this.create(item);
|
||||
}
|
||||
|
||||
|
||||
33
src/stores/ReactionUserStore.js
Normal file
33
src/stores/ReactionUserStore.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const DataStore = require('./DataStore');
|
||||
/**
|
||||
* A data store to store User models who reacted to a MessageReaction.
|
||||
* @extends {DataStore}
|
||||
*/
|
||||
class ReactionUserStore extends DataStore {
|
||||
constructor(client, iterable, reaction) {
|
||||
super(client, iterable, require('../structures/User'));
|
||||
this.reaction = reaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs.
|
||||
* @param {Object} [options] Options for fetching the users
|
||||
* @param {number} [options.limit=100] The maximum amount of users to fetch, defaults to 100
|
||||
* @param {Snowflake} [options.before] Limit fetching users to those with an id lower than the supplied id
|
||||
* @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id
|
||||
* @returns {Promise<ReactionUserStore<Snowflake, User>>}
|
||||
*/
|
||||
async fetch({ limit = 100, after, before } = {}) {
|
||||
const message = this.reaction.message;
|
||||
const users = await this.client.api.channels[message.channel.id].messages[message.id]
|
||||
.reactions[this.reaction.emoji.identifier]
|
||||
.get({ query: { limit, before, after } });
|
||||
for (const rawUser of users) {
|
||||
const user = this.client.users.create(rawUser);
|
||||
this.set(user.id, user);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ReactionUserStore;
|
||||
@@ -6,7 +6,7 @@ const GuildChannel = require('./GuildChannel');
|
||||
*/
|
||||
class CategoryChannel extends GuildChannel {
|
||||
/**
|
||||
* The channels that are part of this category
|
||||
* Channels that are part of this category
|
||||
* @type {?Collection<Snowflake, GuildChannel>}
|
||||
* @readonly
|
||||
*/
|
||||
|
||||
@@ -66,32 +66,37 @@ class Channel extends Base {
|
||||
}
|
||||
|
||||
static create(client, data, guild) {
|
||||
const DMChannel = require('./DMChannel');
|
||||
const GroupDMChannel = require('./GroupDMChannel');
|
||||
const TextChannel = require('./TextChannel');
|
||||
const VoiceChannel = require('./VoiceChannel');
|
||||
const CategoryChannel = require('./CategoryChannel');
|
||||
const GuildChannel = require('./GuildChannel');
|
||||
const Structures = require('../util/Structures');
|
||||
let channel;
|
||||
if (data.type === ChannelTypes.DM) {
|
||||
const DMChannel = Structures.get('DMChannel');
|
||||
channel = new DMChannel(client, data);
|
||||
} else if (data.type === ChannelTypes.GROUP) {
|
||||
const GroupDMChannel = Structures.get('GroupDMChannel');
|
||||
channel = new GroupDMChannel(client, data);
|
||||
} else {
|
||||
guild = guild || client.guilds.get(data.guild_id);
|
||||
if (guild) {
|
||||
switch (data.type) {
|
||||
case ChannelTypes.TEXT:
|
||||
case ChannelTypes.TEXT: {
|
||||
const TextChannel = Structures.get('TextChannel');
|
||||
channel = new TextChannel(guild, data);
|
||||
break;
|
||||
case ChannelTypes.VOICE:
|
||||
}
|
||||
case ChannelTypes.VOICE: {
|
||||
const VoiceChannel = Structures.get('VoiceChannel');
|
||||
channel = new VoiceChannel(guild, data);
|
||||
break;
|
||||
case ChannelTypes.CATEGORY:
|
||||
}
|
||||
case ChannelTypes.CATEGORY: {
|
||||
const CategoryChannel = Structures.get('CategoryChannel');
|
||||
channel = new CategoryChannel(guild, data);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
const GuildChannel = Structures.get('GuildChannel');
|
||||
channel = new GuildChannel(guild, data);
|
||||
}
|
||||
}
|
||||
guild.channels.set(channel.id, channel);
|
||||
}
|
||||
|
||||
@@ -150,11 +150,12 @@ class ClientApplication extends Base {
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
fetchAssets() {
|
||||
return this.client.api.applications(this.id).assets.get()
|
||||
const types = Object.keys(ClientApplicationAssetTypes);
|
||||
return this.client.api.oauth2.applications(this.id).assets.get()
|
||||
.then(assets => assets.map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
type: Object.keys(ClientApplicationAssetTypes)[a.type - 1],
|
||||
type: types[a.type - 1],
|
||||
})));
|
||||
}
|
||||
|
||||
@@ -167,7 +168,7 @@ class ClientApplication extends Base {
|
||||
*/
|
||||
createAsset(name, data, type) {
|
||||
return DataResolver.resolveBase64(data).then(b64 =>
|
||||
this.client.api.applications(this.id).assets.post({ data: {
|
||||
this.client.api.oauth2.applications(this.id).assets.post({ data: {
|
||||
name,
|
||||
data: b64,
|
||||
type: ClientApplicationAssetTypes[type.toUpperCase()],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const User = require('./User');
|
||||
const Structures = require('../util/Structures');
|
||||
const Collection = require('../util/Collection');
|
||||
const ClientUserSettings = require('./ClientUserSettings');
|
||||
const ClientUserGuildSettings = require('./ClientUserGuildSettings');
|
||||
@@ -11,7 +11,7 @@ const Guild = require('./Guild');
|
||||
* Represents the logged in client's Discord user.
|
||||
* @extends {User}
|
||||
*/
|
||||
class ClientUser extends User {
|
||||
class ClientUser extends Structures.get('User') {
|
||||
_patch(data) {
|
||||
super._patch(data);
|
||||
|
||||
@@ -248,7 +248,7 @@ class ClientUser extends User {
|
||||
/**
|
||||
* Fetches messages that mentioned the client's user.
|
||||
* <warn>This is only available when using a user account.</warn>
|
||||
* @param {Object} [options] Options for the fetch
|
||||
* @param {Object} [options={}] Options for the fetch
|
||||
* @param {number} [options.limit=25] Maximum number of mentions to retrieve
|
||||
* @param {boolean} [options.roles=true] Whether to include role mentions
|
||||
* @param {boolean} [options.everyone=true] Whether to include everyone/here mentions
|
||||
|
||||
@@ -114,8 +114,18 @@ class Guild extends Base {
|
||||
this.large = Boolean('large' in data ? data.large : this.large);
|
||||
|
||||
/**
|
||||
* An array of guild features
|
||||
* @type {string[]}
|
||||
* An array of enabled guild features, here are the possible values:
|
||||
* * INVITE_SPLASH
|
||||
* * MORE_EMOJI
|
||||
* * VERIFIED
|
||||
* * VIP_REGIONS
|
||||
* * VANITY_URL
|
||||
* @typedef {string} Features
|
||||
*/
|
||||
|
||||
/**
|
||||
* An array of guild features partnered guilds have enabled
|
||||
* @type {Features[]}
|
||||
*/
|
||||
this.features = data.features;
|
||||
|
||||
@@ -311,7 +321,7 @@ class Guild extends Base {
|
||||
|
||||
/**
|
||||
* System channel for this guild
|
||||
* @type {?GuildChannel}
|
||||
* @type {?TextChannel}
|
||||
* @readonly
|
||||
*/
|
||||
get systemChannel() {
|
||||
@@ -431,10 +441,16 @@ class Guild extends Base {
|
||||
return this.members.resolve(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* An object containing information about a guild member's ban.
|
||||
* @typedef {Object} BanInfo
|
||||
* @property {User} user User that was banned
|
||||
* @property {?string} reason Reason the user was banned
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetches a collection of banned users in this guild.
|
||||
* The returned collection contains user objects keyed under `user` and reasons keyed under `reason`.
|
||||
* @returns {Promise<Collection<Snowflake, Object>>}
|
||||
* @returns {Promise<Collection<Snowflake, BanInfo>>}
|
||||
*/
|
||||
fetchBans() {
|
||||
return this.client.api.guilds(this.id).bans.get().then(bans =>
|
||||
@@ -496,7 +512,7 @@ class Guild extends Base {
|
||||
* @param {Snowflake|GuildAuditLogsEntry} [options.after] Limit to entries from after specified entry
|
||||
* @param {number} [options.limit] Limit number of entries
|
||||
* @param {UserResolvable} [options.user] Only show entries involving this user
|
||||
* @param {ActionType|number} [options.type] Only show entries involving this action type
|
||||
* @param {AuditLogAction|number} [options.type] Only show entries involving this action type
|
||||
* @returns {Promise<GuildAuditLogs>}
|
||||
*/
|
||||
fetchAuditLogs(options = {}) {
|
||||
@@ -528,7 +544,9 @@ class Guild extends Base {
|
||||
* @returns {Promise<GuildMember>}
|
||||
*/
|
||||
addMember(user, options) {
|
||||
if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id));
|
||||
user = this.client.users.resolveID(user);
|
||||
if (!user) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable'));
|
||||
if (this.members.has(user)) return Promise.resolve(this.members.get(user));
|
||||
options.access_token = options.accessToken;
|
||||
if (options.roles) {
|
||||
const roles = [];
|
||||
@@ -541,7 +559,7 @@ class Guild extends Base {
|
||||
roles.push(role.id);
|
||||
}
|
||||
}
|
||||
return this.client.api.guilds(this.id).members(user.id).put({ data: options })
|
||||
return this.client.api.guilds(this.id).members(user).put({ data: options })
|
||||
.then(data => this.members.create(data));
|
||||
}
|
||||
|
||||
@@ -794,6 +812,7 @@ class Guild extends Base {
|
||||
* @returns {Promise<Guild>}
|
||||
*/
|
||||
allowDMs(allow) {
|
||||
if (this.client.user.bot) return Promise.reject(new Error('FEATURE_USER_ONLY'));
|
||||
const settings = this.client.user.settings;
|
||||
if (allow) return settings.removeRestrictedGuild(this);
|
||||
else return settings.addRestrictedGuild(this);
|
||||
@@ -802,11 +821,10 @@ class Guild extends Base {
|
||||
/**
|
||||
* Bans a user from the guild.
|
||||
* @param {UserResolvable} user The user to ban
|
||||
* @param {Object} [options] Ban options. If a number, the number of days to delete messages for, if a
|
||||
* string, the ban reason. Supplying an object allows you to do both.
|
||||
* @param {Object} [options] Options for the ban
|
||||
* @param {number} [options.days=0] Number of days of messages to delete
|
||||
* @param {string} [options.reason] Reason for banning
|
||||
* @returns {Promise<GuildMember|User|string>} Result object will be resolved as specifically as possible.
|
||||
* @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
|
||||
* If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
|
||||
* be resolved, the user ID will be the result.
|
||||
* @example
|
||||
@@ -1054,8 +1072,7 @@ class Guild extends Base {
|
||||
.then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji);
|
||||
}
|
||||
|
||||
return DataResolver.resolveImage(attachment)
|
||||
.then(image => this.createEmoji(image, name, { roles, reason }));
|
||||
return DataResolver.resolveImage(attachment).then(image => this.createEmoji(image, name, { roles, reason }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1133,6 +1150,34 @@ class Guild extends Base {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a collection of this guild's roles, sorted by their position and IDs.
|
||||
* @returns {Collection<Role>}
|
||||
* @private
|
||||
*/
|
||||
_sortedRoles() {
|
||||
return Util.discordSort(this.roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a collection of this guild's or a specific category's channels, sorted by their position and IDs.
|
||||
* @param {GuildChannel} [channel] Category to get the channels of
|
||||
* @returns {Collection<GuildChannel>}
|
||||
* @private
|
||||
*/
|
||||
_sortedChannels(channel) {
|
||||
const category = channel.type === ChannelTypes.CATEGORY;
|
||||
return Util.discordSort(this.channels.filter(c =>
|
||||
c.type === channel.type && (category || c.parent === channel.parent)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a user speaking update in a voice channel.
|
||||
* @param {Snowflake} user ID of the user that the update is for
|
||||
* @param {boolean} speaking Whether the user is speaking
|
||||
* @private
|
||||
*/
|
||||
_memberSpeakUpdate(user, speaking) {
|
||||
const member = this.members.get(user);
|
||||
if (member && member.speaking !== speaking) {
|
||||
@@ -1146,23 +1191,15 @@ class Guild extends Base {
|
||||
this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking);
|
||||
}
|
||||
}
|
||||
|
||||
_sortedRoles() {
|
||||
return Util.discordSort(this.roles);
|
||||
}
|
||||
|
||||
_sortedChannels(channel) {
|
||||
const category = channel.type === ChannelTypes.CATEGORY;
|
||||
return Util.discordSort(this.channels.filter(c =>
|
||||
c.type === channel.type && (category || c.parent === channel.parent)));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Document this thing
|
||||
class VoiceStateCollection extends Collection {
|
||||
constructor(guild) {
|
||||
super();
|
||||
this.guild = guild;
|
||||
}
|
||||
|
||||
set(id, voiceState) {
|
||||
const member = this.guild.members.get(id);
|
||||
if (member) {
|
||||
|
||||
@@ -293,7 +293,7 @@ class GuildChannel extends Channel {
|
||||
topic: data.topic,
|
||||
nsfw: data.nsfw,
|
||||
bitrate: data.bitrate || (this.bitrate ? this.bitrate * 1000 : undefined),
|
||||
user_limit: data.userLimit != null ? data.userLimit : this.userLimit, // eslint-disable-line eqeqeq
|
||||
user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit,
|
||||
parent_id: data.parentID,
|
||||
lock_permissions: data.lockPermissions,
|
||||
permission_overwrites: data.permissionOverwrites,
|
||||
|
||||
@@ -77,36 +77,42 @@ class GuildMember extends Base {
|
||||
/**
|
||||
* Whether this member is deafened server-wide
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get serverDeaf() { return this.voiceState.deaf; }
|
||||
|
||||
/**
|
||||
* Whether this member is muted server-wide
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get serverMute() { return this.voiceState.mute; }
|
||||
|
||||
/**
|
||||
* Whether this member is self-muted
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get selfMute() { return this.voiceState.self_mute; }
|
||||
|
||||
/**
|
||||
* Whether this member is self-deafened
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get selfDeaf() { return this.voiceState.self_deaf; }
|
||||
|
||||
/**
|
||||
* The voice session ID of this member (if any)
|
||||
* @type {?Snowflake}
|
||||
* @readonly
|
||||
*/
|
||||
get voiceSessionID() { return this.voiceState.session_id; }
|
||||
|
||||
/**
|
||||
* The voice channel ID of this member, (if any)
|
||||
* @type {?Snowflake}
|
||||
* @readonly
|
||||
*/
|
||||
get voiceChannelID() { return this.voiceState.channel_id; }
|
||||
|
||||
@@ -515,8 +521,7 @@ class GuildMember extends Base {
|
||||
|
||||
/**
|
||||
* Bans this guild member.
|
||||
* @param {Object} [options] Ban options. If a number, the number of days to delete messages for, if a
|
||||
* string, the ban reason. Supplying an object allows you to do both.
|
||||
* @param {Object} [options] Options for the ban
|
||||
* @param {number} [options.days=0] Number of days of messages to delete
|
||||
* @param {string} [options.reason] Reason for banning
|
||||
* @returns {Promise<GuildMember>}
|
||||
|
||||
@@ -8,9 +8,9 @@ const Collection = require('../util/Collection');
|
||||
const ReactionStore = require('../stores/ReactionStore');
|
||||
const { MessageTypes } = require('../util/Constants');
|
||||
const Permissions = require('../util/Permissions');
|
||||
const GuildMember = require('./GuildMember');
|
||||
const Base = require('./Base');
|
||||
const { Error, TypeError } = require('../errors');
|
||||
const { createMessage } = require('./shared');
|
||||
|
||||
/**
|
||||
* Represents a message on Discord.
|
||||
@@ -368,41 +368,22 @@ class Message extends Base {
|
||||
* .then(msg => console.log(`Updated the content of a message from ${msg.author}`))
|
||||
* .catch(console.error);
|
||||
*/
|
||||
edit(content, options) {
|
||||
async edit(content, options) {
|
||||
if (!options && typeof content === 'object' && !(content instanceof Array)) {
|
||||
options = content;
|
||||
content = '';
|
||||
content = null;
|
||||
} else if (!options) {
|
||||
options = {};
|
||||
}
|
||||
if (options instanceof Embed) options = { embed: options };
|
||||
if (!options.content) options.content = content;
|
||||
|
||||
if (typeof options.content !== 'undefined') content = options.content;
|
||||
|
||||
if (typeof content !== 'undefined') content = Util.resolveString(content);
|
||||
|
||||
let { embed, code, reply } = options;
|
||||
|
||||
if (embed) embed = new Embed(embed)._apiTransform();
|
||||
|
||||
// Wrap everything in a code block
|
||||
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
|
||||
content = Util.escapeMarkdown(Util.resolveString(content), true);
|
||||
content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
|
||||
}
|
||||
|
||||
// Add the reply prefix
|
||||
if (reply && this.channel.type !== 'dm') {
|
||||
const id = this.client.users.resolveID(reply);
|
||||
const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`;
|
||||
content = `${mention}${content ? `, ${content}` : ''}`;
|
||||
}
|
||||
const { data, files } = await createMessage(this, options);
|
||||
|
||||
return this.client.api.channels[this.channel.id].messages[this.id]
|
||||
.patch({ data: { content, embed } })
|
||||
.then(data => {
|
||||
.patch({ data, files })
|
||||
.then(d => {
|
||||
const clone = this._clone();
|
||||
clone._patch(data);
|
||||
clone._patch(d);
|
||||
return clone;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,19 +117,27 @@ class MessageMentions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is mentioned.
|
||||
* Checks if a user, guild member, role, or channel is mentioned.
|
||||
* Takes into account user mentions, role mentions, and @everyone/@here mentions.
|
||||
* @param {UserResolvable|GuildMember|Role|GuildChannel} data User/GuildMember/Role/Channel to check
|
||||
* @param {boolean} [strict=true] If role mentions and everyone/here mentions should be included
|
||||
* @param {Object} [options] Options
|
||||
* @param {boolean} [options.ignoreDirect=false] - Whether to ignore direct mentions to the item
|
||||
* @param {boolean} [options.ignoreRoles=false] - Whether to ignore role mentions to a guild member
|
||||
* @param {boolean} [options.ignoreEveryone=false] - Whether to ignore everyone/here mentions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(data, strict = true) {
|
||||
if (strict && this.everyone) return true;
|
||||
if (strict && data instanceof GuildMember) {
|
||||
has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) {
|
||||
if (!ignoreEveryone && this.everyone) return true;
|
||||
if (!ignoreRoles && data instanceof GuildMember) {
|
||||
for (const role of this.roles.values()) if (data.roles.has(role.id)) return true;
|
||||
}
|
||||
const id = data.id || data;
|
||||
return this.users.has(id) || this.channels.has(id) || this.roles.has(id);
|
||||
|
||||
if (!ignoreDirect) {
|
||||
const id = data.id || data;
|
||||
return this.users.has(id) || this.channels.has(id) || this.roles.has(id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Collection = require('../util/Collection');
|
||||
const Emoji = require('./Emoji');
|
||||
const ReactionEmoji = require('./ReactionEmoji');
|
||||
const ReactionUserStore = require('../stores/ReactionUserStore');
|
||||
const { Error } = require('../errors');
|
||||
|
||||
/**
|
||||
@@ -28,9 +28,9 @@ class MessageReaction {
|
||||
|
||||
/**
|
||||
* The users that have given this reaction, mapped by their ID
|
||||
* @type {Collection<Snowflake, User>}
|
||||
* @type {ReactionUserStore<Snowflake, User>}
|
||||
*/
|
||||
this.users = new Collection();
|
||||
this.users = new ReactionUserStore(client, undefined, this);
|
||||
|
||||
this._emoji = new ReactionEmoji(this, data.emoji.name, data.emoji.id);
|
||||
}
|
||||
@@ -77,26 +77,6 @@ class MessageReaction {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs.
|
||||
* @param {Object} [options] Options for fetching the users
|
||||
* @param {number} [options.limit=100] The maximum amount of users to fetch, defaults to 100
|
||||
* @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id
|
||||
* @returns {Promise<Collection<Snowflake, User>>}
|
||||
*/
|
||||
async fetchUsers({ limit = 100, after } = {}) {
|
||||
const message = this.message;
|
||||
const users = await message.client.api.channels[message.channel.id].messages[message.id]
|
||||
.reactions[this.emoji.identifier]
|
||||
.get({ query: { limit, after } });
|
||||
for (const rawUser of users) {
|
||||
const user = message.client.users.create(rawUser);
|
||||
this.users.set(user.id, user);
|
||||
}
|
||||
this.count = this.users.size;
|
||||
return this.users;
|
||||
}
|
||||
|
||||
_add(user) {
|
||||
if (!this.users.has(user.id)) {
|
||||
this.users.set(user.id, user);
|
||||
|
||||
@@ -83,7 +83,17 @@ class ReactionCollector extends Collector {
|
||||
* @returns {?Snowflake|string}
|
||||
*/
|
||||
dispose(reaction) {
|
||||
return reaction.message.id === this.message.id && !reaction.count ? ReactionCollector.key(reaction) : null;
|
||||
if (reaction.message.id !== this.message.id) return null;
|
||||
|
||||
/**
|
||||
* Emitted whenever a reaction is removed from a message. Will emit on all reaction removals,
|
||||
* as opposed to {@link Collector#dispose} which will only be emitted when the entire reaction
|
||||
* is removed.
|
||||
* @event ReactionCollector#remove
|
||||
* @param {MessageReaction} reaction The reaction that was removed
|
||||
*/
|
||||
if (this.collected.has(reaction)) this.emit('remove', reaction);
|
||||
return reaction.count ? null : ReactionCollector.key(reaction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
const Util = require('../util/Util');
|
||||
const DataResolver = require('../util/DataResolver');
|
||||
const Embed = require('./MessageEmbed');
|
||||
const MessageAttachment = require('./MessageAttachment');
|
||||
const MessageEmbed = require('./MessageEmbed');
|
||||
const { browser } = require('../util/Constants');
|
||||
const { createMessage } = require('./shared');
|
||||
|
||||
/**
|
||||
* Represents a webhook.
|
||||
@@ -98,115 +94,37 @@ class Webhook {
|
||||
* .catch(console.error);
|
||||
*/
|
||||
/* eslint-enable max-len */
|
||||
send(content, options) { // eslint-disable-line complexity
|
||||
async send(content, options) { // eslint-disable-line complexity
|
||||
if (!options && typeof content === 'object' && !(content instanceof Array)) {
|
||||
options = content;
|
||||
content = '';
|
||||
content = null;
|
||||
} else if (!options) {
|
||||
options = {};
|
||||
}
|
||||
if (!options.content) options.content = content;
|
||||
|
||||
if (options instanceof MessageAttachment) options = { files: [options.file] };
|
||||
if (options instanceof MessageEmbed) options = { embeds: [options] };
|
||||
if (options.embed) options = { embeds: [options.embed] };
|
||||
const { data, files } = await createMessage(this, options);
|
||||
|
||||
if (content instanceof Array || options instanceof Array) {
|
||||
const which = content instanceof Array ? content : options;
|
||||
const attachments = which.filter(item => item instanceof MessageAttachment);
|
||||
const embeds = which.filter(item => item instanceof MessageEmbed);
|
||||
if (attachments.length) options = { files: attachments };
|
||||
if (embeds.length) options = { embeds };
|
||||
if ((embeds.length || attachments.length) && content instanceof Array) content = '';
|
||||
}
|
||||
|
||||
if (!options.username) options.username = this.name;
|
||||
if (options.avatarURL) {
|
||||
options.avatar_url = options.avatarURL;
|
||||
options.avatarURL = null;
|
||||
}
|
||||
|
||||
if (content) {
|
||||
content = Util.resolveString(content);
|
||||
let { split, code, disableEveryone } = options;
|
||||
if (split && typeof split !== 'object') split = {};
|
||||
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
|
||||
content = Util.escapeMarkdown(content, true);
|
||||
content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
|
||||
if (split) {
|
||||
split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`;
|
||||
split.append = '\n```';
|
||||
}
|
||||
if (data.content instanceof Array) {
|
||||
const messages = [];
|
||||
for (let i = 0; i < data.content.length; i++) {
|
||||
const opt = i === data.content.length - 1 ? { embeds: data.embeds, files } : {};
|
||||
Object.assign(opt, { avatarURL: data.avatar_url, content: data.content[i], username: data.username });
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const message = await this.send(data.content[i], opt);
|
||||
messages.push(message);
|
||||
}
|
||||
if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) {
|
||||
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
|
||||
}
|
||||
|
||||
if (split) content = Util.splitMessage(content, split);
|
||||
}
|
||||
options.content = content;
|
||||
|
||||
if (options.embeds) options.embeds = options.embeds.map(embed => new Embed(embed)._apiTransform());
|
||||
|
||||
if (options.files) {
|
||||
for (let i = 0; i < options.files.length; i++) {
|
||||
let file = options.files[i];
|
||||
if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file };
|
||||
if (!file.name) {
|
||||
if (typeof file.attachment === 'string') {
|
||||
file.name = Util.basename(file.attachment);
|
||||
} else if (file.attachment && file.attachment.path) {
|
||||
file.name = Util.basename(file.attachment.path);
|
||||
} else if (file instanceof MessageAttachment) {
|
||||
file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' };
|
||||
} else {
|
||||
file.name = 'file.jpg';
|
||||
}
|
||||
} else if (file instanceof MessageAttachment) {
|
||||
file = file.file;
|
||||
}
|
||||
options.files[i] = file;
|
||||
}
|
||||
|
||||
return Promise.all(options.files.map(file =>
|
||||
DataResolver.resolveFile(file.attachment).then(resource => {
|
||||
file.file = resource;
|
||||
return file;
|
||||
})
|
||||
)).then(files => this.client.api.webhooks(this.id, this.token).post({
|
||||
data: options,
|
||||
query: { wait: true },
|
||||
files,
|
||||
auth: false,
|
||||
}));
|
||||
return messages;
|
||||
}
|
||||
|
||||
if (content instanceof Array) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messages = [];
|
||||
(function sendChunk() {
|
||||
const opt = content.length ? null : { embeds: options.embeds, files: options.files };
|
||||
this.client.api.webhooks(this.id, this.token).post({
|
||||
data: { content: content.shift(), opt },
|
||||
query: { wait: true },
|
||||
auth: false,
|
||||
})
|
||||
.then(message => {
|
||||
messages.push(message);
|
||||
if (content.length === 0) return resolve(messages);
|
||||
return sendChunk.call(this);
|
||||
})
|
||||
.catch(reject);
|
||||
}.call(this));
|
||||
});
|
||||
}
|
||||
|
||||
return this.client.api.webhooks(this.id, this.token).post({
|
||||
data: options,
|
||||
data, files,
|
||||
query: { wait: true },
|
||||
auth: false,
|
||||
}).then(data => {
|
||||
if (!this.client.channels) return data;
|
||||
return this.client.channels.get(data.channel_id).messages.create(data, false);
|
||||
}).then(d => {
|
||||
if (!this.client.channels) return d;
|
||||
return this.client.channels.get(d.channel_id).messages.create(d, false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
const MessageCollector = require('../MessageCollector');
|
||||
const Shared = require('../shared');
|
||||
const Util = require('../../util/Util');
|
||||
const { browser } = require('../../util/Constants');
|
||||
const Snowflake = require('../../util/Snowflake');
|
||||
const Collection = require('../../util/Collection');
|
||||
const DataResolver = require('../../util/DataResolver');
|
||||
const MessageAttachment = require('../../structures/MessageAttachment');
|
||||
const MessageEmbed = require('../../structures/MessageEmbed');
|
||||
const { RangeError, TypeError } = require('../../errors');
|
||||
|
||||
/**
|
||||
@@ -80,61 +75,12 @@ class TextBasedChannel {
|
||||
send(content, options) { // eslint-disable-line complexity
|
||||
if (!options && typeof content === 'object' && !(content instanceof Array)) {
|
||||
options = content;
|
||||
content = '';
|
||||
content = null;
|
||||
} else if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (options instanceof MessageEmbed) options = { embed: options };
|
||||
if (options instanceof MessageAttachment) options = { files: [options.file] };
|
||||
|
||||
if (content instanceof Array || options instanceof Array) {
|
||||
const which = content instanceof Array ? content : options;
|
||||
const attachments = which.filter(item => item instanceof MessageAttachment);
|
||||
if (attachments.length) {
|
||||
options = { files: attachments };
|
||||
if (content instanceof Array) content = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.content) options.content = content;
|
||||
|
||||
if (options.embed && options.embed.files) {
|
||||
if (options.files) options.files = options.files.concat(options.embed.files);
|
||||
else options.files = options.embed.files;
|
||||
}
|
||||
|
||||
if (options.files) {
|
||||
for (let i = 0; i < options.files.length; i++) {
|
||||
let file = options.files[i];
|
||||
if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file };
|
||||
if (!file.name) {
|
||||
if (typeof file.attachment === 'string') {
|
||||
file.name = Util.basename(file.attachment);
|
||||
} else if (file.attachment && file.attachment.path) {
|
||||
file.name = Util.basename(file.attachment.path);
|
||||
} else if (file instanceof MessageAttachment) {
|
||||
file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' };
|
||||
} else {
|
||||
file.name = 'file.jpg';
|
||||
}
|
||||
} else if (file instanceof MessageAttachment) {
|
||||
file = file.file;
|
||||
}
|
||||
options.files[i] = file;
|
||||
}
|
||||
|
||||
return Promise.all(options.files.map(file =>
|
||||
DataResolver.resolveFile(file.attachment).then(resource => {
|
||||
file.file = resource;
|
||||
return file;
|
||||
})
|
||||
)).then(files => {
|
||||
options.files = files;
|
||||
return Shared.sendMessage(this, options);
|
||||
});
|
||||
}
|
||||
|
||||
return Shared.sendMessage(this, options);
|
||||
}
|
||||
|
||||
@@ -158,26 +104,45 @@ class TextBasedChannel {
|
||||
|
||||
/**
|
||||
* Starts a typing indicator in the channel.
|
||||
* @param {number} [count] The number of times startTyping should be considered to have been called
|
||||
* @param {number} [count=1] The number of times startTyping should be considered to have been called
|
||||
* @returns {Promise} Resolves once the bot stops typing gracefully, or rejects when an error occurs
|
||||
* @example
|
||||
* // Start typing in a channel
|
||||
* // Start typing in a channel, or increase the typing count by one
|
||||
* channel.startTyping();
|
||||
* @example
|
||||
* // Start typing in a channel with a typing count of five, or set it to five
|
||||
* channel.startTyping(5);
|
||||
*/
|
||||
startTyping(count) {
|
||||
if (typeof count !== 'undefined' && count < 1) throw new RangeError('TYPING_COUNT');
|
||||
if (!this.client.user._typing.has(this.id)) {
|
||||
const endpoint = this.client.api.channels[this.id].typing;
|
||||
this.client.user._typing.set(this.id, {
|
||||
count: count || 1,
|
||||
interval: this.client.setInterval(() => {
|
||||
endpoint.post();
|
||||
}, 9000),
|
||||
});
|
||||
endpoint.post();
|
||||
} else {
|
||||
if (this.client.user._typing.has(this.id)) {
|
||||
const entry = this.client.user._typing.get(this.id);
|
||||
entry.count = count || entry.count + 1;
|
||||
return entry.promise;
|
||||
}
|
||||
|
||||
const entry = {};
|
||||
entry.promise = new Promise((resolve, reject) => {
|
||||
const endpoint = this.client.api.channels[this.id].typing;
|
||||
Object.assign(entry, {
|
||||
count: count || 1,
|
||||
interval: this.client.setInterval(() => {
|
||||
endpoint.post().catch(error => {
|
||||
this.client.clearInterval(entry.interval);
|
||||
this.client.user._typing.delete(this.id);
|
||||
reject(error);
|
||||
});
|
||||
}, 9000),
|
||||
resolve,
|
||||
});
|
||||
endpoint.post().catch(error => {
|
||||
this.client.clearInterval(entry.interval);
|
||||
this.client.user._typing.delete(this.id);
|
||||
reject(error);
|
||||
});
|
||||
this.client.user._typing.set(this.id, entry);
|
||||
});
|
||||
return entry.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,10 +151,10 @@ class TextBasedChannel {
|
||||
* <info>It can take a few seconds for the client user to stop typing.</info>
|
||||
* @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop
|
||||
* @example
|
||||
* // Stop typing in a channel
|
||||
* // Reduce the typing count by one and stop typing if it reached 0
|
||||
* channel.stopTyping();
|
||||
* @example
|
||||
* // Force typing to fully stop in a channel
|
||||
* // Force typing to fully stop regardless of typing count
|
||||
* channel.stopTyping(true);
|
||||
*/
|
||||
stopTyping(force = false) {
|
||||
@@ -199,6 +164,7 @@ class TextBasedChannel {
|
||||
if (entry.count <= 0 || force) {
|
||||
this.client.clearInterval(entry.interval);
|
||||
this.client.user._typing.delete(this.id);
|
||||
entry.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
108
src/structures/shared/CreateMessage.js
Normal file
108
src/structures/shared/CreateMessage.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const Embed = require('../MessageEmbed');
|
||||
const DataResolver = require('../../util/DataResolver');
|
||||
const MessageEmbed = require('../MessageEmbed');
|
||||
const MessageAttachment = require('../MessageAttachment');
|
||||
const { browser } = require('../../util/Constants');
|
||||
const Util = require('../../util/Util');
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
module.exports = async function createMessage(channel, options) {
|
||||
const User = require('../User');
|
||||
const GuildMember = require('../GuildMember');
|
||||
const Webhook = require('../Webhook');
|
||||
const WebhookClient = require('../../client/WebhookClient');
|
||||
|
||||
const webhook = channel instanceof Webhook || channel instanceof WebhookClient;
|
||||
|
||||
if (typeof options.nonce !== 'undefined') {
|
||||
options.nonce = parseInt(options.nonce);
|
||||
if (isNaN(options.nonce) || options.nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE');
|
||||
}
|
||||
|
||||
if (options instanceof MessageEmbed) options = webhook ? { embeds: [options] } : { embed: options };
|
||||
if (options instanceof MessageAttachment) options = { files: [options.file] };
|
||||
|
||||
if (options.reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') {
|
||||
const id = channel.client.users.resolveID(options.reply);
|
||||
const mention = `<@${options.reply instanceof GuildMember && options.reply.nickname ? '!' : ''}${id}>`;
|
||||
if (options.split) options.split.prepend = `${mention}, ${options.split.prepend || ''}`;
|
||||
options.content = `${mention}${typeof options.content !== 'undefined' ? `, ${options.content}` : ''}`;
|
||||
}
|
||||
|
||||
if (options.content) {
|
||||
options.content = Util.resolveString(options.content);
|
||||
if (options.split && typeof options.split !== 'object') options.split = {};
|
||||
// Wrap everything in a code block
|
||||
if (typeof options.code !== 'undefined' && (typeof options.code !== 'boolean' || options.code === true)) {
|
||||
options.content = Util.escapeMarkdown(options.content, true);
|
||||
options.content =
|
||||
`\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n${options.content}\n\`\`\``;
|
||||
if (options.split) {
|
||||
options.split.prepend = `\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n`;
|
||||
options.split.append = '\n```';
|
||||
}
|
||||
}
|
||||
|
||||
// Add zero-width spaces to @everyone/@here
|
||||
if (options.disableEveryone ||
|
||||
(typeof options.disableEveryone === 'undefined' && channel.client.options.disableEveryone)) {
|
||||
options.content = options.content.replace(/@(everyone|here)/g, '@\u200b$1');
|
||||
}
|
||||
|
||||
if (options.split) options.content = Util.splitMessage(options.content, options.split);
|
||||
}
|
||||
|
||||
if (options.embed && options.embed.files) {
|
||||
if (options.files) options.files = options.files.concat(options.embed.files);
|
||||
else options.files = options.embed.files;
|
||||
}
|
||||
|
||||
if (options.embed && webhook) options.embeds = [new Embed(options.embed)._apiTransform()];
|
||||
else if (options.embed) options.embed = new Embed(options.embed)._apiTransform();
|
||||
else if (options.embeds) options.embeds = options.embeds.map(e => new Embed(e)._apiTransform());
|
||||
|
||||
let files;
|
||||
|
||||
if (options.files) {
|
||||
for (let i = 0; i < options.files.length; i++) {
|
||||
let file = options.files[i];
|
||||
if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file };
|
||||
if (!file.name) {
|
||||
if (typeof file.attachment === 'string') {
|
||||
file.name = Util.basename(file.attachment);
|
||||
} else if (file.attachment && file.attachment.path) {
|
||||
file.name = Util.basename(file.attachment.path);
|
||||
} else if (file instanceof MessageAttachment) {
|
||||
file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' };
|
||||
} else {
|
||||
file.name = 'file.jpg';
|
||||
}
|
||||
} else if (file instanceof MessageAttachment) {
|
||||
file = file.file;
|
||||
}
|
||||
options.files[i] = file;
|
||||
}
|
||||
|
||||
files = await Promise.all(options.files.map(file =>
|
||||
DataResolver.resolveFile(file.attachment).then(resource => {
|
||||
file.file = resource;
|
||||
return file;
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
if (webhook) {
|
||||
if (!options.username) options.username = this.name;
|
||||
if (options.avatarURL) options.avatar_url = options.avatarURL;
|
||||
}
|
||||
|
||||
return { data: {
|
||||
content: options.content,
|
||||
tts: options.tts,
|
||||
nonce: options.nonce,
|
||||
embed: options.embed,
|
||||
embeds: options.embeds,
|
||||
username: options.username,
|
||||
avatar_url: options.avatar_url,
|
||||
}, files };
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const long = require('long');
|
||||
const Util = require('../../util/Util');
|
||||
const { TypeError } = require('../../errors');
|
||||
|
||||
/**
|
||||
@@ -40,17 +40,17 @@ module.exports = function search(target, options) {
|
||||
if (typeof options === 'string') options = { content: options };
|
||||
if (options.before) {
|
||||
if (!(options.before instanceof Date)) options.before = new Date(options.before);
|
||||
options.maxID = long.fromNumber(options.before.getTime() - 14200704e5).shiftLeft(22).toString();
|
||||
options.maxID = Util.binaryToID((options.before.getTime() - 14200704e5).toString(2) + '0'.repeat(22));
|
||||
}
|
||||
if (options.after) {
|
||||
if (!(options.after instanceof Date)) options.after = new Date(options.after);
|
||||
options.minID = long.fromNumber(options.after.getTime() - 14200704e5).shiftLeft(22).toString();
|
||||
options.minID = Util.binaryToID((options.after.getTime() - 14200704e5).toString(2) + '0'.repeat(22));
|
||||
}
|
||||
if (options.during) {
|
||||
if (!(options.during instanceof Date)) options.during = new Date(options.during);
|
||||
const t = options.during.getTime() - 14200704e5;
|
||||
options.minID = long.fromNumber(t).shiftLeft(22).toString();
|
||||
options.maxID = long.fromNumber(t + 864e5).shiftLeft(22).toString();
|
||||
options.minID = Util.binaryToID(t.toString(2) + '0'.repeat(22));
|
||||
options.maxID = Util.binaryToID((t + 864e5).toString(2) + '0'.repeat(22));
|
||||
}
|
||||
if (options.channel) options.channel = target.client.channels.resolveID(options.channel);
|
||||
if (options.author) options.author = target.client.users.resolveID(options.author);
|
||||
|
||||
@@ -1,65 +1,23 @@
|
||||
const Util = require('../../util/Util');
|
||||
const Embed = require('../MessageEmbed');
|
||||
const { RangeError } = require('../../errors');
|
||||
const createMessage = require('./CreateMessage');
|
||||
|
||||
module.exports = function sendMessage(channel, options) { // eslint-disable-line complexity
|
||||
module.exports = async function sendMessage(channel, options) { // eslint-disable-line complexity
|
||||
const User = require('../User');
|
||||
const GuildMember = require('../GuildMember');
|
||||
if (channel instanceof User || channel instanceof GuildMember) return channel.createDM().then(dm => dm.send(options));
|
||||
let { content, nonce, reply, code, disableEveryone, tts, embed, files, split } = options;
|
||||
|
||||
if (embed) embed = new Embed(embed)._apiTransform();
|
||||
const { data, files } = await createMessage(channel, options);
|
||||
|
||||
if (typeof nonce !== 'undefined') {
|
||||
nonce = parseInt(nonce);
|
||||
if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE');
|
||||
}
|
||||
|
||||
// Add the reply prefix
|
||||
if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') {
|
||||
const id = channel.client.users.resolveID(reply);
|
||||
const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`;
|
||||
if (split) split.prepend = `${mention}, ${split.prepend || ''}`;
|
||||
content = `${mention}${typeof content !== 'undefined' ? `, ${content}` : ''}`;
|
||||
}
|
||||
|
||||
if (content) {
|
||||
content = Util.resolveString(content);
|
||||
if (split && typeof split !== 'object') split = {};
|
||||
// Wrap everything in a code block
|
||||
if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) {
|
||||
content = Util.escapeMarkdown(content, true);
|
||||
content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``;
|
||||
if (split) {
|
||||
split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`;
|
||||
split.append = '\n```';
|
||||
}
|
||||
if (data.content instanceof Array) {
|
||||
const messages = [];
|
||||
for (let i = 0; i < data.content.length; i++) {
|
||||
const opt = i === data.content.length - 1 ? { tts: data.tts, embed: data.embed, files } : { tts: data.tts };
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const message = await channel.send(data.content[i], opt);
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// Add zero-width spaces to @everyone/@here
|
||||
if (disableEveryone || (typeof disableEveryone === 'undefined' && channel.client.options.disableEveryone)) {
|
||||
content = content.replace(/@(everyone|here)/g, '@\u200b$1');
|
||||
}
|
||||
|
||||
if (split) content = Util.splitMessage(content, split);
|
||||
return messages;
|
||||
}
|
||||
|
||||
if (content instanceof Array) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messages = [];
|
||||
(function sendChunk() {
|
||||
const opt = content.length ? { tts } : { tts, embed, files };
|
||||
channel.send(content.shift(), opt).then(message => {
|
||||
messages.push(message);
|
||||
if (content.length === 0) return resolve(messages);
|
||||
return sendChunk();
|
||||
}).catch(reject);
|
||||
}());
|
||||
});
|
||||
}
|
||||
|
||||
return channel.client.api.channels[channel.id].messages.post({
|
||||
data: { content, tts, nonce, embed },
|
||||
files,
|
||||
}).then(data => channel.client.actions.MessageCreate.handle(data).message);
|
||||
return channel.client.api.channels[channel.id].messages.post({ data, files })
|
||||
.then(d => channel.client.actions.MessageCreate.handle(d).message);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
search: require('./Search'),
|
||||
sendMessage: require('./SendMessage'),
|
||||
createMessage: require('./CreateMessage'),
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ exports.DefaultOptions = {
|
||||
* WebSocket options (these are left as snake_case to match the API)
|
||||
* @typedef {Object} WebsocketOptions
|
||||
* @property {number} [large_threshold=250] Number of members in a guild to be considered large
|
||||
* @property {boolean} [compress=true] Whether to compress data sent on the connection
|
||||
* @property {boolean} [compress=false] Whether to compress data sent on the connection
|
||||
* (defaults to `false` for browsers)
|
||||
*/
|
||||
ws: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const Long = require('long');
|
||||
const Util = require('../util/Util');
|
||||
|
||||
// Discord epoch (2015-01-01T00:00:00.000Z)
|
||||
const EPOCH = 1420070400000;
|
||||
@@ -31,8 +31,9 @@ class SnowflakeUtil {
|
||||
*/
|
||||
static generate() {
|
||||
if (INCREMENT >= 4095) INCREMENT = 0;
|
||||
const BINARY = `${pad((Date.now() - EPOCH).toString(2), 42)}0000100000${pad((INCREMENT++).toString(2), 12)}`;
|
||||
return Long.fromString(BINARY, 2).toString();
|
||||
// eslint-disable-next-line max-len
|
||||
const BINARY = `${(Date.now() - EPOCH).toString(2).padStart(42, '0')}0000100000${(INCREMENT++).toString(2).padStart(12, '0')}`;
|
||||
return Util.binaryToID(BINARY);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +53,7 @@ class SnowflakeUtil {
|
||||
* @returns {DeconstructedSnowflake} Deconstructed snowflake
|
||||
*/
|
||||
static deconstruct(snowflake) {
|
||||
const BINARY = pad(Long.fromString(snowflake).toString(2), 64);
|
||||
const BINARY = Util.idToBinary(snowflake).toString(2).padStart(64, '0');
|
||||
const res = {
|
||||
timestamp: parseInt(BINARY.substring(0, 42), 2) + EPOCH,
|
||||
workerID: parseInt(BINARY.substring(42, 47), 2),
|
||||
@@ -68,8 +69,4 @@ class SnowflakeUtil {
|
||||
}
|
||||
}
|
||||
|
||||
function pad(v, n, c = '0') {
|
||||
return String(v).length >= n ? String(v) : (String(c).repeat(n) + v).slice(-n);
|
||||
}
|
||||
|
||||
module.exports = SnowflakeUtil;
|
||||
|
||||
80
src/util/Structures.js
Normal file
80
src/util/Structures.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}.
|
||||
*/
|
||||
class Structures {
|
||||
constructor() {
|
||||
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a structure class.
|
||||
* @param {string} structure Name of the structure to retrieve
|
||||
* @returns {Function}
|
||||
*/
|
||||
static get(structure) {
|
||||
if (typeof structure === 'string') return structures[structure];
|
||||
throw new TypeError(`"structure" argument must be a string (received ${typeof structure})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends a structure.
|
||||
* @param {string} structure Name of the structure class to extend
|
||||
* @param {Function} extender Function that takes the base class to extend as its only parameter and returns the
|
||||
* extended class/prototype
|
||||
* @returns {Function} Extended class/prototype returned from the extender
|
||||
* @example
|
||||
* const { Structures } = require('discord.js');
|
||||
*
|
||||
* Structures.extend('Guild', Guild => {
|
||||
* class CoolGuild extends Guild {
|
||||
* constructor(client, data) {
|
||||
* super(client, data);
|
||||
* this.cool = true;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* return CoolGuild;
|
||||
* });
|
||||
*/
|
||||
static extend(structure, extender) {
|
||||
if (!structures[structure]) throw new RangeError(`"${structure}" is not a valid extensible structure.`);
|
||||
if (typeof extender !== 'function') {
|
||||
const received = `(received ${typeof extender})`;
|
||||
throw new TypeError(
|
||||
`"extender" argument must be a function that returns the extended structure class/prototype ${received}`
|
||||
);
|
||||
}
|
||||
|
||||
const extended = extender(structures[structure]);
|
||||
if (typeof extended !== 'function') {
|
||||
throw new TypeError('The extender function must return the extended structure class/prototype.');
|
||||
}
|
||||
if (Object.getPrototypeOf(extended) !== structures[structure]) {
|
||||
throw new Error(
|
||||
'The class/prototype returned from the extender function must extend the existing structure class/prototype.'
|
||||
);
|
||||
}
|
||||
|
||||
structures[structure] = extended;
|
||||
return extended;
|
||||
}
|
||||
}
|
||||
|
||||
const structures = {
|
||||
Emoji: require('../structures/Emoji'),
|
||||
DMChannel: require('../structures/DMChannel'),
|
||||
GroupDMChannel: require('../structures/GroupDMChannel'),
|
||||
TextChannel: require('../structures/TextChannel'),
|
||||
VoiceChannel: require('../structures/VoiceChannel'),
|
||||
CategoryChannel: require('../structures/CategoryChannel'),
|
||||
GuildChannel: require('../structures/GuildChannel'),
|
||||
GuildMember: require('../structures/GuildMember'),
|
||||
Guild: require('../structures/Guild'),
|
||||
Message: require('../structures/Message'),
|
||||
MessageReaction: require('../structures/MessageReaction'),
|
||||
Presence: require('../structures/Presence').Presence,
|
||||
Role: require('../structures/Role'),
|
||||
User: require('../structures/User'),
|
||||
};
|
||||
|
||||
module.exports = Structures;
|
||||
168
src/util/Util.js
168
src/util/Util.js
@@ -1,4 +1,3 @@
|
||||
const Long = require('long');
|
||||
const snekfetch = require('snekfetch');
|
||||
const { Colors, DefaultOptions, Endpoints } = require('./Constants');
|
||||
const { Error: DiscordError, RangeError, TypeError } = require('../errors');
|
||||
@@ -22,9 +21,7 @@ class Util {
|
||||
static splitMessage(text, { maxLength = 1950, char = '\n', prepend = '', append = '' } = {}) {
|
||||
if (text.length <= maxLength) return text;
|
||||
const splitText = text.split(char);
|
||||
if (splitText.length === 1) {
|
||||
throw new RangeError('SPLIT_MAX_LEN');
|
||||
}
|
||||
if (splitText.length === 1) throw new RangeError('SPLIT_MAX_LEN');
|
||||
const messages = [''];
|
||||
let msg = 0;
|
||||
for (let i = 0; i < splitText.length; i++) {
|
||||
@@ -84,10 +81,7 @@ class Util {
|
||||
const [name, id] = text.split(':');
|
||||
return { name, id };
|
||||
} else {
|
||||
return {
|
||||
name: text,
|
||||
id: null,
|
||||
};
|
||||
return { name: text, id: null };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +220,6 @@ class Util {
|
||||
* @param {StringResolvable} data The string resolvable to resolve
|
||||
* @returns {string}
|
||||
*/
|
||||
|
||||
static resolveString(data) {
|
||||
if (typeof data === 'string') return data;
|
||||
if (data instanceof Array) return data.join('\n');
|
||||
@@ -234,39 +227,34 @@ class Util {
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following
|
||||
* Can be a number, hex string, an RGB array like:
|
||||
* ```js
|
||||
* [255, 0, 255] // purple
|
||||
* ```
|
||||
* [
|
||||
* 'DEFAULT',
|
||||
* 'AQUA',
|
||||
* 'GREEN',
|
||||
* 'BLUE',
|
||||
* 'PURPLE',
|
||||
* 'GOLD',
|
||||
* 'ORANGE',
|
||||
* 'RED',
|
||||
* 'GREY',
|
||||
* 'DARKER_GREY',
|
||||
* 'NAVY',
|
||||
* 'DARK_AQUA',
|
||||
* 'DARK_GREEN',
|
||||
* 'DARK_BLUE',
|
||||
* 'DARK_PURPLE',
|
||||
* 'DARK_GOLD',
|
||||
* 'DARK_ORANGE',
|
||||
* 'DARK_RED',
|
||||
* 'DARK_GREY',
|
||||
* 'LIGHT_GREY',
|
||||
* 'DARK_NAVY',
|
||||
* 'RANDOM',
|
||||
* ]
|
||||
* ```
|
||||
* or something like
|
||||
* ```
|
||||
* [255, 0, 255]
|
||||
* ```
|
||||
* for purple
|
||||
* @typedef {string|number|Array} ColorResolvable
|
||||
* or one of the following strings:
|
||||
* - `DEFAULT`
|
||||
* - `AQUA`
|
||||
* - `GREEN`
|
||||
* - `BLUE`
|
||||
* - `PURPLE`
|
||||
* - `GOLD`
|
||||
* - `ORANGE`
|
||||
* - `RED`
|
||||
* - `GREY`
|
||||
* - `DARKER_GREY`
|
||||
* - `NAVY`
|
||||
* - `DARK_AQUA`
|
||||
* - `DARK_GREEN`
|
||||
* - `DARK_BLUE`
|
||||
* - `DARK_PURPLE`
|
||||
* - `DARK_GOLD`
|
||||
* - `DARK_ORANGE`
|
||||
* - `DARK_RED`
|
||||
* - `DARK_GREY`
|
||||
* - `LIGHT_GREY`
|
||||
* - `DARK_NAVY`
|
||||
* - `RANDOM`
|
||||
* @typedef {string|number|number[]} ColorResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -274,7 +262,6 @@ class Util {
|
||||
* @param {ColorResolvable} color Color to resolve
|
||||
* @returns {number} A color
|
||||
*/
|
||||
|
||||
static resolveColor(color) {
|
||||
if (typeof color === 'string') {
|
||||
if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1));
|
||||
@@ -283,25 +270,36 @@ class Util {
|
||||
color = (color[0] << 16) + (color[1] << 8) + color[2];
|
||||
}
|
||||
|
||||
if (color < 0 || color > 0xFFFFFF) {
|
||||
throw new RangeError('COLOR_RANGE');
|
||||
} else if (color && isNaN(color)) {
|
||||
throw new TypeError('COLOR_CONVERT');
|
||||
}
|
||||
if (color < 0 || color > 0xFFFFFF) throw new RangeError('COLOR_RANGE');
|
||||
else if (color && isNaN(color)) throw new TypeError('COLOR_CONVERT');
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts by Discord's position and then by ID.
|
||||
* Sorts by Discord's position and ID.
|
||||
* @param {Collection} collection Collection of objects to sort
|
||||
* @returns {Collection}
|
||||
*/
|
||||
static discordSort(collection) {
|
||||
return collection
|
||||
.sort((a, b) => a.rawPosition - b.rawPosition || Long.fromString(a.id).sub(Long.fromString(b.id)).toNumber());
|
||||
return collection.sort((a, b) =>
|
||||
a.rawPosition - b.rawPosition ||
|
||||
parseInt(a.id.slice(0, -10)) - parseInt(b.id.slice(0, -10)) ||
|
||||
parseInt(a.id.slice(10)) - parseInt(b.id.slice(10))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position of a Channel or Role.
|
||||
* @param {Channel|Role} item Object to set the position of
|
||||
* @param {number} position New position for the object
|
||||
* @param {boolean} relative Whether `position` is relative to its current position
|
||||
* @param {Collection<string, Channel|Role>} sorted A collection of the objects sorted properly
|
||||
* @param {APIRouter} route Route to call PATCH on
|
||||
* @param {string} [reason] Reason for the change
|
||||
* @returns {Promise<Object[]>} Updated item list, with `id` and `position` properties
|
||||
* @private
|
||||
*/
|
||||
static setPosition(item, position, relative, sorted, route, reason) {
|
||||
let updatedItems = sorted.array();
|
||||
Util.moveElementInArray(updatedItems, item, position, relative);
|
||||
@@ -309,13 +307,77 @@ class Util {
|
||||
return route.patch({ data: updatedItems, reason }).then(() => updatedItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative to Node's `path.basename` that we have for some (probably stupid) reason.
|
||||
* @param {string} path Path to get the basename of
|
||||
* @param {string} [ext] File extension to remove
|
||||
* @returns {string} Basename of the path
|
||||
* @private
|
||||
*/
|
||||
static basename(path, ext) {
|
||||
let f = splitPathRe.exec(path).slice(1)[2];
|
||||
if (ext && f.substr(-1 * ext.length) === ext) {
|
||||
f = f.substr(0, f.length - ext.length);
|
||||
}
|
||||
if (ext && f.substr(-1 * ext.length) === ext) f = f.substr(0, f.length - ext.length);
|
||||
return f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a snowflake from a decimal string to a bit string.
|
||||
* @param {Snowflake} num Snowflake to be transformed
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
static idToBinary(num) {
|
||||
let bin = '';
|
||||
let high = parseInt(num.slice(0, -10)) || 0;
|
||||
let low = parseInt(num.slice(-10));
|
||||
while (low > 0 || high > 0) {
|
||||
bin = String(low & 1) + bin;
|
||||
low = Math.floor(low / 2);
|
||||
if (high > 0) {
|
||||
low += 5000000000 * (high % 2);
|
||||
high = Math.floor(high / 2);
|
||||
}
|
||||
}
|
||||
return bin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a snowflake from a bit string to a decimal string.
|
||||
* @param {string} num Bit string to be transformed
|
||||
* @returns {Snowflake}
|
||||
* @private
|
||||
*/
|
||||
static binaryToID(num) {
|
||||
let dec = '';
|
||||
|
||||
while (num.length > 50) {
|
||||
const high = parseInt(num.slice(0, -32), 2);
|
||||
const low = parseInt((high % 10).toString(2) + num.slice(-32), 2);
|
||||
|
||||
dec = (low % 10).toString() + dec;
|
||||
num = Math.floor(high / 10).toString(2) + Math.floor(low / 10).toString(2).padStart(32, '0');
|
||||
}
|
||||
|
||||
num = parseInt(num, 2);
|
||||
while (num > 0) {
|
||||
dec = (num % 10).toString() + dec;
|
||||
num = Math.floor(num / 10);
|
||||
}
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Promise that resolves after a specified duration.
|
||||
* @param {number} ms How long to wait before resolving (in milliseconds)
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
static delayFor(ms) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Util;
|
||||
|
||||
@@ -24,12 +24,6 @@ else
|
||||
SOURCE_TYPE="branch"
|
||||
fi
|
||||
|
||||
# For Node != 8, do nothing
|
||||
if [ "$TRAVIS_NODE_VERSION" != "8" ]; then
|
||||
echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - doing nothing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run the build
|
||||
npm run docs
|
||||
VERSIONED=false npm run webpack
|
||||
@@ -88,4 +82,3 @@ git config user.name "Travis CI"
|
||||
git config user.email "$COMMIT_AUTHOR_EMAIL"
|
||||
git commit -m "Webpack build for ${SOURCE_TYPE} ${SOURCE}: ${SHA}" || true
|
||||
git push $SSH_REPO $TARGET_BRANCH
|
||||
|
||||
|
||||
2
typings
2
typings
Submodule typings updated: 5131e88ffe...0b5b13f4a5
Reference in New Issue
Block a user