From ab3a43919890f0a45812fd91788ea911a106f937 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 29 Oct 2018 15:02:36 -0400 Subject: [PATCH 001/428] Add worker-based sharding to the ShardingManager (#2908) * Add worker-based sharding mode to ShardingManager * Fix ClientShardUtil mode * Fix worker not being cleared on shard death * Update docs and typings * Clean up Client sharding logic a bit * Add info about requirements for worker mode --- src/client/Client.js | 34 ++++++++-- src/sharding/Shard.js | 113 ++++++++++++++++++++------------ src/sharding/ShardClientUtil.js | 74 +++++++++++++++------ src/sharding/ShardingManager.js | 40 +++++++++-- typings/index.d.ts | 11 +++- 5 files changed, 196 insertions(+), 76 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index b87d21c9..4b3160df 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -31,12 +31,30 @@ class Client extends BaseClient { constructor(options = {}) { super(Object.assign({ _tokenType: 'Bot' }, options)); - // Obtain shard details from environment - if (!browser && !this.options.shardId && 'SHARD_ID' in process.env) { - this.options.shardId = Number(process.env.SHARD_ID); - } - if (!browser && !this.options.shardCount && 'SHARD_COUNT' in process.env) { - this.options.shardCount = Number(process.env.SHARD_COUNT); + // Figure out the shard details + if (!browser && process.env.SHARDING_MANAGER) { + // Try loading workerData if it's present + let workerData; + try { + workerData = require('worker_threads').workerData; + } catch (err) { + // Do nothing + } + + if (!this.options.shardId) { + if (workerData && 'SHARD_ID' in workerData) { + this.options.shardId = workerData.SHARD_ID; + } else if ('SHARD_ID' in process.env) { + this.options.shardId = Number(process.env.SHARD_ID); + } + } + if (!this.options.shardCount) { + if (workerData && 'SHARD_COUNT' in workerData) { + this.options.shardCount = workerData.SHARD_COUNT; + } else if ('SHARD_COUNT' in process.env) { + this.options.shardCount = Number(process.env.SHARD_COUNT); + } + } } this._validateOptions(); @@ -73,7 +91,9 @@ class Client extends BaseClient { * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) * @type {?ShardClientUtil} */ - this.shard = !browser && process.env.SHARDING_MANAGER ? ShardClientUtil.singleton(this) : null; + this.shard = !browser && process.env.SHARDING_MANAGER ? + ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE) : + null; /** * All of the {@link User} objects that have been cached at any point, mapped by their IDs diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 1fb47975..a43bb7e9 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -1,13 +1,14 @@ -const childProcess = require('child_process'); const EventEmitter = require('events'); const path = require('path'); const Util = require('../util/Util'); const { Error } = require('../errors'); +let childProcess = null; +let Worker = null; /** * 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. + * an instance of the bot and its {@link Client}. When its child process/worker exits for any reason, the shard will + * spawn a new one to replace it as necessary. * @extends EventEmitter */ class Shard extends EventEmitter { @@ -18,6 +19,9 @@ class Shard extends EventEmitter { constructor(manager, id) { super(); + if (manager.mode === 'process') childProcess = require('child_process'); + else if (manager.mode === 'worker') Worker = require('worker_threads').Worker; + /** * Manager that created the shard * @type {ShardingManager} @@ -31,26 +35,24 @@ class Shard extends EventEmitter { this.id = id; /** - * Arguments for the shard's process + * Arguments for the shard's process (only when {@link ShardingManager#mode} is `process`) * @type {string[]} */ this.args = manager.shardArgs || []; /** - * Arguments for the shard's process executable + * Arguments for the shard's process executable (only when {@link ShardingManager#mode} is `process`) * @type {?string[]} */ this.execArgv = manager.execArgv; /** - * Environment variables for the shard's process + * Environment variables for the shard's process, or workerData for the shard's worker * @type {Object} */ this.env = Object.assign({}, process.env, { - SHARDING_MANAGER: true, SHARD_ID: this.id, SHARD_COUNT: this.manager.totalShards, - DISCORD_TOKEN: this.manager.token, }); /** @@ -60,11 +62,17 @@ class Shard extends EventEmitter { this.ready = false; /** - * Process of the shard + * Process of the shard (if {@link ShardingManager#mode} is `process`) * @type {?ChildProcess} */ this.process = null; + /** + * Worker of the shard (if {@link ShardingManager#mode} is `worker`) + * @type {?Worker} + */ + this.worker = null; + /** * Ongoing promises for calls to {@link Shard#eval}, mapped by the `script` they were called with * @type {Map} @@ -88,49 +96,62 @@ class Shard extends EventEmitter { } /** - * Forks a child process for the shard. + * Forks a child process or creates a worker thread for the shard. * You should not need to call this manually. * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving * @returns {Promise} */ async spawn(waitForReady = true) { if (this.process) throw new Error('SHARDING_PROCESS_EXISTS', this.id); + if (this.worker) throw new Error('SHARDING_WORKER_EXISTS', this.id); - this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { - env: this.env, execArgv: this.execArgv, - }) - .on('message', this._handleMessage.bind(this)) - .on('exit', this._exitListener); + if (this.manager.mode === 'process') { + this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { + env: this.env, execArgv: this.execArgv, + }) + .on('message', this._handleMessage.bind(this)) + .on('exit', this._exitListener); + } else if (this.manager.mode === 'worker') { + this.worker = new Worker(path.resolve(this.manager.file), { workerData: this.env }) + .on('message', this._handleMessage.bind(this)) + .on('exit', this._exitListener); + } /** - * Emitted upon the creation of the shard's child process. + * Emitted upon the creation of the shard's child process/worker. * @event Shard#spawn - * @param {ChildProcess} process Child process that was created + * @param {ChildProcess|Worker} process Child process/worker that was created */ - this.emit('spawn', this.process); + this.emit('spawn', this.process || this.worker); - if (!waitForReady) return this.process; + if (!waitForReady) return this.process || this.worker; 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; + return this.process || this.worker; } /** - * Immediately kills the shard's process and does not restart it. + * Immediately kills the shard's process/worker and does not restart it. */ kill() { - this.process.removeListener('exit', this._exitListener); - this.process.kill(); + if (this.process) { + this.process.removeListener('exit', this._exitListener); + this.process.kill(); + } else { + this.worker.removeListener('exit', this._exitListener); + this.worker.terminate(); + } + this._handleExit(false); } /** - * Kills and restarts the shard's process. - * @param {number} [delay=500] How long to wait between killing the process and restarting it (in milliseconds) + * Kills and restarts the shard's process/worker. + * @param {number} [delay=500] How long to wait between killing the process/worker and restarting it (in milliseconds) * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving * @returns {Promise} */ @@ -141,15 +162,20 @@ class Shard extends EventEmitter { } /** - * Sends a message to the shard's process. + * Sends a message to the shard's process/worker. * @param {*} message Message to send to the shard * @returns {Promise} */ send(message) { return new Promise((resolve, reject) => { - this.process.send(message, err => { - if (err) reject(err); else resolve(this); - }); + if (this.process) { + this.process.send(message, err => { + if (err) reject(err); else resolve(this); + }); + } else { + this.worker.postMessage(message); + resolve(this); + } }); } @@ -166,16 +192,18 @@ class Shard extends EventEmitter { if (this._fetches.has(prop)) return this._fetches.get(prop); const promise = new Promise((resolve, reject) => { + const child = this.process || this.worker; + const listener = message => { if (!message || message._fetchProp !== prop) return; - this.process.removeListener('message', listener); + child.removeListener('message', listener); this._fetches.delete(prop); resolve(message._result); }; - this.process.on('message', listener); + child.on('message', listener); this.send({ _fetchProp: prop }).catch(err => { - this.process.removeListener('message', listener); + child.removeListener('message', listener); this._fetches.delete(prop); reject(err); }); @@ -194,17 +222,19 @@ class Shard extends EventEmitter { if (this._evals.has(script)) return this._evals.get(script); const promise = new Promise((resolve, reject) => { + const child = this.process || this.worker; + const listener = message => { if (!message || message._eval !== script) return; - this.process.removeListener('message', listener); + child.removeListener('message', listener); this._evals.delete(script); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - this.process.on('message', listener); + child.on('message', listener); const _eval = typeof script === 'function' ? `(${script})(this)` : script; this.send({ _eval }).catch(err => { - this.process.removeListener('message', listener); + child.removeListener('message', listener); this._evals.delete(script); reject(err); }); @@ -215,7 +245,7 @@ class Shard extends EventEmitter { } /** - * Handles an IPC message. + * Handles a message received from the child process/worker. * @param {*} message Message received * @private */ @@ -283,7 +313,7 @@ class Shard extends EventEmitter { } /** - * Emitted upon recieving a message from the child process. + * Emitted upon recieving a message from the child process/worker. * @event Shard#message * @param {*} message Message that was received */ @@ -291,20 +321,21 @@ class Shard extends EventEmitter { } /** - * Handles the shard's process exiting. + * Handles the shard's process/worker 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. + * Emitted upon the shard's child process/worker exiting. * @event Shard#death - * @param {ChildProcess} process Child process that exited + * @param {ChildProcess|Worker} process Child process/worker that exited */ - this.emit('death', this.process); + this.emit('death', this.process || this.worker); this.ready = false; this.process = null; + this.worker = null; this._evals.clear(); this._fetches.clear(); diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 07c282eb..4aa35a3b 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -2,19 +2,45 @@ const Util = require('../util/Util'); const { Events } = require('../util/Constants'); /** - * Helper class for sharded clients spawned as a child process, such as from a {@link ShardingManager}. + * Helper class for sharded clients spawned as a child process/worker, 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 Client of the current shard + * @param {ShardingManagerMode} mode Mode the shard was spawned with */ - constructor(client) { + constructor(client, mode) { + /** + * Client for the shard + * @type {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 }); }); + + /** + * Mode the shard was spawned with + * @type {ShardingManagerMode} + */ + this.mode = mode; + + /** + * Message port for the master process (only when {@link ShardClientUtil#mode} is `worker`) + * @type {?MessagePort} + */ + this.parentPort = null; + + if (mode === 'process') { + 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 }); }); + } else if (mode === 'worker') { + this.parentPort = require('worker_threads').parentPort; + this.parentPort.on('message', this._handleMessage.bind(this)); + client.on('ready', () => { this.parentPort.postMessage({ _ready: true }); }); + client.on('disconnect', () => { this.parentPort.postMessage({ _disconnect: true }); }); + client.on('reconnecting', () => { this.parentPort.postMessage({ _reconnecting: true }); }); + } } /** @@ -42,9 +68,14 @@ class ShardClientUtil { */ send(message) { return new Promise((resolve, reject) => { - process.send(message, err => { - if (err) reject(err); else resolve(); - }); + if (this.mode === 'process') { + process.send(message, err => { + if (err) reject(err); else resolve(); + }); + } else if (this.mode === 'worker') { + this.parentPort.postMessage(message); + resolve(); + } }); } @@ -60,15 +91,17 @@ class ShardClientUtil { */ fetchClientValues(prop) { return new Promise((resolve, reject) => { + const parent = this.parentPort || process; + const listener = message => { if (!message || message._sFetchProp !== prop) return; - process.removeListener('message', listener); + parent.removeListener('message', listener); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - process.on('message', listener); + parent.on('message', listener); this.send({ _sFetchProp: prop }).catch(err => { - process.removeListener('message', listener); + parent.removeListener('message', listener); reject(err); }); }); @@ -86,16 +119,18 @@ class ShardClientUtil { */ broadcastEval(script) { return new Promise((resolve, reject) => { + const parent = this.parentPort || process; script = typeof script === 'function' ? `(${script})(this)` : script; + const listener = message => { if (!message || message._sEval !== script) return; - process.removeListener('message', listener); + parent.removeListener('message', listener); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - process.on('message', listener); + parent.on('message', listener); this.send({ _sEval: script }).catch(err => { - process.removeListener('message', listener); + parent.removeListener('message', listener); reject(err); }); }); @@ -104,7 +139,7 @@ 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 + * @param {number} [respawnDelay=500] How long to wait between killing a shard's process/worker and restarting it * (in milliseconds) * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another * @returns {Promise} Resolves upon the message being sent @@ -151,14 +186,15 @@ class ShardClientUtil { /** * Creates/gets the singleton of this class. * @param {Client} client The client to use + * @param {ShardingManagerMode} mode Mode the shard was spawned with * @returns {ShardClientUtil} */ - static singleton(client) { + static singleton(client, mode) { if (!this._singleton) { - this._singleton = new this(client); + this._singleton = new this(client, mode); } else { client.emit(Events.WARN, - 'Multiple clients created in child process; only the first will handle sharding helpers.'); + 'Multiple clients created in child process/worker; only the first will handle sharding helpers.'); } return this._singleton; } diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index bbec15b6..7cedc37e 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -8,29 +8,42 @@ const { Error, TypeError, RangeError } = require('../errors'); /** * 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. + * It works by spawning a self-contained {@link ChildProcess} or {@link Worker} 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 { + /** + * The mode to spawn shards with for a {@link ShardingManager}: either "process" to use child processes, or + * "worker" to use workers. The "worker" mode relies on the experimental + * [Worker threads](https://nodejs.org/api/worker_threads.html) functionality that is present in Node v10.5.0 or + * newer. Node must be started with the `--experimental-worker` flag to expose it. + * @typedef {Object} ShardingManagerMode + */ + /** * @param {string} file Path to your shard script file * @param {Object} [options] Options for the sharding manager * @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto" + * @param {ShardingManagerMode} [options.mode='process'] Which mode to use for shards * @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 + * (only available when using the `process` mode) * @param {string[]} [options.execArgv=[]] Arguments to pass to the shard script executable when spawning + * (only available when using the `process` mode) * @param {string} [options.token] Token to use for automatic shard count and passing to shards */ constructor(file, options = {}) { super(); options = Util.mergeDefault({ totalShards: 'auto', + mode: 'process', respawn: true, shardArgs: [], + execArgv: [], token: process.env.DISCORD_TOKEN, }, options); @@ -59,6 +72,15 @@ class ShardingManager extends EventEmitter { } } + /** + * Mode for shards to spawn with + * @type {ShardingManagerMode} + */ + this.mode = options.mode; + if (this.mode !== 'process' && this.mode !== 'worker') { + throw new RangeError('CLIENT_INVALID_OPTION', 'Sharding mode', '"process" or "worker"'); + } + /** * Whether shards should automatically respawn upon exiting * @type {boolean} @@ -66,13 +88,13 @@ class ShardingManager extends EventEmitter { this.respawn = options.respawn; /** - * An array of arguments to pass to shards + * An array of arguments to pass to shards (only when {@link ShardingManager#mode} is `process`) * @type {string[]} */ this.shardArgs = options.shardArgs; /** - * An array of arguments to pass to the executable + * An array of arguments to pass to the executable (only when {@link ShardingManager#mode} is `process`) * @type {string[]} */ this.execArgv = options.execArgv; @@ -88,6 +110,10 @@ class ShardingManager extends EventEmitter { * @type {Collection} */ this.shards = new Collection(); + + process.env.SHARDING_MANAGER = true; + process.env.SHARDING_MANAGER_MODE = this.mode; + process.env.DISCORD_TOKEN = this.token; } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 70ff34b3..30fe364a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -921,6 +921,7 @@ declare module 'discord.js' { public manager: ShardingManager; public process: ChildProcess; public ready: boolean; + public worker: Worker; public eval(script: string): Promise; public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; @@ -945,24 +946,28 @@ declare module 'discord.js' { } export class ShardClientUtil { - constructor(client: Client); + constructor(client: Client, mode: ShardingManagerMode); private _handleMessage(message: any): void; private _respond(type: string, message: any): void; + public client: Client; public readonly count: number; public readonly id: number; + public mode: ShardingManagerMode; + public parentPort: MessagePort; public broadcastEval(script: string): Promise; public broadcastEval(fn: (client: Client) => T): Promise; public fetchClientValues(prop: string): Promise; public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: boolean): Promise; public send(message: any): Promise; - public static singleton(client: Client): ShardClientUtil; + public static singleton(client: Client, mode: ShardingManagerMode): ShardClientUtil; } export class ShardingManager extends EventEmitter { constructor(file: string, options?: { totalShards?: number | 'auto'; + mode?: ShardingManagerMode; respawn?: boolean; shardArgs?: string[]; token?: string; @@ -2022,6 +2027,8 @@ declare module 'discord.js' { type RoleResolvable = Role | string; + type ShardingManagerMode = 'process' | 'worker'; + type Snowflake = string; type SplitOptions = { From 18f065867ce7f8369e2c660b62b093e092433822 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 3 Nov 2018 13:09:56 -0400 Subject: [PATCH 002/428] Fix #2924 with a bandage (Node typings not updated) --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 30fe364a..2672d184 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -921,7 +921,7 @@ declare module 'discord.js' { public manager: ShardingManager; public process: ChildProcess; public ready: boolean; - public worker: Worker; + public worker: any; public eval(script: string): Promise; public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; @@ -954,7 +954,7 @@ declare module 'discord.js' { public readonly count: number; public readonly id: number; public mode: ShardingManagerMode; - public parentPort: MessagePort; + public parentPort: any; public broadcastEval(script: string): Promise; public broadcastEval(fn: (client: Client) => T): Promise; public fetchClientValues(prop: string): Promise; From f3cad81f5314a3997ac188277e2471ba84cbd771 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sat, 3 Nov 2018 13:21:23 -0500 Subject: [PATCH 003/428] feat: Internal sharding (#2902) * internal sharding * ready event * the square deal * the new deal * the second new deal * add actual documentation * the new freedom * the great society * federal intervention * some of requested changes * i ran out of things to call these * destroy this * fix: Client#uptime went missing * fix(Client): destroy the client on login failure This may happen duo invalid sharding config / invalid token / user requested destroy * fix(Client): reject login promise when the client is destroyed before ready * fix(WebSocketManager): remove redundancy in destroy method (#2491) * typo(ErrorMessages): duo -> duo to * typo(ErrorMessages): duo -> due * fix: docs and options * docs(WebSocketManager): WebSockethard -> WebSocketShard (#2502) * fix(ClientUser): lazily load to account for extended user structure (#2501) * docs(WebSocketShard): document class to make it visible in documentation (#2504) * fix: WebSocketShard#reconnect * fix: presenceUpdate & userUpdate * presenceUpdate wasn't really being handled at all * userUpdate handled incorrectly because as of v7 in the Discord API, it comes inside presenceUpdate * re-add raw event * member is now part of message create payload * feat: Add functionality to support multiple servers with different shards (#2395) * Added functionallity to spawn multiple sharding managers due to adding start and end shards * Small fixes and limiting shard amount to max recommended * Forgot a check in spawn() * Fixed indentation * Removed optiosn object documentation for totalShards * More fixes and a check that the startShard + amount doesnt go over the recommended shard amount * fix getting max recommended * Removed async from constructor (my fault) * Changed start and end shard to a shardList or "auto" + fixed some brainfarts with isNaN * Changed the loop and totalShard count calculation * shards are actually 0 based * Fixed a problem with the gateway and handled some range errors and type errors * Changed Number.isNan to isNaN and changed a few Integer checks to use Number.isInteger * Added check if shardList contains smth greater than totalShards; made spawn use totalShards again; shardList will be ignored and rebuild if totalShards is 'auto'; fixed docs * ShardingManager#spawn now uses a for..of loop; fixed the if statement inside the new for..of loop to still work as intended; made the totalShards be set to a new amount if smth manual is put into ShardingManager#spawn just like before; Fixed some spelling * internal sharding * ready event * the square deal * the new deal * the second new deal * add actual documentation * the new freedom * the great society * federal intervention * some of requested changes * i ran out of things to call these * destroy this * fix: Client#uptime went missing * fix(Client): destroy the client on login failure This may happen duo invalid sharding config / invalid token / user requested destroy * fix(Client): reject login promise when the client is destroyed before ready * fix(WebSocketManager): remove redundancy in destroy method (#2491) * typo(ErrorMessages): duo -> duo to * typo(ErrorMessages): duo -> due * fix: docs and options * docs(WebSocketManager): WebSockethard -> WebSocketShard (#2502) * fix(ClientUser): lazily load to account for extended user structure (#2501) * docs(WebSocketShard): document class to make it visible in documentation (#2504) * fix: WebSocketShard#reconnect * fix: presenceUpdate & userUpdate * presenceUpdate wasn't really being handled at all * userUpdate handled incorrectly because as of v7 in the Discord API, it comes inside presenceUpdate * Internal Sharding adaptation Adapted to internal sharding Fixed a bug where non ready invalidated sessions wouldnt respawn * Fixed shardCount not retrieving * Fixing style removed unnecessary parenthesis * Fixing and rebasing lets hope i didnt dun hecklered it * Fixing my own retardation * Thanks git rebase * fix: assigning member in message create payload * fix: resumes * fix: IS wont give up reconnecting now * docs: add missing docs mostly * fix: found lost methods * fix: WebSocketManager#broadcast check if shard exists * fix: ShardClientUtil#id returning undefined * feat: handle new session rate limits (#2796) * feat: handle new session rate limits * i have no idea what i was doing last night * fix if statement weirdness * fix: re-add presence parsing from ClientOptions (#2893) * resolve conflicts * typings: missing typings * re-add missing linter rule * fix: replacing ClientUser wrongly * address unecessary performance waste * docs: missing disconnect event * fix(typings): Fix 2 issues with typings (#2909) * (Typings) Update typings to reflect current ClientOptions * fix(Typings) fixes a bug with Websockets and DOM Types * fix travis * feat: allow setting presence per shard * add WebSocketManager#shardX events * adjust typings, docs and performance issues * readjust shard events, now provide shardId parameter instead * fix: ready event should check shardCount, not actualShardCount * fix: re-add replayed parameter of Client#resume * fix(Sharding): fixes several things in Internal Sharding (#2914) * fix(Sharding) fixes several things in Internal Sharding * add default value for shards property * better implement checking for shards array * fix travis & some casing * split shard count into 2 words * update to latest Internal Sharding, fix requested changes * make sure totalShardCount is a number * fix comment * fix small typo * dynamically set totalShardCount if either shards or shardCount is provided * consistency: rename shardID to shardId * remove Client#shardIds * fix: typo in GuildIntegrationsUpdate handler * fix: incorrect packet data being passed in some events (#2919) * fix: edgecase of ShardingManager and totalShardCount (#2918) * fix: Client#userUpdate being passed wrong parameter and fix a potential edgecase of returning null in ClientUser#edit from this event * fix consistency and typings issues * consistency: shardId instances renamed to shardID * typings: fix typings regarding WebSocket * style(.eslintrc): remove additional whitespace * fix(Client): remove ondisconnect handler on timeout * docs(BaseClient): fix typo of Immediate * nitpick: typings, private fields and methods * typo: improve grammar a bit * fix: error assigning client in WebSocketManager * typo: actually spell milliseconds properly --- package.json | 1 + src/client/BaseClient.js | 35 +- src/client/Client.js | 192 +++--- src/client/ClientManager.js | 70 --- src/client/actions/ActionsManager.js | 4 + src/client/actions/GuildIntegrationsUpdate.js | 18 + src/client/actions/GuildMemberRemove.js | 4 +- src/client/actions/MessageCreate.js | 4 +- src/client/actions/PresenceUpdate.js | 38 ++ src/client/actions/UserUpdate.js | 15 +- .../handlers => actions}/VoiceStateUpdate.js | 17 +- .../handlers => actions}/WebhooksUpdate.js | 11 +- src/client/voice/VoiceConnection.js | 2 +- src/client/websocket/WebSocketManager.js | 279 +++++++-- ...bSocketConnection.js => WebSocketShard.js} | 583 +++++++++--------- .../websocket/handlers/CHANNEL_CREATE.js | 3 + .../websocket/handlers/CHANNEL_DELETE.js | 3 + .../websocket/handlers/CHANNEL_PINS_UPDATE.js | 20 + .../websocket/handlers/CHANNEL_UPDATE.js | 15 + .../websocket/handlers/GUILD_BAN_ADD.js | 14 + .../websocket/handlers/GUILD_BAN_REMOVE.js | 3 + src/client/websocket/handlers/GUILD_CREATE.js | 26 + src/client/websocket/handlers/GUILD_DELETE.js | 3 + .../websocket/handlers/GUILD_EMOJIS_UPDATE.js | 3 + .../handlers/GUILD_INTEGRATIONS_UPDATE.js | 3 + .../websocket/handlers/GUILD_MEMBERS_CHUNK.js | 17 + .../websocket/handlers/GUILD_MEMBER_ADD.js | 17 + .../websocket/handlers/GUILD_MEMBER_REMOVE.js | 3 + .../websocket/handlers/GUILD_MEMBER_UPDATE.js | 20 + .../websocket/handlers/GUILD_ROLE_CREATE.js | 3 + .../websocket/handlers/GUILD_ROLE_DELETE.js | 3 + .../websocket/handlers/GUILD_ROLE_UPDATE.js | 3 + src/client/websocket/handlers/GUILD_SYNC.js | 3 + src/client/websocket/handlers/GUILD_UPDATE.js | 3 + .../websocket/handlers/MESSAGE_CREATE.js | 3 + .../websocket/handlers/MESSAGE_DELETE.js | 3 + .../websocket/handlers/MESSAGE_DELETE_BULK.js | 3 + .../handlers/MESSAGE_REACTION_ADD.js | 6 + .../handlers/MESSAGE_REACTION_REMOVE.js | 3 + .../handlers/MESSAGE_REACTION_REMOVE_ALL.js | 3 + .../websocket/handlers/MESSAGE_UPDATE.js | 14 + .../websocket/handlers/PRESENCE_UPDATE.js | 3 + src/client/websocket/handlers/READY.js | 16 + src/client/websocket/handlers/RESUMED.js | 12 + src/client/websocket/handlers/TYPING_START.js | 16 + src/client/websocket/handlers/USER_UPDATE.js | 3 + .../websocket/handlers/VOICE_SERVER_UPDATE.js | 3 + .../websocket/handlers/VOICE_STATE_UPDATE.js | 3 + .../websocket/handlers/WEBHOOKS_UPDATE.js | 3 + src/client/websocket/handlers/index.js | 11 + .../packets/WebSocketPacketManager.js | 104 ---- .../packets/handlers/AbstractHandler.js | 11 - .../packets/handlers/ChannelCreate.js | 15 - .../packets/handlers/ChannelDelete.js | 9 - .../packets/handlers/ChannelPinsUpdate.js | 37 -- .../packets/handlers/ChannelUpdate.js | 20 - .../websocket/packets/handlers/GuildBanAdd.js | 23 - .../packets/handlers/GuildBanRemove.js | 20 - .../websocket/packets/handlers/GuildCreate.js | 33 - .../websocket/packets/handlers/GuildDelete.js | 16 - .../packets/handlers/GuildEmojisUpdate.js | 11 - .../handlers/GuildIntegrationsUpdate.js | 19 - .../packets/handlers/GuildMemberAdd.js | 27 - .../packets/handlers/GuildMemberRemove.js | 13 - .../packets/handlers/GuildMemberUpdate.js | 29 - .../packets/handlers/GuildMembersChunk.js | 28 - .../packets/handlers/GuildRoleCreate.js | 11 - .../packets/handlers/GuildRoleDelete.js | 11 - .../packets/handlers/GuildRoleUpdate.js | 11 - .../websocket/packets/handlers/GuildUpdate.js | 11 - .../packets/handlers/MessageCreate.js | 9 - .../packets/handlers/MessageDelete.js | 9 - .../packets/handlers/MessageDeleteBulk.js | 9 - .../packets/handlers/MessageReactionAdd.js | 13 - .../packets/handlers/MessageReactionRemove.js | 11 - .../handlers/MessageReactionRemoveAll.js | 11 - .../packets/handlers/MessageUpdate.js | 20 - .../packets/handlers/PresenceUpdate.js | 68 -- .../websocket/packets/handlers/Ready.js | 41 -- .../websocket/packets/handlers/Resumed.js | 28 - .../websocket/packets/handlers/TypingStart.js | 68 -- .../websocket/packets/handlers/UserUpdate.js | 11 - .../packets/handlers/VoiceServerUpdate.js | 19 - src/errors/Messages.js | 1 + src/sharding/Shard.js | 8 +- src/sharding/ShardClientUtil.js | 2 +- src/sharding/ShardingManager.js | 48 +- src/stores/GuildMemberStore.js | 2 +- src/structures/ClientPresence.js | 10 +- src/structures/ClientUser.js | 19 +- src/structures/Guild.js | 15 + src/util/Constants.js | 23 +- test/shard.js | 6 +- test/tester1000.js | 4 +- typings/index.d.ts | 81 ++- 95 files changed, 1145 insertions(+), 1393 deletions(-) delete mode 100644 src/client/ClientManager.js create mode 100644 src/client/actions/GuildIntegrationsUpdate.js create mode 100644 src/client/actions/PresenceUpdate.js rename src/client/{websocket/packets/handlers => actions}/VoiceStateUpdate.js (75%) rename src/client/{websocket/packets/handlers => actions}/WebhooksUpdate.js (57%) rename src/client/websocket/{WebSocketConnection.js => WebSocketShard.js} (52%) create mode 100644 src/client/websocket/handlers/CHANNEL_CREATE.js create mode 100644 src/client/websocket/handlers/CHANNEL_DELETE.js create mode 100644 src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js create mode 100644 src/client/websocket/handlers/CHANNEL_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_BAN_ADD.js create mode 100644 src/client/websocket/handlers/GUILD_BAN_REMOVE.js create mode 100644 src/client/websocket/handlers/GUILD_CREATE.js create mode 100644 src/client/websocket/handlers/GUILD_DELETE.js create mode 100644 src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js create mode 100644 src/client/websocket/handlers/GUILD_MEMBER_ADD.js create mode 100644 src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js create mode 100644 src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_ROLE_CREATE.js create mode 100644 src/client/websocket/handlers/GUILD_ROLE_DELETE.js create mode 100644 src/client/websocket/handlers/GUILD_ROLE_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_SYNC.js create mode 100644 src/client/websocket/handlers/GUILD_UPDATE.js create mode 100644 src/client/websocket/handlers/MESSAGE_CREATE.js create mode 100644 src/client/websocket/handlers/MESSAGE_DELETE.js create mode 100644 src/client/websocket/handlers/MESSAGE_DELETE_BULK.js create mode 100644 src/client/websocket/handlers/MESSAGE_REACTION_ADD.js create mode 100644 src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js create mode 100644 src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js create mode 100644 src/client/websocket/handlers/MESSAGE_UPDATE.js create mode 100644 src/client/websocket/handlers/PRESENCE_UPDATE.js create mode 100644 src/client/websocket/handlers/READY.js create mode 100644 src/client/websocket/handlers/RESUMED.js create mode 100644 src/client/websocket/handlers/TYPING_START.js create mode 100644 src/client/websocket/handlers/USER_UPDATE.js create mode 100644 src/client/websocket/handlers/VOICE_SERVER_UPDATE.js create mode 100644 src/client/websocket/handlers/VOICE_STATE_UPDATE.js create mode 100644 src/client/websocket/handlers/WEBHOOKS_UPDATE.js create mode 100644 src/client/websocket/handlers/index.js delete mode 100644 src/client/websocket/packets/WebSocketPacketManager.js delete mode 100644 src/client/websocket/packets/handlers/AbstractHandler.js delete mode 100644 src/client/websocket/packets/handlers/ChannelCreate.js delete mode 100644 src/client/websocket/packets/handlers/ChannelDelete.js delete mode 100644 src/client/websocket/packets/handlers/ChannelPinsUpdate.js delete mode 100644 src/client/websocket/packets/handlers/ChannelUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildBanAdd.js delete mode 100644 src/client/websocket/packets/handlers/GuildBanRemove.js delete mode 100644 src/client/websocket/packets/handlers/GuildCreate.js delete mode 100644 src/client/websocket/packets/handlers/GuildDelete.js delete mode 100644 src/client/websocket/packets/handlers/GuildEmojisUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildMemberAdd.js delete mode 100644 src/client/websocket/packets/handlers/GuildMemberRemove.js delete mode 100644 src/client/websocket/packets/handlers/GuildMemberUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildMembersChunk.js delete mode 100644 src/client/websocket/packets/handlers/GuildRoleCreate.js delete mode 100644 src/client/websocket/packets/handlers/GuildRoleDelete.js delete mode 100644 src/client/websocket/packets/handlers/GuildRoleUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildUpdate.js delete mode 100644 src/client/websocket/packets/handlers/MessageCreate.js delete mode 100644 src/client/websocket/packets/handlers/MessageDelete.js delete mode 100644 src/client/websocket/packets/handlers/MessageDeleteBulk.js delete mode 100644 src/client/websocket/packets/handlers/MessageReactionAdd.js delete mode 100644 src/client/websocket/packets/handlers/MessageReactionRemove.js delete mode 100644 src/client/websocket/packets/handlers/MessageReactionRemoveAll.js delete mode 100644 src/client/websocket/packets/handlers/MessageUpdate.js delete mode 100644 src/client/websocket/packets/handlers/PresenceUpdate.js delete mode 100644 src/client/websocket/packets/handlers/Ready.js delete mode 100644 src/client/websocket/packets/handlers/Resumed.js delete mode 100644 src/client/websocket/packets/handlers/TypingStart.js delete mode 100644 src/client/websocket/packets/handlers/UserUpdate.js delete mode 100644 src/client/websocket/packets/handlers/VoiceServerUpdate.js diff --git a/package.json b/package.json index 4f27bf48..c3b4b40c 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node-fetch": "^2.1.2", "pako": "^1.0.0", "prism-media": "amishshah/prism-media", + "setimmediate": "^1.0.5", "tweetnacl": "^1.0.0", "ws": "^6.0.0" }, diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index 6e671f99..c9fce590 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -1,3 +1,4 @@ +require('setimmediate'); const EventEmitter = require('events'); const RESTManager = require('../rest/RESTManager'); const Util = require('../util/Util'); @@ -25,6 +26,13 @@ class BaseClient extends EventEmitter { */ this._intervals = new Set(); + /** + * Intervals set by {@link BaseClient#setImmediate} that are still active + * @type {Set} + * @private + */ + this._immediates = new Set(); + /** * The options the client was instantiated with * @type {ClientOptions} @@ -53,10 +61,12 @@ class BaseClient extends EventEmitter { * Destroys all assets used by the base client. */ destroy() { - for (const t of this._timeouts) clearTimeout(t); - for (const i of this._intervals) clearInterval(i); + for (const t of this._timeouts) this.clearTimeout(t); + for (const i of this._intervals) this.clearInterval(i); + for (const i of this._immediates) this.clearImmediate(i); this._timeouts.clear(); this._intervals.clear(); + this._immediates.clear(); } /** @@ -106,6 +116,27 @@ class BaseClient extends EventEmitter { this._intervals.delete(interval); } + /** + * Sets an immediate that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {...*} args Arguments for the function + * @returns {Immediate} + */ + setImmediate(fn, ...args) { + const immediate = setImmediate(fn, ...args); + this._immediates.add(immediate); + return immediate; + } + + /** + * Clears an immediate. + * @param {Immediate} immediate Immediate to cancel + */ + clearImmediate(immediate) { + clearImmediate(immediate); + this._immediates.delete(immediate); + } + toJSON(...props) { return Util.flatten(this, { domain: false }, ...props); } diff --git a/src/client/Client.js b/src/client/Client.js index 4b3160df..339e5564 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,6 +1,5 @@ const BaseClient = require('./BaseClient'); const Permissions = require('../util/Permissions'); -const ClientManager = require('./ClientManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); const WebSocketManager = require('./websocket/WebSocketManager'); const ActionsManager = require('./actions/ActionsManager'); @@ -15,7 +14,8 @@ const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); -const { Events, browser } = require('../util/Constants'); +const { Events, WSCodes, browser, DefaultOptions } = require('../util/Constants'); +const { delayFor } = require('../util/Util'); const DataResolver = require('../util/DataResolver'); const Structures = require('../util/Structures'); const { Error, TypeError, RangeError } = require('../errors'); @@ -31,45 +31,34 @@ class Client extends BaseClient { constructor(options = {}) { super(Object.assign({ _tokenType: 'Bot' }, options)); - // Figure out the shard details - if (!browser && process.env.SHARDING_MANAGER) { - // Try loading workerData if it's present - let workerData; - try { - workerData = require('worker_threads').workerData; - } catch (err) { - // Do nothing + // Obtain shard details from environment or if present, worker threads + let data = process.env; + try { + // Test if worker threads module is present and used + data = require('worker_threads').workerData || data; + } catch (_) { + // Do nothing + } + if (this.options.shards === DefaultOptions.shards) { + if ('SHARDS' in data) { + this.options.shards = JSON.parse(data.SHARDS); } - - if (!this.options.shardId) { - if (workerData && 'SHARD_ID' in workerData) { - this.options.shardId = workerData.SHARD_ID; - } else if ('SHARD_ID' in process.env) { - this.options.shardId = Number(process.env.SHARD_ID); - } - } - if (!this.options.shardCount) { - if (workerData && 'SHARD_COUNT' in workerData) { - this.options.shardCount = workerData.SHARD_COUNT; - } else if ('SHARD_COUNT' in process.env) { - this.options.shardCount = Number(process.env.SHARD_COUNT); - } + } + if (this.options.totalShardCount === DefaultOptions.totalShardCount) { + if ('TOTAL_SHARD_COUNT' in data) { + this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); + } else if (Array.isArray(this.options.shards)) { + this.options.totalShardCount = this.options.shards.length; + } else { + this.options.totalShardCount = this.options.shardCount; } } this._validateOptions(); - /** - * The manager of the client - * @type {ClientManager} - * @private - */ - this.manager = new ClientManager(this); - /** * The WebSocket manager of the client * @type {WebSocketManager} - * @private */ this.ws = new WebSocketManager(this); @@ -155,54 +144,11 @@ class Client extends BaseClient { */ this.broadcasts = []; - /** - * Previous heartbeat pings of the websocket (most recent first, limited to three elements) - * @type {number[]} - */ - this.pings = []; - if (this.options.messageSweepInterval > 0) { this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); } } - /** - * Timestamp of the latest ping's start time - * @type {number} - * @readonly - * @private - */ - get _pingTimestamp() { - return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0; - } - - /** - * Current status of the client's connection to Discord - * @type {?Status} - * @readonly - */ - get status() { - return this.ws.connection ? this.ws.connection.status : null; - } - - /** - * How long it has been since the client last entered the `READY` state in milliseconds - * @type {?number} - * @readonly - */ - get uptime() { - return this.readyAt ? Date.now() - this.readyAt : null; - } - - /** - * Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property - * @type {number} - * @readonly - */ - get ping() { - return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length; - } - /** * All active voice connections that have been established, mapped by guild ID * @type {Collection} @@ -235,6 +181,15 @@ class Client extends BaseClient { return this.readyAt ? this.readyAt.getTime() : null; } + /** + * How long it has been since the client last entered the `READY` state in milliseconds + * @type {?number} + * @readonly + */ + get uptime() { + return this.readyAt ? Date.now() - this.readyAt : null; + } + /** * Creates a voice broadcast. * @returns {VoiceBroadcast} @@ -252,15 +207,54 @@ class Client extends BaseClient { * @example * client.login('my token'); */ - login(token = this.token) { - return new Promise((resolve, reject) => { - if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); - token = token.replace(/^Bot\s*/i, ''); - this.manager.connectToWebSocket(token, resolve, reject); - }).catch(e => { - this.destroy(); - return Promise.reject(e); + async login(token = this.token) { + if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); + this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); + this.emit(Events.DEBUG, `Authenticating using token ${token}`); + let endpoint = this.api.gateway; + if (this.options.shardCount === 'auto') endpoint = endpoint.bot; + const res = await endpoint.get(); + if (this.options.presence) { + this.options.ws.presence = await this.presence._parse(this.options.presence); + } + if (res.session_start_limit && res.session_start_limit.remaining === 0) { + const { session_start_limit: { reset_after } } = res; + this.emit(Events.DEBUG, `Exceeded identify threshold, setting a timeout for ${reset_after} ms`); + await delayFor(reset_after); + } + const gateway = `${res.url}/`; + if (this.options.shardCount === 'auto') { + this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); + this.options.shardCount = res.shards; + this.options.totalShardCount = res.shards; + } + this.emit(Events.DEBUG, `Using gateway ${gateway}`); + this.ws.connect(gateway); + await new Promise((resolve, reject) => { + const onready = () => { + clearTimeout(timeout); + this.removeListener(Events.DISCONNECT, ondisconnect); + resolve(); + }; + const ondisconnect = event => { + clearTimeout(timeout); + this.removeListener(Events.READY, onready); + this.destroy(); + if (WSCodes[event.code]) { + reject(new Error(WSCodes[event.code])); + } + }; + const timeout = setTimeout(() => { + this.removeListener(Events.READY, onready); + this.removeListener(Events.DISCONNECT, ondisconnect); + this.destroy(); + reject(new Error('WS_CONNECTION_TIMEOUT')); + }, this.options.shardCount * 25e3); + if (timeout.unref !== undefined) timeout.unref(); + this.once(Events.READY, onready); + this.once(Events.DISCONNECT, ondisconnect); }); + return token; } /** @@ -269,7 +263,8 @@ class Client extends BaseClient { */ destroy() { super.destroy(); - return this.manager.destroy(); + this.ws.destroy(); + this.token = null; } /** @@ -386,22 +381,10 @@ class Client extends BaseClient { return super.toJSON({ readyAt: false, broadcasts: false, - pings: false, presences: false, }); } - /** - * Adds a ping to {@link Client#pings}. - * @param {number} startTime Starting time of the ping - * @private - */ - _pong(startTime) { - this.pings.unshift(Date.now() - startTime); - if (this.pings.length > 3) this.pings.length = 3; - this.ws.lastHeartbeatAck = true; - } - /** * Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script * with the client as `this`. @@ -419,17 +402,13 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { // eslint-disable-line complexity - if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number'); + if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); } - if (typeof options.shardId !== 'number' || isNaN(options.shardId)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardId', 'a number'); - } - if (options.shardCount < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 0'); - if (options.shardId < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'at least 0'); - if (options.shardId !== 0 && options.shardId >= options.shardCount) { - throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'less than shardCount'); + if (options.shards && typeof options.shards !== 'number' && !Array.isArray(options.shards)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); } + if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1'); if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); } @@ -451,9 +430,6 @@ class Client extends BaseClient { if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); } - if (typeof options.internalSharding !== 'boolean') { - throw new TypeError('CLIENT_INVALID_OPTION', 'internalSharding', 'a boolean'); - } if (!(options.disabledEvents instanceof Array)) { throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array'); } diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js deleted file mode 100644 index e6d10782..00000000 --- a/src/client/ClientManager.js +++ /dev/null @@ -1,70 +0,0 @@ -const { Events, Status } = require('../util/Constants'); -const { Error } = require('../errors'); - -/** - * Manages the state and background tasks of the client. - * @private - */ -class ClientManager { - constructor(client) { - /** - * The client that instantiated this Manager - * @type {Client} - */ - this.client = client; - - /** - * The heartbeat interval - * @type {?number} - */ - this.heartbeatInterval = null; - } - - /** - * The status of the client - * @readonly - * @type {number} - */ - get status() { - return this.connection ? this.connection.status : Status.IDLE; - } - - /** - * Connects the client to the WebSocket. - * @param {string} token The authorization token - * @param {Function} resolve Function to run when connection is successful - * @param {Function} reject Function to run when connection fails - */ - connectToWebSocket(token, resolve, reject) { - this.client.emit(Events.DEBUG, `Authenticated using token ${token}`); - this.client.token = token; - const timeout = this.client.setTimeout(() => reject(new Error('WS_CONNECTION_TIMEOUT')), 1000 * 300); - this.client.api.gateway.get().then(async res => { - if (this.client.options.presence != null) { // eslint-disable-line eqeqeq - const presence = await this.client.presence._parse(this.client.options.presence); - this.client.options.ws.presence = presence; - this.client.presence.patch(presence); - } - const gateway = `${res.url}/`; - this.client.emit(Events.DEBUG, `Using gateway ${gateway}`); - this.client.ws.connect(gateway); - this.client.ws.connection.once('error', reject); - this.client.ws.connection.once('close', event => { - if (event.code === 4004) reject(new Error('TOKEN_INVALID')); - if (event.code === 4010) reject(new Error('SHARDING_INVALID')); - if (event.code === 4011) reject(new Error('SHARDING_REQUIRED')); - }); - this.client.once(Events.READY, () => { - resolve(token); - this.client.clearTimeout(timeout); - }); - }, reject); - } - - destroy() { - this.client.ws.destroy(); - if (this.client.user) this.client.token = null; - } -} - -module.exports = ClientManager; diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 9e44f426..f349b9bc 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -19,13 +19,17 @@ class ActionsManager { this.register(require('./GuildRoleCreate')); this.register(require('./GuildRoleDelete')); this.register(require('./GuildRoleUpdate')); + this.register(require('./PresenceUpdate')); this.register(require('./UserUpdate')); + this.register(require('./VoiceStateUpdate')); this.register(require('./GuildEmojiCreate')); this.register(require('./GuildEmojiDelete')); this.register(require('./GuildEmojiUpdate')); this.register(require('./GuildEmojisUpdate')); this.register(require('./GuildRolesPositionUpdate')); this.register(require('./GuildChannelsPositionUpdate')); + this.register(require('./GuildIntegrationsUpdate')); + this.register(require('./WebhooksUpdate')); } register(Action) { diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js new file mode 100644 index 00000000..e9c3bdbf --- /dev/null +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -0,0 +1,18 @@ +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildIntegrationsUpdate extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); + } +} + +module.exports = GuildIntegrationsUpdate; + +/** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 2e0a1d8c..febeb73a 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -2,7 +2,7 @@ const Action = require('./Action'); const { Events, Status } = require('../../util/Constants'); class GuildMemberRemoveAction extends Action { - handle(data) { + handle(data, shard) { const client = this.client; const guild = client.guilds.get(data.guild_id); let member = null; @@ -13,7 +13,7 @@ class GuildMemberRemoveAction extends Action { guild.voiceStates.delete(member.id); member.deleted = true; guild.members.remove(member.id); - if (client.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); + if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } } return { guild, member }; diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index aebc0d38..9308aedb 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -10,7 +10,9 @@ class MessageCreateAction extends Action { if (existing) return { message: existing }; const message = channel.messages.add(data); const user = message.author; - const member = channel.guild ? channel.guild.member(user) : null; + let member = null; + if (message.member && channel.guild) member = channel.guild.members.add(message.member); + else if (channel.guild) member = channel.guild.member(user); channel.lastMessageID = data.id; if (user) { user.lastMessageID = data.id; diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js new file mode 100644 index 00000000..6da03caf --- /dev/null +++ b/src/client/actions/PresenceUpdate.js @@ -0,0 +1,38 @@ +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class PresenceUpdateAction extends Action { + handle(data) { + let cached = this.client.users.get(data.user.id); + if (!cached && data.user.username) cached = this.client.users.add(data.user); + if (!cached) return; + + if (data.user && data.user.username) { + if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data); + } + + const guild = this.client.guilds.get(data.guild_id); + if (!guild) return; + + let member = guild.members.get(cached.id); + if (!member && data.status !== 'offline') { + member = guild.members.add({ user: cached, roles: data.roles, deaf: false, mute: false }); + this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member); + } + + if (member) { + if (this.client.listenerCount(Events.PRESENCE_UPDATE) === 0) { + guild.presences.add(data); + return; + } + const old = member._clone(); + if (member.presence) old.frozenPresence = member.presence._clone(); + guild.presences.add(data); + this.client.emit(Events.PRESENCE_UPDATE, old, member); + } else { + guild.presences.add(data); + } + } +} + +module.exports = PresenceUpdateAction; diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index de796db2..60adc174 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -5,19 +5,14 @@ class UserUpdateAction extends Action { handle(data) { const client = this.client; - if (client.user) { - if (client.user.equals(data)) { - return { - old: client.user, - updated: client.user, - }; - } + const newUser = client.users.get(data.user.id); + const oldUser = newUser._update(data.user); - const oldUser = client.user._update(data); - client.emit(Events.USER_UPDATE, oldUser, client.user); + if (!oldUser.equals(newUser)) { + client.emit(Events.USER_UPDATE, oldUser, newUser); return { old: oldUser, - updated: client.user, + updated: newUser, }; } diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js similarity index 75% rename from src/client/websocket/packets/handlers/VoiceStateUpdate.js rename to src/client/actions/VoiceStateUpdate.js index e423d0d0..0eb9c5a5 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -1,13 +1,10 @@ -const AbstractHandler = require('./AbstractHandler'); - -const { Events } = require('../../../../util/Constants'); -const VoiceState = require('../../../../structures/VoiceState'); - -class VoiceStateUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); +const VoiceState = require('../../structures/VoiceState'); +class VoiceStateUpdate extends Action { + handle(data) { + const client = this.client; const guild = client.guilds.get(data.guild_id); if (guild) { // Update the state @@ -42,4 +39,4 @@ class VoiceStateUpdateHandler extends AbstractHandler { * @param {VoiceState} newState The voice state after the update */ -module.exports = VoiceStateUpdateHandler; +module.exports = VoiceStateUpdate; diff --git a/src/client/websocket/packets/handlers/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js similarity index 57% rename from src/client/websocket/packets/handlers/WebhooksUpdate.js rename to src/client/actions/WebhooksUpdate.js index 7ed2721e..5ffc41a4 100644 --- a/src/client/websocket/packets/handlers/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -1,10 +1,9 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); -class WebhooksUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; +class WebhooksUpdate extends Action { + handle(data) { + const client = this.client; const channel = client.channels.get(data.channel_id); if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 9bec8150..004cf201 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -169,7 +169,7 @@ class VoiceConnection extends EventEmitter { self_deaf: false, }, options); - this.client.ws.send({ + this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, }); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 960bc0b7..4e4eec1e 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,88 +1,281 @@ -const EventEmitter = require('events'); -const { Events, Status } = require('../../util/Constants'); -const WebSocketConnection = require('./WebSocketConnection'); +const WebSocketShard = require('./WebSocketShard'); +const { Events, Status, WSEvents } = require('../../util/Constants'); +const PacketHandlers = require('./handlers'); + +const BeforeReadyWhitelist = [ + WSEvents.READY, + WSEvents.RESUMED, + WSEvents.GUILD_CREATE, + WSEvents.GUILD_DELETE, + WSEvents.GUILD_MEMBERS_CHUNK, + WSEvents.GUILD_MEMBER_ADD, + WSEvents.GUILD_MEMBER_REMOVE, +]; /** * WebSocket Manager of the client. - * @private */ -class WebSocketManager extends EventEmitter { +class WebSocketManager { constructor(client) { - super(); /** * The client that instantiated this WebSocketManager * @type {Client} + * @readonly */ - this.client = client; + Object.defineProperty(this, 'client', { value: client }); /** - * The WebSocket connection of this manager - * @type {?WebSocketConnection} + * The gateway this WebSocketManager uses. + * @type {?string} */ - this.connection = null; + this.gateway = undefined; + + /** + * An array of shards spawned by this WebSocketManager. + * @type {WebSocketShard[]} + */ + this.shards = []; + + /** + * An array of queued shards to be spawned by this WebSocketManager. + * @type {Array} + * @private + */ + this.spawnQueue = []; + + /** + * Whether or not this WebSocketManager is currently spawning shards. + * @type {boolean} + * @private + */ + this.spawning = false; + + /** + * An array of queued events before this WebSocketManager became ready. + * @type {object[]} + * @private + */ + this.packetQueue = []; + + /** + * The current status of this WebSocketManager. + * @type {number} + */ + this.status = Status.IDLE; + + /** + * The current session limit of the client. + * @type {?Object} + * @prop {number} total Total number of identifies available + * @prop {number} remaining Number of identifies remaining + * @prop {number} reset_after Number of milliseconds after which the limit resets + */ + this.sessionStartLimit = null; } /** - * Sends a heartbeat on the available connection. - * @returns {void} + * The average ping of all WebSocketShards + * @type {number} + * @readonly */ - heartbeat() { - if (!this.connection) return this.debug('No connection to heartbeat'); - return this.connection.heartbeat(); + get ping() { + const sum = this.shards.reduce((a, b) => a + b.ping, 0); + return sum / this.shards.length; } /** * Emits a debug event. * @param {string} message Debug message * @returns {void} + * @private */ debug(message) { - return this.client.emit(Events.DEBUG, `[ws] ${message}`); + this.client.emit(Events.DEBUG, `[connection] ${message}`); } /** - * Destroy the client. - * @returns {void} Whether or not destruction was successful + * Handles the session identify rate limit for a shard. + * @param {WebSocketShard} shard Shard to handle + * @private */ - destroy() { - if (!this.connection) { - this.debug('Attempted to destroy WebSocket but no connection exists!'); + async _handleSessionLimit(shard) { + this.sessionStartLimit = await this.client.api.gateway.bot.get().then(r => r.session_start_limit); + const { remaining, reset_after } = this.sessionStartLimit; + if (remaining !== 0) { + this.spawn(); + } else { + shard.debug(`Exceeded identify threshold, setting a timeout for ${reset_after} ms`); + setTimeout(() => this.spawn(), this.sessionStartLimit.reset_after); + } + } + + /** + * Used to spawn WebSocketShards. + * @param {?WebSocketShard|WebSocketShard[]|number|string} query The WebSocketShards to be spawned + * @returns {void} + * @private + */ + spawn(query) { + if (query !== undefined) { + if (Array.isArray(query)) { + for (const item of query) { + if (!this.spawnQueue.includes(item)) this.spawnQueue.push(item); + } + } else if (!this.spawnQueue.includes(query)) { + this.spawnQueue.push(query); + } + } + + if (this.spawning || !this.spawnQueue.length) return; + + this.spawning = true; + let item = this.spawnQueue.shift(); + + if (typeof item === 'string' && !isNaN(item)) item = Number(item); + if (typeof item === 'number') { + const shard = new WebSocketShard(this, item, this.shards[item]); + this.shards[item] = shard; + shard.once(Events.READY, () => { + this.spawning = false; + this.client.setTimeout(() => this._handleSessionLimit(shard), 5000); + }); + shard.once(Events.INVALIDATED, () => { + this.spawning = false; + }); + } else if (item instanceof WebSocketShard) { + item.reconnect(); + } + } + + /** + * Creates a connection to a gateway. + * @param {string} [gateway=this.gateway] The gateway to connect to + * @returns {void} + * @private + */ + connect(gateway = this.gateway) { + this.gateway = gateway; + + if (typeof this.client.options.shards === 'number') { + this.debug('Spawning 1 shard'); + this.spawn(this.client.options.shards); + } else if (Array.isArray(this.client.options.shards)) { + this.debug(`Spawning ${this.client.options.shards.length} shards`); + for (let i = 0; i < this.client.options.shards.length; i++) { + this.spawn(this.client.options.shards[i]); + } + } else { + this.debug(`Spawning ${this.client.options.shardCount} shards`); + for (let i = 0; i < this.client.options.shardCount; i++) { + this.spawn(i); + } + } + } + + /** + * Processes a packet and queues it if this WebSocketManager is not ready. + * @param {Object} packet The packet to be handled + * @param {WebSocketShard} shard The shard that will handle this packet + * @returns {boolean} + * @private + */ + handlePacket(packet, shard) { + if (packet && this.status !== Status.READY) { + if (!BeforeReadyWhitelist.includes(packet.t)) { + this.packetQueue.push({ packet, shardID: shard.id }); + return false; + } + } + + if (this.packetQueue.length) { + const item = this.packetQueue.shift(); + this.client.setImmediate(() => { + this.handlePacket(item.packet, this.shards[item.shardID]); + }); + } + + if (packet && PacketHandlers[packet.t]) { + PacketHandlers[packet.t](this.client, packet, shard); + } + + return false; + } + + /** + * Checks whether the client is ready to be marked as ready. + * @returns {boolean} + * @private + */ + checkReady() { + if (this.shards.filter(s => s).length !== this.client.options.shardCount || + this.shards.some(s => s && s.status !== Status.READY)) { return false; } - return this.connection.destroy(); + + let unavailableGuilds = 0; + for (const guild of this.client.guilds.values()) { + if (!guild.available) unavailableGuilds++; + } + if (unavailableGuilds === 0) { + this.status = Status.NEARLY; + if (!this.client.options.fetchAllMembers) return this.triggerReady(); + // Fetch all members before marking self as ready + const promises = this.client.guilds.map(g => g.members.fetch()); + Promise.all(promises) + .then(() => this.triggerReady()) + .catch(e => { + this.debug(`Failed to fetch all members before ready! ${e}`); + this.triggerReady(); + }); + } + return true; } /** - * Send a packet on the available WebSocket. - * @param {Object} packet Packet to send + * Causes the client to be marked as ready and emits the ready event. * @returns {void} + * @private */ - send(packet) { - if (!this.connection) { - this.debug('No connection to websocket'); + triggerReady() { + if (this.status === Status.READY) { + this.debug('Tried to mark self as ready, but already ready'); return; } - this.connection.send(packet); + this.status = Status.READY; + + /** + * Emitted when the client becomes ready to start working. + * @event Client#ready + */ + this.client.emit(Events.READY); + + this.handlePacket(); } /** - * Connects the client to a gateway. - * @param {string} gateway The gateway to connect to - * @returns {boolean} + * Broadcasts a message to every shard in this WebSocketManager. + * @param {*} packet The packet to send */ - connect(gateway) { - if (!this.connection) { - this.connection = new WebSocketConnection(this, gateway); - return true; + broadcast(packet) { + for (const shard of this.shards) { + if (!shard) continue; + shard.send(packet); } - switch (this.connection.status) { - case Status.IDLE: - case Status.DISCONNECTED: - this.connection.connect(gateway, 5500); - return true; - default: - this.debug(`Couldn't connect to ${gateway} as the websocket is at state ${this.connection.status}`); - return false; + } + + /** + * Destroys all shards. + * @returns {void} + * @private + */ + destroy() { + this.gateway = undefined; + // Lock calls to spawn + this.spawning = true; + + for (const shard of this.shards) { + if (!shard) continue; + shard.destroy(); } } } diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketShard.js similarity index 52% rename from src/client/websocket/WebSocketConnection.js rename to src/client/websocket/WebSocketShard.js index 12e90075..1c51be0a 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketShard.js @@ -1,25 +1,21 @@ const EventEmitter = require('events'); -const { Events, OPCodes, Status, WSCodes } = require('../../util/Constants'); -const PacketManager = require('./packets/WebSocketPacketManager'); const WebSocket = require('../../WebSocket'); +const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); +let zlib; try { - var zlib = require('zlib-sync'); + zlib = require('zlib-sync'); if (!zlib.Inflate) zlib = require('pako'); } catch (err) { zlib = require('pako'); } /** - * Abstracts a WebSocket connection with decoding/encoding for the Discord gateway. - * @private + * Represents a Shard's Websocket connection. */ -class WebSocketConnection extends EventEmitter { - /** - * @param {WebSocketManager} manager The WebSocket manager - * @param {string} gateway The WebSocket gateway to connect to - */ - constructor(manager, gateway) { +class WebSocketShard extends EventEmitter { + constructor(manager, id, oldShard) { super(); + /** * The WebSocket Manager of this connection * @type {WebSocketManager} @@ -27,242 +23,240 @@ class WebSocketConnection extends EventEmitter { this.manager = manager; /** - * The client this belongs to - * @type {Client} - */ - this.client = manager.client; - - /** - * The WebSocket connection itself - * @type {WebSocket} - */ - this.ws = null; - - /** - * The current sequence of the WebSocket + * The id of the this shard. * @type {number} */ - this.sequence = -1; + this.id = id; /** - * The current sessionID of the WebSocket - * @type {string} - */ - this.sessionID = null; - - /** - * The current status of the client - * @type {number} + * The current status of the shard + * @type {Status} */ this.status = Status.IDLE; /** - * The Packet Manager of the connection - * @type {WebSocketPacketManager} - */ - this.packetManager = new PacketManager(this); - - /** - * The last time a ping was sent (a timestamp) + * The current sequence of the WebSocket * @type {number} + * @private */ - this.lastPingTimestamp = 0; - - /** - * Contains the rate limit queue and metadata - * @type {Object} - */ - this.ratelimit = { - queue: [], - remaining: 120, - total: 120, - time: 60e3, - resetTimer: null, - }; - - /** - * Events that are disabled (will not be processed) - * @type {Object} - */ - this.disabledEvents = {}; - for (const event of this.client.options.disabledEvents) this.disabledEvents[event] = true; + this.sequence = oldShard ? oldShard.sequence : -1; /** * The sequence on WebSocket close * @type {number} + * @private */ this.closeSequence = 0; /** - * Whether or not the WebSocket is expecting to be closed - * @type {boolean} + * The current session id of the WebSocket + * @type {?string} + * @private */ - this.expectingClose = false; + this.sessionID = oldShard && oldShard.sessionID; - this.inflate = null; - this.connect(gateway); - } - - /** - * Causes the client to be marked as ready and emits the ready event. - * @returns {void} - */ - triggerReady() { - if (this.status === Status.READY) { - this.debug('Tried to mark self as ready, but already ready'); - return; - } /** - * Emitted when the client becomes ready to start working. - * @event Client#ready + * Previous heartbeat pings of the websocket (most recent first, limited to three elements) + * @type {number[]} */ - this.status = Status.READY; - this.client.emit(Events.READY); - this.packetManager.handleQueue(); + this.pings = []; + + /** + * The last time a ping was sent (a timestamp) + * @type {number} + * @private + */ + this.lastPingTimestamp = -1; + + /** + * List of servers the shard is connected to + * @type {string[]} + * @private + */ + this.trace = []; + + /** + * Contains the rate limit queue and metadata + * @type {Object} + * @private + */ + this.ratelimit = { + queue: [], + total: 120, + remaining: 120, + time: 60e3, + timer: null, + }; + + /** + * The WebSocket connection for the current shard + * @type {?WebSocket} + * @private + */ + this.ws = null; + + /** + * @external Inflate + * @see {@link https://www.npmjs.com/package/zlib-sync} + */ + + /** + * The compression to use + * @type {?Inflate} + * @private + */ + this.inflate = null; + + this.connect(); } /** - * Checks whether the client is ready to be marked as ready. - * @returns {void} + * Average heartbeat ping of the websocket, obtained by averaging the WebSocketShard#pings property + * @type {number} + * @readonly */ - checkIfReady() { - if (this.status === Status.READY || this.status === Status.NEARLY) return false; - let unavailableGuilds = 0; - for (const guild of this.client.guilds.values()) { - if (!guild.available) unavailableGuilds++; - } - if (unavailableGuilds === 0) { - this.status = Status.NEARLY; - if (!this.client.options.fetchAllMembers) return this.triggerReady(); - // Fetch all members before marking self as ready - const promises = this.client.guilds.map(g => g.members.fetch()); - Promise.all(promises) - .then(() => this.triggerReady()) - .catch(e => { - this.debug(`Failed to fetch all members before ready! ${e}`); - this.triggerReady(); - }); - } - return true; + get ping() { + const sum = this.pings.reduce((a, b) => a + b, 0); + return sum / this.pings.length; } - // Util /** - * Emits a debug message. + * Emits a debug event. * @param {string} message Debug message - * @returns {void} + * @private */ debug(message) { - if (message instanceof Error) message = message.stack; - return this.manager.debug(`[connection] ${message}`); + this.manager.debug(`[shard ${this.id}] ${message}`); } /** - * Processes the current WebSocket queue. + * Sends a heartbeat or sets an interval for sending heartbeats. + * @param {number} [time] If -1, clears the interval, any other number sets an interval + * If no value is given, a heartbeat will be sent instantly + * @private */ - processQueue() { - if (this.ratelimit.remaining === 0) return; - if (this.ratelimit.queue.length === 0) return; - if (this.ratelimit.remaining === this.ratelimit.total) { - this.ratelimit.resetTimer = this.client.setTimeout(() => { - this.ratelimit.remaining = this.ratelimit.total; - this.processQueue(); - }, this.ratelimit.time); - } - while (this.ratelimit.remaining > 0) { - const item = this.ratelimit.queue.shift(); - if (!item) return; - this._send(item); - this.ratelimit.remaining--; - } - } - - /** - * Sends data, bypassing the queue. - * @param {Object} data Packet to send - * @returns {void} - */ - _send(data) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${data} but no WebSocket is available!`); + heartbeat(time) { + if (!isNaN(time)) { + if (time === -1) { + this.debug('Clearing heartbeat interval'); + this.manager.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } else { + this.debug(`Setting a heartbeat interval for ${time}ms`); + this.heartbeatInterval = this.manager.client.setInterval(() => this.heartbeat(), time); + } return; } - this.ws.send(WebSocket.pack(data)); + + this.debug('Sending a heartbeat'); + this.lastPingTimestamp = Date.now(); + this.send({ + op: OPCodes.HEARTBEAT, + d: this.sequence, + }); } /** - * Adds data to the queue to be sent. - * @param {Object} data Packet to send - * @returns {void} + * Acknowledges a heartbeat. + * @private */ - send(data) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${data} but no WebSocket is available!`); - return; - } - this.ratelimit.queue.push(data); - this.processQueue(); + ackHeartbeat() { + const latency = Date.now() - this.lastPingTimestamp; + this.debug(`Heartbeat acknowledged, latency of ${latency}ms`); + this.pings.unshift(latency); + if (this.pings.length > 3) this.pings.length = 3; } /** - * Creates a connection to a gateway. - * @param {string} gateway The gateway to connect to - * @param {number} [after=0] How long to wait before connecting - * @param {boolean} [force=false] Whether or not to force a new connection even if one already exists - * @returns {boolean} + * Connects the shard to a gateway. + * @private */ - connect(gateway = this.gateway, after = 0, force = false) { - if (after) return this.client.setTimeout(() => this.connect(gateway, 0, force), after); // eslint-disable-line - if (this.ws && !force) { - this.debug('WebSocket connection already exists'); - return false; - } else if (typeof gateway !== 'string') { - this.debug(`Tried to connect to an invalid gateway: ${gateway}`); - return false; - } + connect() { this.inflate = new zlib.Inflate({ chunkSize: 65535, flush: zlib.Z_SYNC_FLUSH, to: WebSocket.encoding === 'json' ? 'string' : '', }); - this.expectingClose = false; - this.gateway = gateway; + const gateway = this.manager.gateway; this.debug(`Connecting to ${gateway}`); const ws = this.ws = WebSocket.create(gateway, { - v: this.client.options.ws.version, + v: this.manager.client.options.ws.version, compress: 'zlib-stream', }); - ws.onmessage = this.onMessage.bind(this); ws.onopen = this.onOpen.bind(this); + ws.onmessage = this.onMessage.bind(this); ws.onerror = this.onError.bind(this); ws.onclose = this.onClose.bind(this); this.status = Status.CONNECTING; - return true; } /** - * Destroys the connection. + * Called whenever a packet is received + * @param {Object} packet Packet received * @returns {boolean} + * @private */ - destroy() { - const ws = this.ws; - if (!ws) { - this.debug('Attempted to destroy WebSocket but no connection exists!'); + onPacket(packet) { + if (!packet) { + this.debug('Received null packet'); return false; } - this.heartbeat(-1); - this.expectingClose = true; - ws.close(1000); - this.packetManager.handleQueue(); - this.ws = null; - this.status = Status.DISCONNECTED; - this.ratelimit.remaining = this.ratelimit.total; - return true; + + this.manager.client.emit(Events.RAW, packet, this.id); + + switch (packet.t) { + case WSEvents.READY: + this.sessionID = packet.d.session_id; + this.trace = packet.d._trace; + this.status = Status.READY; + this.debug(`READY ${this.trace.join(' -> ')} ${this.sessionID}`); + this.heartbeat(); + break; + case WSEvents.RESUMED: { + this.trace = packet.d._trace; + this.status = Status.READY; + const replayed = packet.s - this.sequence; + this.debug(`RESUMED ${this.trace.join(' -> ')} | replayed ${replayed} events.`); + this.heartbeat(); + break; + } + } + + if (packet.s > this.sequence) this.sequence = packet.s; + + switch (packet.op) { + case OPCodes.HELLO: + this.identify(); + return this.heartbeat(packet.d.heartbeat_interval); + case OPCodes.RECONNECT: + return this.reconnect(); + case OPCodes.INVALID_SESSION: + if (!packet.d) this.sessionID = null; + this.sequence = -1; + this.debug('Session invalidated'); + return this.reconnect(Events.INVALIDATED); + case OPCodes.HEARTBEAT_ACK: + return this.ackHeartbeat(); + case OPCodes.HEARTBEAT: + return this.heartbeat(); + default: + return this.manager.handlePacket(packet, this); + } + } + + /** + * Called whenever a connection is opened to the gateway. + * @param {Event} event Received open event + * @private + */ + onOpen() { + this.debug('Connection open'); } /** * Called whenever a message is received. * @param {Event} event Event received + * @private */ onMessage({ data }) { if (data instanceof ArrayBuffer) data = new Uint8Array(data); @@ -278,89 +272,40 @@ class WebSocketConnection extends EventEmitter { let packet; try { packet = WebSocket.unpack(this.inflate.result); + this.manager.client.emit(Events.RAW, packet); } catch (err) { - this.client.emit('debug', err); + this.manager.client.emit(Events.ERROR, err); return; } + if (packet.t === 'READY') { + /** + * Emitted when a shard becomes ready + * @event WebSocketShard#ready + */ + this.emit(Events.READY); + + /** + * Emitted when a shard becomes ready + * @event Client#shardReady + * @param {number} shardID The id of the shard + */ + this.manager.client.emit(Events.SHARD_READY, this.id); + } this.onPacket(packet); - if (this.client.listenerCount('raw')) this.client.emit('raw', packet); - } - - /** - * Sets the current sequence of the connection. - * @param {number} s New sequence - */ - setSequence(s) { - this.sequence = s > this.sequence ? s : this.sequence; - } - - /** - * Called whenever a packet is received. - * @param {Object} packet Received packet - * @returns {boolean} - */ - onPacket(packet) { - if (!packet) { - this.debug('Received null packet'); - return false; - } - switch (packet.op) { - case OPCodes.HELLO: - return this.heartbeat(packet.d.heartbeat_interval); - case OPCodes.RECONNECT: - return this.reconnect(); - case OPCodes.INVALID_SESSION: - if (!packet.d) this.sessionID = null; - this.sequence = -1; - this.debug('Session invalidated -- will identify with a new session'); - return this.identify(packet.d ? 2500 : 0); - case OPCodes.HEARTBEAT_ACK: - return this.ackHeartbeat(); - case OPCodes.HEARTBEAT: - return this.heartbeat(); - default: - return this.packetManager.handle(packet); - } - } - - /** - * Called whenever a connection is opened to the gateway. - * @param {Event} event Received open event - */ - onOpen(event) { - if (event && event.target && event.target.url) this.gateway = event.target.url; - this.debug(`Connected to gateway ${this.gateway}`); - this.identify(); - } - - /** - * Causes a reconnection to the gateway. - */ - reconnect() { - this.debug('Attempting to reconnect in 5500ms...'); - /** - * Emitted whenever the client tries to reconnect to the WebSocket. - * @event Client#reconnecting - */ - this.client.emit(Events.RECONNECTING); - this.connect(this.gateway, 5500, true); } /** * Called whenever an error occurs with the WebSocket. * @param {Error} error The error that occurred + * @private */ onError(error) { if (error && error.message === 'uWs client connection error') { this.reconnect(); return; } - /** - * Emitted whenever the client's WebSocket encounters a connection error. - * @event Client#error - * @param {Error} error The encountered error - */ - this.client.emit(Events.ERROR, error); + this.emit(Events.INVALIDATED); + this.manager.client.emit(Events.ERROR, error); } /** @@ -371,90 +316,50 @@ class WebSocketConnection extends EventEmitter { /** * Called whenever a connection to the gateway is closed. * @param {CloseEvent} event Close event that was received + * @private */ onClose(event) { - this.debug(`${this.expectingClose ? 'Client' : 'Server'} closed the WebSocket connection: ${event.code}`); this.closeSequence = this.sequence; - // Reset the state before trying to fix anything this.emit('close', event); - this.heartbeat(-1); - // Should we reconnect? if (event.code === 1000 ? this.expectingClose : WSCodes[event.code]) { - this.expectingClose = false; /** * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. * @event Client#disconnect * @param {CloseEvent} event The WebSocket close event + * @param {number} shardID The shard that disconnected */ - this.client.emit(Events.DISCONNECT, event); + this.manager.client.emit(Events.DISCONNECT, event, this.id); + this.debug(WSCodes[event.code]); - this.destroy(); return; } - this.expectingClose = false; - this.reconnect(); + this.reconnect(Events.INVALIDATED); } - // Heartbeat - /** - * Acknowledges a heartbeat. - */ - ackHeartbeat() { - this.debug(`Heartbeat acknowledged, latency of ${Date.now() - this.lastPingTimestamp}ms`); - this.client._pong(this.lastPingTimestamp); - } - - /** - * Sends a heartbeat or sets an interval for sending heartbeats. - * @param {number} [time] If -1, clears the interval, any other number sets an interval - * If no value is given, a heartbeat will be sent instantly - */ - heartbeat(time) { - if (!isNaN(time)) { - if (time === -1) { - this.debug('Clearing heartbeat interval'); - this.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } else { - this.debug(`Setting a heartbeat interval for ${time}ms`); - this.heartbeatInterval = this.client.setInterval(() => this.heartbeat(), time); - } - return; - } - this.debug('Sending a heartbeat'); - this.lastPingTimestamp = Date.now(); - this.send({ - op: OPCodes.HEARTBEAT, - d: this.sequence, - }); - } - - // Identification /** * Identifies the client on a connection. - * @param {number} [after] How long to wait before identifying * @returns {void} + * @private */ - identify(after) { - if (after) return this.client.setTimeout(this.identify.bind(this), after); + identify() { return this.sessionID ? this.identifyResume() : this.identifyNew(); } /** * Identifies as a new connection on the gateway. * @returns {void} + * @private */ identifyNew() { - if (!this.client.token) { + if (!this.manager.client.token) { this.debug('No token available to identify a new session with'); return; } // Clone the generic payload and assign the token - const d = Object.assign({ token: this.client.token }, this.client.options.ws); + const d = Object.assign({ token: this.manager.client.token }, this.manager.client.options.ws); - // Sharding stuff - const { shardId, shardCount } = this.client.options; - if (shardCount > 0) d.shard = [Number(shardId), Number(shardCount)]; + const { totalShardCount } = this.manager.client.options; + d.shard = [this.id, Number(totalShardCount)]; // Send the payload this.debug('Identifying as a new session'); @@ -464,6 +369,7 @@ class WebSocketConnection extends EventEmitter { /** * Resumes a session on the gateway. * @returns {void} + * @private */ identifyResume() { if (!this.sessionID) { @@ -473,7 +379,7 @@ class WebSocketConnection extends EventEmitter { this.debug(`Attempting to resume session ${this.sessionID}`); const d = { - token: this.client.token, + token: this.manager.client.token, session_id: this.sessionID, seq: this.sequence, }; @@ -483,6 +389,85 @@ class WebSocketConnection extends EventEmitter { d, }); } -} -module.exports = WebSocketConnection; + /** + * Adds data to the queue to be sent. + * @param {Object} data Packet to send + * @returns {void} + */ + send(data) { + this.ratelimit.queue.push(data); + this.processQueue(); + } + + /** + * Sends data, bypassing the queue. + * @param {Object} data Packet to send + * @returns {void} + * @private + */ + _send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${data} but no WebSocket is available!`); + return; + } + this.ws.send(WebSocket.pack(data)); + } + + /** + * Processes the current WebSocket queue. + * @returns {void} + * @private + */ + processQueue() { + if (this.ratelimit.remaining === 0) return; + if (this.ratelimit.queue.length === 0) return; + if (this.ratelimit.remaining === this.ratelimit.total) { + this.ratelimit.resetTimer = this.manager.client.setTimeout(() => { + this.ratelimit.remaining = this.ratelimit.total; + this.processQueue(); + }, this.ratelimit.time); + } + while (this.ratelimit.remaining > 0) { + const item = this.ratelimit.queue.shift(); + if (!item) return; + this._send(item); + this.ratelimit.remaining--; + } + } + + /** + * Triggers a shard reconnect. + * @param {?string} [event] The event for the shard to emit + * @returns {void} + * @private + */ + reconnect(event) { + this.heartbeat(-1); + this.status = Status.RECONNECTING; + + /** + * Emitted whenever a shard tries to reconnect to the WebSocket. + * @event Client#reconnecting + */ + this.manager.client.emit(Events.RECONNECTING, this.id); + + if (event === Events.INVALIDATED) this.emit(event); + this.manager.spawn(this.id); + } + + /** + * Destroys the current shard and terminates its connection. + * @returns {void} + * @private + */ + destroy() { + this.heartbeat(-1); + this.expectingClose = true; + if (this.ws) this.ws.close(1000); + this.ws = null; + this.status = Status.DISCONNECTED; + this.ratelimit.remaining = this.ratelimit.total; + } +} +module.exports = WebSocketShard; diff --git a/src/client/websocket/handlers/CHANNEL_CREATE.js b/src/client/websocket/handlers/CHANNEL_CREATE.js new file mode 100644 index 00000000..3074254f --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.ChannelCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/CHANNEL_DELETE.js b/src/client/websocket/handlers/CHANNEL_DELETE.js new file mode 100644 index 00000000..158ccb35 --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.ChannelDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js new file mode 100644 index 00000000..1272dbbb --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -0,0 +1,20 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const channel = client.channels.get(data.channel_id); + const time = new Date(data.last_pin_timestamp); + + if (channel && time) { + // Discord sends null for last_pin_timestamp if the last pinned message was removed + channel.lastPinTimestamp = time.getTime() || null; + + /** + * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, + * not much information can be provided easily here - you need to manually check the pins yourself. + * @event Client#channelPinsUpdate + * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occured in + * @param {Date} time The time of the pins update + */ + client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); + } +}; diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js new file mode 100644 index 00000000..46d9037a --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -0,0 +1,15 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { old, updated } = client.actions.ChannelUpdate.handle(packet.d); + if (old && updated) { + /** + * Emitted whenever a channel is updated - e.g. name change, topic change. + * @event Client#channelUpdate + * @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update + * @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update + */ + client.emit(Events.CHANNEL_UPDATE, old, updated); + } +}; + diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js new file mode 100644 index 00000000..00772c8c --- /dev/null +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -0,0 +1,14 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const guild = client.guilds.get(data.guild_id); + const user = client.users.get(data.user.id); + + /** + * Emitted whenever a member is banned from a guild. + * @event Client#guildBanAdd + * @param {Guild} guild The guild that the ban occurred in + * @param {User} user The user that was banned + */ + if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); +}; diff --git a/src/client/websocket/handlers/GUILD_BAN_REMOVE.js b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js new file mode 100644 index 00000000..08483a83 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildBanRemove.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js new file mode 100644 index 00000000..05250ed6 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -0,0 +1,26 @@ +const { Events, Status } = require('../../../util/Constants'); + +module.exports = async (client, { d: data }, shard) => { + let guild = client.guilds.get(data.id); + if (guild) { + if (!guild.available && !data.unavailable) { + // A newly available guild + guild._patch(data); + client.ws.checkReady(); + } + } else { + // A new guild + data.shardID = shard.id; + guild = client.guilds.add(data); + const emitEvent = client.ws.status === Status.READY; + if (emitEvent) { + /** + * Emitted whenever the client joins a guild. + * @event Client#guildCreate + * @param {Guild} guild The created guild + */ + if (client.options.fetchAllMembers) await guild.members.fetch(); + client.emit(Events.GUILD_CREATE, guild); + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_DELETE.js b/src/client/websocket/handlers/GUILD_DELETE.js new file mode 100644 index 00000000..19d1b3b0 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js new file mode 100644 index 00000000..5fa5a9d5 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildEmojisUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js new file mode 100644 index 00000000..6c1a0cfd --- /dev/null +++ b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildIntegrationsUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js new file mode 100644 index 00000000..7178264d --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -0,0 +1,17 @@ +const { Events } = require('../../../util/Constants'); +const Collection = require('../../../util/Collection'); + +module.exports = (client, { d: data }) => { + const guild = client.guilds.get(data.guild_id); + if (!guild) return; + const members = new Collection(); + + for (const member of data.members) members.set(member.user.id, guild.members.add(member)); + /** + * Emitted whenever a chunk of guild members is received (all members come from the same guild). + * @event Client#guildMembersChunk + * @param {Collection} members The members in the chunk + * @param {Guild} guild The guild related to the member chunk + */ + client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js new file mode 100644 index 00000000..367058a5 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -0,0 +1,17 @@ +const { Events, Status } = require('../../../util/Constants'); + +module.exports = (client, { d: data }, shard) => { + const guild = client.guilds.get(data.guild_id); + if (guild) { + guild.memberCount++; + const member = guild.members.add(data); + if (shard.status === Status.READY) { + /** + * Emitted whenever a user joins a guild. + * @event Client#guildMemberAdd + * @param {GuildMember} member The member that has joined a guild + */ + client.emit(Events.GUILD_MEMBER_ADD, member); + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js new file mode 100644 index 00000000..b00da0e6 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet, shard) => { + client.actions.GuildMemberRemove.handle(packet.d, shard); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js new file mode 100644 index 00000000..be4f573a --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -0,0 +1,20 @@ +const { Status, Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }, shard) => { + const guild = client.guilds.get(data.guild_id); + if (guild) { + const member = guild.members.get(data.user.id); + if (member) { + const old = member._update(data); + if (shard.status === Status.READY) { + /** + * Emitted whenever a guild member changes - i.e. new role, removed role, nickname. + * @event Client#guildMemberUpdate + * @param {GuildMember} oldMember The member before the update + * @param {GuildMember} newMember The member after the update + */ + client.emit(Events.GUILD_MEMBER_UPDATE, old, member); + } + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_CREATE.js b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js new file mode 100644 index 00000000..b6ea8038 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_DELETE.js b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js new file mode 100644 index 00000000..d1093cb2 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js new file mode 100644 index 00000000..c1f526c5 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_SYNC.js b/src/client/websocket/handlers/GUILD_SYNC.js new file mode 100644 index 00000000..f27da424 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_SYNC.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildSync.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_UPDATE.js b/src/client/websocket/handlers/GUILD_UPDATE.js new file mode 100644 index 00000000..0f3e24f7 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_CREATE.js b/src/client/websocket/handlers/MESSAGE_CREATE.js new file mode 100644 index 00000000..bc9303fd --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE.js b/src/client/websocket/handlers/MESSAGE_DELETE.js new file mode 100644 index 00000000..09062196 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js new file mode 100644 index 00000000..a927b3b1 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageDeleteBulk.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js new file mode 100644 index 00000000..d8176212 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -0,0 +1,6 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { user, reaction } = client.actions.MessageReactionAdd.handle(packet.d); + if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js new file mode 100644 index 00000000..8b9f22a1 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageReactionRemove.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js new file mode 100644 index 00000000..2323cfe0 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageReactionRemoveAll.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_UPDATE.js b/src/client/websocket/handlers/MESSAGE_UPDATE.js new file mode 100644 index 00000000..9be750c7 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_UPDATE.js @@ -0,0 +1,14 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { old, updated } = client.actions.MessageUpdate.handle(packet.d); + if (old && updated) { + /** + * Emitted whenever a message is updated - e.g. embed or content change. + * @event Client#messageUpdate + * @param {Message} oldMessage The message before the update + * @param {Message} newMessage The message after the update + */ + client.emit(Events.MESSAGE_UPDATE, old, updated); + } +}; diff --git a/src/client/websocket/handlers/PRESENCE_UPDATE.js b/src/client/websocket/handlers/PRESENCE_UPDATE.js new file mode 100644 index 00000000..89b9f0e2 --- /dev/null +++ b/src/client/websocket/handlers/PRESENCE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.PresenceUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js new file mode 100644 index 00000000..f22968d3 --- /dev/null +++ b/src/client/websocket/handlers/READY.js @@ -0,0 +1,16 @@ +let ClientUser; + +module.exports = (client, { d: data }, shard) => { + if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); + const clientUser = new ClientUser(client, data.user); + client.user = clientUser; + client.readyAt = new Date(); + client.users.set(clientUser.id, clientUser); + + for (const guild of data.guilds) { + guild.shardID = shard.id; + client.guilds.add(guild); + } + + client.ws.checkReady(); +}; diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js new file mode 100644 index 00000000..6cc355e9 --- /dev/null +++ b/src/client/websocket/handlers/RESUMED.js @@ -0,0 +1,12 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet, shard) => { + const replayed = shard.sequence - shard.closeSequence; + /** + * Emitted when the client gateway resumes. + * @event Client#resume + * @param {number} replayed The number of events that were replayed + * @param {number} shardID The ID of the shard that resumed + */ + client.emit(Events.RESUMED, replayed, shard.id); +}; diff --git a/src/client/websocket/handlers/TYPING_START.js b/src/client/websocket/handlers/TYPING_START.js new file mode 100644 index 00000000..ac01d30d --- /dev/null +++ b/src/client/websocket/handlers/TYPING_START.js @@ -0,0 +1,16 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const channel = client.channels.get(data.channel_id); + const user = client.users.get(data.user_id); + + if (channel && user) { + /** + * Emitted whenever a user starts typing in a channel. + * @event Client#typingStart + * @param {Channel} channel The channel the user started typing in + * @param {User} user The user that started typing + */ + client.emit(Events.TYPING_START, channel, user); + } +}; diff --git a/src/client/websocket/handlers/USER_UPDATE.js b/src/client/websocket/handlers/USER_UPDATE.js new file mode 100644 index 00000000..3c5b859c --- /dev/null +++ b/src/client/websocket/handlers/USER_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.UserUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js new file mode 100644 index 00000000..c8ac3883 --- /dev/null +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.emit('self.voiceServer', packet.d); +}; diff --git a/src/client/websocket/handlers/VOICE_STATE_UPDATE.js b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js new file mode 100644 index 00000000..a9527ada --- /dev/null +++ b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.VoiceStateUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/WEBHOOKS_UPDATE.js b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js new file mode 100644 index 00000000..b1afb91c --- /dev/null +++ b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.WebhooksUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js new file mode 100644 index 00000000..77bcf8fd --- /dev/null +++ b/src/client/websocket/handlers/index.js @@ -0,0 +1,11 @@ +const { WSEvents } = require('../../../util/Constants'); + +const handlers = {}; + +for (const name of Object.keys(WSEvents)) { + try { + handlers[name] = require(`./${name}.js`); + } catch (err) {} // eslint-disable-line no-empty +} + +module.exports = handlers; diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js deleted file mode 100644 index a5887138..00000000 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ /dev/null @@ -1,104 +0,0 @@ -const { OPCodes, Status, WSEvents } = require('../../../util/Constants'); - -const BeforeReadyWhitelist = [ - WSEvents.READY, - WSEvents.RESUMED, - WSEvents.GUILD_CREATE, - WSEvents.GUILD_DELETE, - WSEvents.GUILD_MEMBERS_CHUNK, - WSEvents.GUILD_MEMBER_ADD, - WSEvents.GUILD_MEMBER_REMOVE, -]; - -class WebSocketPacketManager { - constructor(connection) { - this.ws = connection; - this.handlers = {}; - this.queue = []; - - this.register(WSEvents.READY, require('./handlers/Ready')); - this.register(WSEvents.RESUMED, require('./handlers/Resumed')); - this.register(WSEvents.GUILD_CREATE, require('./handlers/GuildCreate')); - this.register(WSEvents.GUILD_DELETE, require('./handlers/GuildDelete')); - this.register(WSEvents.GUILD_UPDATE, require('./handlers/GuildUpdate')); - this.register(WSEvents.GUILD_BAN_ADD, require('./handlers/GuildBanAdd')); - this.register(WSEvents.GUILD_BAN_REMOVE, require('./handlers/GuildBanRemove')); - this.register(WSEvents.GUILD_MEMBER_ADD, require('./handlers/GuildMemberAdd')); - this.register(WSEvents.GUILD_MEMBER_REMOVE, require('./handlers/GuildMemberRemove')); - this.register(WSEvents.GUILD_MEMBER_UPDATE, require('./handlers/GuildMemberUpdate')); - this.register(WSEvents.GUILD_ROLE_CREATE, require('./handlers/GuildRoleCreate')); - this.register(WSEvents.GUILD_ROLE_DELETE, require('./handlers/GuildRoleDelete')); - this.register(WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate')); - this.register(WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate')); - this.register(WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk')); - this.register(WSEvents.GUILD_INTEGRATIONS_UPDATE, require('./handlers/GuildIntegrationsUpdate')); - this.register(WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate')); - this.register(WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete')); - this.register(WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate')); - this.register(WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate')); - this.register(WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate')); - this.register(WSEvents.USER_UPDATE, require('./handlers/UserUpdate')); - this.register(WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate')); - this.register(WSEvents.TYPING_START, require('./handlers/TypingStart')); - this.register(WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate')); - this.register(WSEvents.MESSAGE_DELETE, require('./handlers/MessageDelete')); - this.register(WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate')); - this.register(WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk')); - this.register(WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate')); - this.register(WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd')); - this.register(WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove')); - this.register(WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll')); - this.register(WSEvents.WEBHOOKS_UPDATE, require('./handlers/WebhooksUpdate')); - } - - get client() { - return this.ws.client; - } - - register(event, Handler) { - this.handlers[event] = new Handler(this); - } - - handleQueue() { - this.queue.forEach((element, index) => { - this.handle(this.queue[index], true); - this.queue.splice(index, 1); - }); - } - - handle(packet, queue = false) { - if (packet.op === OPCodes.HEARTBEAT_ACK) { - this.ws.client._pong(this.ws.client._pingTimestamp); - this.ws.lastHeartbeatAck = true; - this.ws.client.emit('debug', 'Heartbeat acknowledged'); - } else if (packet.op === OPCodes.HEARTBEAT) { - this.client.ws.send({ - op: OPCodes.HEARTBEAT, - d: this.client.ws.sequence, - }); - this.ws.client.emit('debug', 'Received gateway heartbeat'); - } - - if (this.ws.status === Status.RECONNECTING) { - this.ws.reconnecting = false; - this.ws.checkIfReady(); - } - - this.ws.setSequence(packet.s); - - if (this.ws.disabledEvents[packet.t] !== undefined) return false; - - if (this.ws.status !== Status.READY) { - if (BeforeReadyWhitelist.indexOf(packet.t) === -1) { - this.queue.push(packet); - return false; - } - } - - if (!queue && this.queue.length > 0) this.handleQueue(); - if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet); - return false; - } -} - -module.exports = WebSocketPacketManager; diff --git a/src/client/websocket/packets/handlers/AbstractHandler.js b/src/client/websocket/packets/handlers/AbstractHandler.js deleted file mode 100644 index c1c2a5a2..00000000 --- a/src/client/websocket/packets/handlers/AbstractHandler.js +++ /dev/null @@ -1,11 +0,0 @@ -class AbstractHandler { - constructor(packetManager) { - this.packetManager = packetManager; - } - - handle(packet) { - return packet; - } -} - -module.exports = AbstractHandler; diff --git a/src/client/websocket/packets/handlers/ChannelCreate.js b/src/client/websocket/packets/handlers/ChannelCreate.js deleted file mode 100644 index 5ccc0570..00000000 --- a/src/client/websocket/packets/handlers/ChannelCreate.js +++ /dev/null @@ -1,15 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class ChannelCreateHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.ChannelCreate.handle(packet.d); - } -} - -module.exports = ChannelCreateHandler; - -/** - * Emitted whenever a channel is created. - * @event Client#channelCreate - * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was created - */ diff --git a/src/client/websocket/packets/handlers/ChannelDelete.js b/src/client/websocket/packets/handlers/ChannelDelete.js deleted file mode 100644 index 68eb9a90..00000000 --- a/src/client/websocket/packets/handlers/ChannelDelete.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class ChannelDeleteHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.ChannelDelete.handle(packet.d); - } -} - -module.exports = ChannelDeleteHandler; diff --git a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js deleted file mode 100644 index b8cb6401..00000000 --- a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js +++ /dev/null @@ -1,37 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -/* -{ t: 'CHANNEL_PINS_UPDATE', - s: 666, - op: 0, - d: - { last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00', - channel_id: '314866471639044027' } } -*/ - -class ChannelPinsUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const channel = client.channels.get(data.channel_id); - const time = new Date(data.last_pin_timestamp); - if (channel && time) { - // Discord sends null for last_pin_timestamp if the last pinned message was removed - channel.lastPinTimestamp = time.getTime() || null; - - client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); - } - } -} - -module.exports = ChannelPinsUpdate; - -/** - * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, not much information - * can be provided easily here - you need to manually check the pins yourself. - * The `time` parameter will be a Unix Epoch Date object when there are no pins left. - * @event Client#channelPinsUpdate - * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occurred in - * @param {Date} time The time when the last pinned message was pinned - */ diff --git a/src/client/websocket/packets/handlers/ChannelUpdate.js b/src/client/websocket/packets/handlers/ChannelUpdate.js deleted file mode 100644 index f0d1873a..00000000 --- a/src/client/websocket/packets/handlers/ChannelUpdate.js +++ /dev/null @@ -1,20 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class ChannelUpdateHandler extends AbstractHandler { - handle(packet) { - const { old, updated } = this.packetManager.client.actions.ChannelUpdate.handle(packet.d); - if (old && updated) { - this.packetManager.client.emit(Events.CHANNEL_UPDATE, old, updated); - } - } -} - -module.exports = ChannelUpdateHandler; - -/** - * Emitted whenever a channel is updated - e.g. name change, topic change. - * @event Client#channelUpdate - * @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update - * @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update - */ diff --git a/src/client/websocket/packets/handlers/GuildBanAdd.js b/src/client/websocket/packets/handlers/GuildBanAdd.js deleted file mode 100644 index 89f57b78..00000000 --- a/src/client/websocket/packets/handlers/GuildBanAdd.js +++ /dev/null @@ -1,23 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class GuildBanAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - const user = client.users.add(data.user); - if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); - } -} - -/** - * Emitted whenever a member is banned from a guild. - * @event Client#guildBanAdd - * @param {Guild} guild The guild that the ban occurred in - * @param {User} user The user that was banned - */ - -module.exports = GuildBanAddHandler; diff --git a/src/client/websocket/packets/handlers/GuildBanRemove.js b/src/client/websocket/packets/handlers/GuildBanRemove.js deleted file mode 100644 index c4edbdeb..00000000 --- a/src/client/websocket/packets/handlers/GuildBanRemove.js +++ /dev/null @@ -1,20 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); - -class GuildBanRemoveHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildBanRemove.handle(data); - } -} - -/** - * Emitted whenever a member is unbanned from a guild. - * @event Client#guildBanRemove - * @param {Guild} guild The guild that the unban occurred in - * @param {User} user The user that was unbanned - */ - -module.exports = GuildBanRemoveHandler; diff --git a/src/client/websocket/packets/handlers/GuildCreate.js b/src/client/websocket/packets/handlers/GuildCreate.js deleted file mode 100644 index 96c5ae98..00000000 --- a/src/client/websocket/packets/handlers/GuildCreate.js +++ /dev/null @@ -1,33 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildCreateHandler extends AbstractHandler { - async handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - - let guild = client.guilds.get(data.id); - if (guild) { - if (!guild.available && !data.unavailable) { - // A newly available guild - guild._patch(data); - this.packetManager.ws.checkIfReady(); - } - } else { - // A new guild - guild = client.guilds.add(data); - const emitEvent = client.ws.connection.status === Status.READY; - if (emitEvent) { - /** - * Emitted whenever the client joins a guild. - * @event Client#guildCreate - * @param {Guild} guild The created guild - */ - if (client.options.fetchAllMembers) await guild.members.fetch(); - client.emit(Events.GUILD_CREATE, guild); - } - } - } -} - -module.exports = GuildCreateHandler; diff --git a/src/client/websocket/packets/handlers/GuildDelete.js b/src/client/websocket/packets/handlers/GuildDelete.js deleted file mode 100644 index 58d60bc1..00000000 --- a/src/client/websocket/packets/handlers/GuildDelete.js +++ /dev/null @@ -1,16 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildDeleteHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - client.actions.GuildDelete.handle(packet.d); - } -} - -/** - * Emitted whenever a guild kicks the client or the guild is deleted/left. - * @event Client#guildDelete - * @param {Guild} guild The guild that was deleted - */ - -module.exports = GuildDeleteHandler; diff --git a/src/client/websocket/packets/handlers/GuildEmojisUpdate.js b/src/client/websocket/packets/handlers/GuildEmojisUpdate.js deleted file mode 100644 index 2906e74f..00000000 --- a/src/client/websocket/packets/handlers/GuildEmojisUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildEmojisUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildEmojisUpdate.handle(data); - } -} - -module.exports = GuildEmojisUpdate; diff --git a/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js b/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js deleted file mode 100644 index 5adfb5b0..00000000 --- a/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class GuildIntegrationsHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); - } -} - -module.exports = GuildIntegrationsHandler; - -/** - * Emitted whenever a guild integration is updated - * @event Client#guildIntegrationsUpdate - * @param {Guild} guild The guild whose integrations were updated - */ diff --git a/src/client/websocket/packets/handlers/GuildMemberAdd.js b/src/client/websocket/packets/handlers/GuildMemberAdd.js deleted file mode 100644 index 15201b82..00000000 --- a/src/client/websocket/packets/handlers/GuildMemberAdd.js +++ /dev/null @@ -1,27 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildMemberAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) { - guild.memberCount++; - const member = guild.members.add(data); - if (client.ws.connection.status === Status.READY) { - client.emit(Events.GUILD_MEMBER_ADD, member); - } - } - } -} - -module.exports = GuildMemberAddHandler; - -/** - * Emitted whenever a user joins a guild. - * @event Client#guildMemberAdd - * @param {GuildMember} member The member that has joined a guild - */ diff --git a/src/client/websocket/packets/handlers/GuildMemberRemove.js b/src/client/websocket/packets/handlers/GuildMemberRemove.js deleted file mode 100644 index 6ec1bfe6..00000000 --- a/src/client/websocket/packets/handlers/GuildMemberRemove.js +++ /dev/null @@ -1,13 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); - -class GuildMemberRemoveHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildMemberRemove.handle(data); - } -} - -module.exports = GuildMemberRemoveHandler; diff --git a/src/client/websocket/packets/handlers/GuildMemberUpdate.js b/src/client/websocket/packets/handlers/GuildMemberUpdate.js deleted file mode 100644 index 90189369..00000000 --- a/src/client/websocket/packets/handlers/GuildMemberUpdate.js +++ /dev/null @@ -1,29 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildMemberUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) { - const member = guild.members.get(data.user.id); - if (member) { - const old = member._update(data); - if (client.ws.connection.status === Status.READY) { - /** - * Emitted whenever a guild member's details (e.g. role, nickname) are changed - * @event Client#guildMemberUpdate - * @param {GuildMember} oldMember The member before the update - * @param {GuildMember} newMember The member after the update - */ - client.emit(Events.GUILD_MEMBER_UPDATE, old, member); - } - } - } - } -} - -module.exports = GuildMemberUpdateHandler; diff --git a/src/client/websocket/packets/handlers/GuildMembersChunk.js b/src/client/websocket/packets/handlers/GuildMembersChunk.js deleted file mode 100644 index 4e821f5c..00000000 --- a/src/client/websocket/packets/handlers/GuildMembersChunk.js +++ /dev/null @@ -1,28 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); -const Collection = require('../../../../util/Collection'); - -class GuildMembersChunkHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (!guild) return; - const members = new Collection(); - - for (const member of data.members) members.set(member.user.id, guild.members.add(member)); - - client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild); - - client.ws.lastHeartbeatAck = true; - } -} - -/** - * Emitted whenever a chunk of guild members is received (all members come from the same guild). - * @event Client#guildMembersChunk - * @param {Collection} members The members in the chunk - * @param {Guild} guild The guild related to the member chunk - */ - -module.exports = GuildMembersChunkHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleCreate.js b/src/client/websocket/packets/handlers/GuildRoleCreate.js deleted file mode 100644 index 8581d53f..00000000 --- a/src/client/websocket/packets/handlers/GuildRoleCreate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleCreateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleCreate.handle(data); - } -} - -module.exports = GuildRoleCreateHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleDelete.js b/src/client/websocket/packets/handlers/GuildRoleDelete.js deleted file mode 100644 index 63439b0f..00000000 --- a/src/client/websocket/packets/handlers/GuildRoleDelete.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleDeleteHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleDelete.handle(data); - } -} - -module.exports = GuildRoleDeleteHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleUpdate.js b/src/client/websocket/packets/handlers/GuildRoleUpdate.js deleted file mode 100644 index 6fbdc109..00000000 --- a/src/client/websocket/packets/handlers/GuildRoleUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleUpdate.handle(data); - } -} - -module.exports = GuildRoleUpdateHandler; diff --git a/src/client/websocket/packets/handlers/GuildUpdate.js b/src/client/websocket/packets/handlers/GuildUpdate.js deleted file mode 100644 index 70eff52c..00000000 --- a/src/client/websocket/packets/handlers/GuildUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildUpdate.handle(data); - } -} - -module.exports = GuildUpdateHandler; diff --git a/src/client/websocket/packets/handlers/MessageCreate.js b/src/client/websocket/packets/handlers/MessageCreate.js deleted file mode 100644 index 31f5f280..00000000 --- a/src/client/websocket/packets/handlers/MessageCreate.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageCreateHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageCreate.handle(packet.d); - } -} - -module.exports = MessageCreateHandler; diff --git a/src/client/websocket/packets/handlers/MessageDelete.js b/src/client/websocket/packets/handlers/MessageDelete.js deleted file mode 100644 index 831a0ae0..00000000 --- a/src/client/websocket/packets/handlers/MessageDelete.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageDeleteHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageDelete.handle(packet.d); - } -} - -module.exports = MessageDeleteHandler; diff --git a/src/client/websocket/packets/handlers/MessageDeleteBulk.js b/src/client/websocket/packets/handlers/MessageDeleteBulk.js deleted file mode 100644 index 0077dbf8..00000000 --- a/src/client/websocket/packets/handlers/MessageDeleteBulk.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageDeleteBulkHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageDeleteBulk.handle(packet.d); - } -} - -module.exports = MessageDeleteBulkHandler; diff --git a/src/client/websocket/packets/handlers/MessageReactionAdd.js b/src/client/websocket/packets/handlers/MessageReactionAdd.js deleted file mode 100644 index 34e5c613..00000000 --- a/src/client/websocket/packets/handlers/MessageReactionAdd.js +++ /dev/null @@ -1,13 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class MessageReactionAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const { user, reaction } = client.actions.MessageReactionAdd.handle(data); - if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); - } -} - -module.exports = MessageReactionAddHandler; diff --git a/src/client/websocket/packets/handlers/MessageReactionRemove.js b/src/client/websocket/packets/handlers/MessageReactionRemove.js deleted file mode 100644 index cddde703..00000000 --- a/src/client/websocket/packets/handlers/MessageReactionRemove.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageReactionRemove extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.MessageReactionRemove.handle(data); - } -} - -module.exports = MessageReactionRemove; diff --git a/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js b/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js deleted file mode 100644 index 303da9ca..00000000 --- a/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageReactionRemoveAll extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.MessageReactionRemoveAll.handle(data); - } -} - -module.exports = MessageReactionRemoveAll; diff --git a/src/client/websocket/packets/handlers/MessageUpdate.js b/src/client/websocket/packets/handlers/MessageUpdate.js deleted file mode 100644 index 33e45b19..00000000 --- a/src/client/websocket/packets/handlers/MessageUpdate.js +++ /dev/null @@ -1,20 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class MessageUpdateHandler extends AbstractHandler { - handle(packet) { - const { old, updated } = this.packetManager.client.actions.MessageUpdate.handle(packet.d); - if (old && updated) { - this.packetManager.client.emit(Events.MESSAGE_UPDATE, old, updated); - } - } -} - -module.exports = MessageUpdateHandler; - -/** - * Emitted whenever a message is updated - e.g. embed or content change. - * @event Client#messageUpdate - * @param {Message} oldMessage The message before the update - * @param {Message} newMessage The message after the update - */ diff --git a/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js deleted file mode 100644 index 2892d44c..00000000 --- a/src/client/websocket/packets/handlers/PresenceUpdate.js +++ /dev/null @@ -1,68 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class PresenceUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - let user = client.users.get(data.user.id); - const guild = client.guilds.get(data.guild_id); - - // Step 1 - if (!user) { - if (data.user.username) { - user = client.users.add(data.user); - } else { - return; - } - } - - const oldUser = user._update(data.user); - if (!user.equals(oldUser)) { - client.emit(Events.USER_UPDATE, oldUser, user); - } - - if (guild) { - let oldPresence = guild.presences.get(user.id); - if (oldPresence) oldPresence = oldPresence._clone(); - let member = guild.members.get(user.id); - if (!member && data.status !== 'offline') { - member = guild.members.add({ - user, - roles: data.roles, - deaf: false, - mute: false, - }); - client.emit(Events.GUILD_MEMBER_AVAILABLE, member); - } - guild.presences.add(Object.assign(data, { guild })); - if (member && client.listenerCount(Events.PRESENCE_UPDATE)) { - client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence); - } - } - } -} - -/** - * Emitted whenever a guild member's presence (e.g. status, activity) is changed. - * @event Client#presenceUpdate - * @param {?Presence} oldPresence The presence before the update, if one at all - * @param {Presence} newPresence The presence after the update - */ - -/** - * Emitted whenever a user's details (e.g. username, avatar) are changed. - * Disabling {@link Client#presenceUpdate} will cause this event to only fire - * on {@link ClientUser} update. - * @event Client#userUpdate - * @param {User} oldUser The user before the update - * @param {User} newUser The user after the update - */ - -/** - * Emitted whenever a member becomes available in a large guild. - * @event Client#guildMemberAvailable - * @param {GuildMember} member The member that became available - */ - -module.exports = PresenceUpdateHandler; diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js deleted file mode 100644 index 5b14c833..00000000 --- a/src/client/websocket/packets/handlers/Ready.js +++ /dev/null @@ -1,41 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); -let ClientUser; - -class ReadyHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - - client.ws.heartbeat(); - - client.presence.userID = data.user.id; - if (!ClientUser) ClientUser = require('../../../../structures/ClientUser'); - const clientUser = new ClientUser(client, data.user); - client.user = clientUser; - client.readyAt = new Date(); - client.users.set(clientUser.id, clientUser); - - for (const guild of data.guilds) client.guilds.add(guild); - - const t = client.setTimeout(() => { - client.ws.connection.triggerReady(); - }, 1200 * data.guilds.length); - - client.setMaxListeners(data.guilds.length + 10); - - client.once('ready', () => { - client.setMaxListeners(10); - client.clearTimeout(t); - }); - - const ws = this.packetManager.ws; - - ws.sessionID = data.session_id; - ws._trace = data._trace; - client.emit(Events.DEBUG, `READY ${ws._trace.join(' -> ')} ${ws.sessionID}`); - ws.checkIfReady(); - } -} - -module.exports = ReadyHandler; diff --git a/src/client/websocket/packets/handlers/Resumed.js b/src/client/websocket/packets/handlers/Resumed.js deleted file mode 100644 index cd7cab77..00000000 --- a/src/client/websocket/packets/handlers/Resumed.js +++ /dev/null @@ -1,28 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class ResumedHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const ws = client.ws.connection; - - ws._trace = packet.d._trace; - - ws.status = Status.READY; - this.packetManager.handleQueue(); - - const replayed = ws.sequence - ws.closeSequence; - - ws.debug(`RESUMED ${ws._trace.join(' -> ')} | replayed ${replayed} events.`); - client.emit(Events.RESUMED, replayed); - ws.heartbeat(); - } -} - -/** - * Emitted whenever a WebSocket resumes. - * @event Client#resumed - * @param {number} replayed The number of events that were replayed - */ - -module.exports = ResumedHandler; diff --git a/src/client/websocket/packets/handlers/TypingStart.js b/src/client/websocket/packets/handlers/TypingStart.js deleted file mode 100644 index 52a0f6ba..00000000 --- a/src/client/websocket/packets/handlers/TypingStart.js +++ /dev/null @@ -1,68 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class TypingStartHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const channel = client.channels.get(data.channel_id); - const user = client.users.get(data.user_id); - const timestamp = new Date(data.timestamp * 1000); - - if (channel && user) { - if (channel.type === 'voice') { - client.emit(Events.WARN, `Discord sent a typing packet to voice channel ${channel.id}`); - return; - } - if (channel._typing.has(user.id)) { - const typing = channel._typing.get(user.id); - typing.lastTimestamp = timestamp; - typing.resetTimeout(tooLate(channel, user)); - } else { - channel._typing.set(user.id, new TypingData(client, timestamp, timestamp, tooLate(channel, user))); - client.emit(Events.TYPING_START, channel, user); - } - } - } -} - -class TypingData { - constructor(client, since, lastTimestamp, _timeout) { - this.client = client; - this.since = since; - this.lastTimestamp = lastTimestamp; - this._timeout = _timeout; - } - - resetTimeout(_timeout) { - this.client.clearTimeout(this._timeout); - this._timeout = _timeout; - } - - get elapsedTime() { - return Date.now() - this.since; - } -} - -function tooLate(channel, user) { - return channel.client.setTimeout(() => { - channel.client.emit(Events.TYPING_STOP, channel, user, channel._typing.get(user.id)); - channel._typing.delete(user.id); - }, 6000); -} - -/** - * Emitted whenever a user starts typing in a channel. - * @event Client#typingStart - * @param {Channel} channel The channel the user started typing in - * @param {User} user The user that started typing - */ - -/** - * Emitted whenever a user stops typing in a channel. - * @event Client#typingStop - * @param {Channel} channel The channel the user stopped typing in - * @param {User} user The user that stopped typing - */ - -module.exports = TypingStartHandler; diff --git a/src/client/websocket/packets/handlers/UserUpdate.js b/src/client/websocket/packets/handlers/UserUpdate.js deleted file mode 100644 index bc34f347..00000000 --- a/src/client/websocket/packets/handlers/UserUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class UserUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.UserUpdate.handle(data); - } -} - -module.exports = UserUpdateHandler; diff --git a/src/client/websocket/packets/handlers/VoiceServerUpdate.js b/src/client/websocket/packets/handlers/VoiceServerUpdate.js deleted file mode 100644 index 97885d6c..00000000 --- a/src/client/websocket/packets/handlers/VoiceServerUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -/* -{ - "token": "my_token", - "guild_id": "41771983423143937", - "endpoint": "smart.loyal.discord.gg" -} -*/ - -class VoiceServerUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.emit('self.voiceServer', data); - } -} - -module.exports = VoiceServerUpdate; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 2d1bc3c2..2e2ec97b 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -6,6 +6,7 @@ const Messages = { TOKEN_INVALID: 'An invalid token was provided.', TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', + WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.', WS_CONNECTION_TIMEOUT: 'The connection to the gateway timed out.', WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index a43bb7e9..3b25c1d0 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -29,7 +29,7 @@ class Shard extends EventEmitter { this.manager = manager; /** - * ID of the shard + * ID of the shard in the manager * @type {number} */ this.id = id; @@ -51,8 +51,10 @@ class Shard extends EventEmitter { * @type {Object} */ this.env = Object.assign({}, process.env, { - SHARD_ID: this.id, - SHARD_COUNT: this.manager.totalShards, + SHARDING_MANAGER: true, + SHARDS: this.id, + TOTAL_SHARD_COUNT: this.manager.totalShards, + DISCORD_TOKEN: this.manager.token, }); /** diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 4aa35a3b..9a2a746b 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -49,7 +49,7 @@ class ShardClientUtil { * @readonly */ get id() { - return this.client.options.shardId; + return this.client.options.shards; } /** diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 7cedc37e..9bfce6fe 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -27,7 +27,8 @@ class ShardingManager extends EventEmitter { /** * @param {string} file Path to your shard script file * @param {Object} [options] Options for the sharding manager - * @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto" + * @param {string|number[]} [options.totalShards='auto'] Number of total shards of all shard managers or "auto" + * @param {string|number[]} [options.shardList='auto'] List of shards to spawn or "auto" * @param {ShardingManagerMode} [options.mode='process'] Which mode to use for shards * @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 @@ -58,16 +59,33 @@ class ShardingManager extends EventEmitter { if (!stats.isFile()) throw new Error('CLIENT_INVALID_OPTION', 'File', 'a file'); /** - * Amount of shards that this manager is going to spawn - * @type {number|string} + * List of shards this sharding manager spawns + * @type {string|number[]} */ - this.totalShards = options.totalShards; + this.shardList = options.shardList || 'auto'; + if (this.shardList !== 'auto') { + if (!Array.isArray(this.shardList)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array.'); + } + this.shardList = [...new Set(this.shardList)]; + if (this.shardList.length < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardList', 'at least 1 ID.'); + if (this.shardList.some(shardID => typeof shardID !== 'number' || isNaN(shardID) || + !Number.isInteger(shardID) || shardID < 0)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array of postive integers.'); + } + } + + /** + * Amount of shards that all sharding managers spawn in total + * @type {number} + */ + this.totalShards = options.totalShards || 'auto'; if (this.totalShards !== 'auto') { if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); } if (this.totalShards < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.'); - if (this.totalShards !== Math.floor(this.totalShards)) { + if (!Number.isInteger(this.totalShards)) { throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); } } @@ -150,21 +168,31 @@ class ShardingManager extends EventEmitter { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); } if (amount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.'); - if (amount !== Math.floor(amount)) { + if (!Number.isInteger(amount)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); } } // 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; + if (this.shardList === 'auto' || this.totalShards === 'auto' || this.totalShards !== amount) { + this.shardList = [...Array(amount).keys()]; + } + if (this.totalShards === 'auto' || this.totalShards !== amount) { + this.totalShards = amount; + } + + if (this.shardList.some(shardID => shardID >= amount)) { + throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', + 'bigger than the highest shardID in the shardList option.'); + } // Spawn the shards - for (let s = 1; s <= amount; s++) { + for (const shardID of this.shardList) { const promises = []; - const shard = this.createShard(); + const shard = this.createShard(shardID); promises.push(shard.spawn(waitForReady)); - if (delay > 0 && s !== amount) promises.push(Util.delayFor(delay)); + if (delay > 0 && this.shards.size !== this.shardList.length - 1) promises.push(Util.delayFor(delay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 44494be4..6f95aec9 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -189,7 +189,7 @@ class GuildMemberStore extends DataStore { resolve(query || limit ? new Collection() : this); return; } - this.guild.client.ws.send({ + this.guild.shard.send({ op: OPCodes.REQUEST_GUILD_MEMBERS, d: { guild_id: this.guild.id, diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 90d1cb4a..06924589 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -11,7 +11,15 @@ class ClientPresence extends Presence { async set(presence) { const packet = await this._parse(presence); this.patch(packet); - this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet }); + if (typeof presence.shardID === 'undefined') { + this.client.ws.broadcast({ op: OPCodes.STATUS_UPDATE, d: packet }); + } else if (Array.isArray(presence.shardID)) { + for (const shardID of presence.shardID) { + this.client.ws.shards[shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + } + } else { + this.client.ws.shards[presence.shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + } return this; } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 9827e16d..1b8d90d9 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -15,14 +15,14 @@ class ClientUser extends Structures.get('User') { */ this.verified = data.verified; - this._typing = new Map(); - /** * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account * @type {?boolean} */ this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + this._typing = new Map(); + if (data.token) this.client.token = data.token; } @@ -39,7 +39,9 @@ class ClientUser extends Structures.get('User') { return this.client.api.users('@me').patch({ data }) .then(newData => { this.client.token = newData.token; - return this.client.actions.UserUpdate.handle(newData).updated; + const { updated } = this.client.actions.UserUpdate.handle(newData); + if (updated) return updated; + return this; }); } @@ -84,6 +86,7 @@ class ClientUser extends Structures.get('User') { * @property {string} [activity.name] Name of the activity * @property {ActivityType|number} [activity.type] Type of the activity * @property {string} [activity.url] Stream url + * @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on */ /** @@ -112,6 +115,7 @@ class ClientUser extends Structures.get('User') { /** * Sets the status of the client user. * @param {PresenceStatus} status Status to change to + * @param {?number|number[]} [shardID] Shard ID(s) to have the activity set on * @returns {Promise} * @example * // Set the client user's status @@ -119,8 +123,8 @@ class ClientUser extends Structures.get('User') { * .then(console.log) * .catch(console.error); */ - setStatus(status) { - return this.setPresence({ status }); + setStatus(status, shardID) { + return this.setPresence({ status, shardID }); } /** @@ -129,6 +133,7 @@ class ClientUser extends Structures.get('User') { * @type {Object} * @property {string} [url] Twitch stream URL * @property {ActivityType|number} [type] Type of the activity + * @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on */ /** @@ -143,10 +148,10 @@ class ClientUser extends Structures.get('User') { * .catch(console.error); */ setActivity(name, options = {}) { - if (!name) return this.setPresence({ activity: null }); + if (!name) return this.setPresence({ activity: null, shardID: options.shardID }); const activity = Object.assign({}, options, typeof name === 'object' ? name : { name }); - return this.setPresence({ activity }); + return this.setPresence({ activity, shardID: activity.shardID }); } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 493c0df8..ffef8578 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -74,6 +74,21 @@ class Guild extends Base { this._patch(data); if (!data.channels) this.available = false; } + + /** + * The id of the shard this Guild belongs to. + * @type {number} + */ + this.shardID = data.shardID; + } + + /** + * The Shard this Guild belongs to. + * @type {WebSocketShard} + * @readonly + */ + get shard() { + return this.client.ws.shards[this.shardID]; } /* eslint-disable complexity */ diff --git a/src/util/Constants.js b/src/util/Constants.js index 32413c25..e956c8a4 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -5,8 +5,10 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number} [shardId=0] ID of the shard to run - * @property {number} [shardCount=0] Total number of shards + * @property {number|number[]} [shards=0] ID of the shard to run, or an array of shard IDs + * @property {number} [shardCount=1] Total number of shards that will be spawned by this Client + * @property {number} [totalShardCount=1] The total amount of shards used by all processes of this bot + * (e.g. recommended shard count, shard count of the ShardingManager) * @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel * (-1 or Infinity for unlimited - don't do this without message sweeping, otherwise memory usage will climb * indefinitely) @@ -33,9 +35,9 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {HTTPOptions} [http] HTTP options */ exports.DefaultOptions = { - shardId: 0, - shardCount: 0, - internalSharding: false, + shards: 0, + shardCount: 1, + totalShardCount: 1, messageCacheMaxSize: 200, messageCacheLifetime: 0, messageSweepInterval: 0, @@ -86,10 +88,10 @@ exports.UserAgent = browser ? null : `DiscordBot (${Package.homepage.split('#')[0]}, ${Package.version}) Node.js/${process.version}`; exports.WSCodes = { - 1000: 'Connection gracefully closed', - 4004: 'Tried to identify with an invalid token', - 4010: 'Sharding data provided was invalid', - 4011: 'Shard would be on too many guilds if connected', + 1000: 'WS_CLOSE_REQUESTED', + 4004: 'TOKEN_INVALID', + 4010: 'SHARDING_INVALID', + 4011: 'SHARDING_REQUIRED', }; const AllowedImageFormats = [ @@ -253,6 +255,9 @@ exports.Events = { ERROR: 'error', WARN: 'warn', DEBUG: 'debug', + SHARD_READY: 'shardReady', + INVALIDATED: 'invalidated', + RAW: 'raw', }; /** diff --git a/test/shard.js b/test/shard.js index 5f5f17c4..5c185b5f 100644 --- a/test/shard.js +++ b/test/shard.js @@ -2,7 +2,7 @@ const Discord = require('../'); const { token } = require('./auth.json'); const client = new Discord.Client({ - shardId: process.argv[2], + shardID: process.argv[2], shardCount: process.argv[3], }); @@ -20,8 +20,8 @@ client.on('message', msg => { process.send(123); client.on('ready', () => { - console.log('Ready', client.options.shardId); - if (client.options.shardId === 0) + console.log('Ready', client.options.shardID); + if (client.options.shardID === 0) setTimeout(() => { console.log('kek dying'); client.destroy(); diff --git a/test/tester1000.js b/test/tester1000.js index 99209c71..d726188c 100644 --- a/test/tester1000.js +++ b/test/tester1000.js @@ -4,7 +4,9 @@ const { token, prefix, owner } = require('./auth.js'); // eslint-disable-next-line no-console const log = (...args) => console.log(process.uptime().toFixed(3), ...args); -const client = new Discord.Client(); +const client = new Discord.Client({ + shardCount: 2, +}); client.on('debug', log); client.on('ready', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 2672d184..ddda0bfe 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -80,17 +80,20 @@ declare module 'discord.js' { export class BaseClient extends EventEmitter { constructor(options?: ClientOptions); - private _intervals: Set; private _timeouts: Set; + private _intervals: Set; + private _immediates: Set; private readonly api: object; private rest: object; public options: ClientOptions; public clearInterval(interval: NodeJS.Timer): void; public clearTimeout(timeout: NodeJS.Timer): void; + public clearImmediate(timeout: NodeJS.Immediate): void; public destroy(): void; public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + public setImmediate(fn: Function, delay: number, ...args: any[]): NodeJS.Immediate; public toJSON(...props: { [key: string]: boolean | string }[]): object; } @@ -133,31 +136,26 @@ declare module 'discord.js' { export class Client extends BaseClient { constructor(options?: ClientOptions); - private readonly _pingTimestamp: number; private actions: object; - private manager: ClientManager; private voice: object; - private ws: object; private _eval(script: string): any; - private _pong(startTime: number): void; private _validateOptions(options?: ClientOptions): void; public broadcasts: VoiceBroadcast[]; public channels: ChannelStore; public readonly emojis: GuildEmojiStore; public guilds: GuildStore; - public readonly ping: number; - public pings: number[]; - public readyAt: Date; + public readyAt: Date | null; public readonly readyTimestamp: number; public shard: ShardClientUtil; - public readonly status: Status; public token: string; public readonly uptime: number; - public user: ClientUser; + public user: ClientUser | null; public users: UserStore; public readonly voiceConnections: Collection; + public ws: WebSocketManager; public createVoiceBroadcast(): VoiceBroadcast; + public destroy(): void; public fetchApplication(): Promise; public fetchInvite(invite: InviteResolvable): Promise; public fetchVoiceRegions(): Promise>; @@ -171,7 +169,7 @@ declare module 'discord.js' { public on(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; public on(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; public on(event: 'debug' | 'warn', listener: (info: string) => void): this; - public on(event: 'disconnect', listener: (event: any) => void): this; + public on(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public on(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public on(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public on(event: 'error', listener: (error: Error) => void): this; @@ -189,10 +187,12 @@ declare module 'discord.js' { public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public on(event: 'ready' | 'reconnecting', listener: () => void): this; - public on(event: 'resumed', listener: (replayed: number) => void): this; + public on(event: 'ready', listener: () => void): this; + public on(event: 'reconnecting', listener: (shardID: number) => void): this; + public on(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public on(event: 'shardReady', listener: (shardID: number) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; @@ -203,7 +203,7 @@ declare module 'discord.js' { public once(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; public once(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; public once(event: 'debug' | 'warn', listener: (info: string) => void): this; - public once(event: 'disconnect', listener: (event: any) => void): this; + public once(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public once(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public once(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public once(event: 'error', listener: (error: Error) => void): this; @@ -221,10 +221,12 @@ declare module 'discord.js' { public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public once(event: 'ready' | 'reconnecting', listener: () => void): this; - public once(event: 'resumed', listener: (replayed: number) => void): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'reconnecting', listener: (shardID: number) => void): this; + public once(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public once(event: 'shardReady', listener: (shardID: number) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; @@ -252,18 +254,11 @@ declare module 'discord.js' { public toString(): string; } - class ClientManager { - constructor(client: Client); - public client: Client; - public heartbeatInterval: number; - public readonly status: number; - public connectToWebSocket(token: string, resolve: Function, reject: Function): void; - } - export interface ActivityOptions { name?: string; url?: string; type?: ActivityType | number; + shardID?: number | number[]; } export class ClientUser extends User { @@ -275,7 +270,7 @@ declare module 'discord.js' { public setAFK(afk: boolean): Promise; public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; public setPresence(data: PresenceData): Promise; - public setStatus(status: PresenceStatus): Promise; + public setStatus(status: PresenceStatus, shardID?: number | number[]): Promise; public setUsername(username: string): Promise; } @@ -1289,6 +1284,31 @@ declare module 'discord.js' { constructor(id: string, token: string, options?: ClientOptions); } + export class WebSocketManager { + constructor(client: Client); + public readonly client: Client; + public gateway: string | undefined; + public readonly ping: number; + public shards: WebSocketShard[]; + public sessionStartLimit: { total: number; remaining: number; reset_after: number; }; + public status: Status; + public broadcast(packet: any): void; + } + + export class WebSocketShard extends EventEmitter { + constructor(manager: WebSocketManager, id: number, oldShard?: WebSocketShard); + public id: number; + public readonly ping: number; + public pings: number[]; + public status: Status; + public manager: WebSocketManager; + public send(data: object): void; + + public on(event: 'ready', listener: () => void): this; + + public once(event: 'ready', listener: () => void): this; + } + //#endregion //#region Stores @@ -1572,9 +1592,9 @@ declare module 'discord.js' { }; type ClientOptions = { - presence?: PresenceData; - shardId?: number; + shards?: number | number[]; shardCount?: number; + totalShardCount?: number; messageCacheMaxSize?: number; messageCacheLifetime?: number; messageSweepInterval?: number; @@ -1582,7 +1602,9 @@ declare module 'discord.js' { disableEveryone?: boolean; restWsBridgeTimeout?: number; restTimeOffset?: number; - retryLimit?: number, + restSweepInterval?: number; + retryLimit?: number; + presence?: PresenceData; disabledEvents?: WSEventType[]; ws?: WebSocketOptions; http?: HTTPOptions; @@ -1982,7 +2004,8 @@ declare module 'discord.js' { name?: string; type?: ActivityType | number; url?: string; - } + }; + shardID?: number | number[]; }; type PresenceResolvable = Presence | UserResolvable | Snowflake; From be0d1cd6637bca837f687bfd00e07e8bacf52838 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 4 Nov 2018 23:25:54 -0600 Subject: [PATCH 004/428] fix: Client#shards not being set properly --- src/client/Client.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/Client.js b/src/client/Client.js index 339e5564..5d5deddf 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -53,6 +53,10 @@ class Client extends BaseClient { this.options.totalShardCount = this.options.shardCount; } } + if (!this.options.shards && this.options.shardCount) { + this.options.shards = []; + for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); + } this._validateOptions(); @@ -227,6 +231,10 @@ class Client extends BaseClient { this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); this.options.shardCount = res.shards; this.options.totalShardCount = res.shards; + if (!this.options.shards || !this.options.shards.length) { + this.options.shards = []; + for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); + } } this.emit(Events.DEBUG, `Using gateway ${gateway}`); this.ws.connect(gateway); From 7796cb5d0589a40fa719676d4b8b129a3598732e Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 4 Nov 2018 23:26:40 -0600 Subject: [PATCH 005/428] fix: Client#raw emitting twice --- src/client/websocket/WebSocketShard.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 1c51be0a..0058398e 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -202,8 +202,6 @@ class WebSocketShard extends EventEmitter { return false; } - this.manager.client.emit(Events.RAW, packet, this.id); - switch (packet.t) { case WSEvents.READY: this.sessionID = packet.d.session_id; @@ -272,7 +270,7 @@ class WebSocketShard extends EventEmitter { let packet; try { packet = WebSocket.unpack(this.inflate.result); - this.manager.client.emit(Events.RAW, packet); + this.manager.client.emit(Events.RAW, packet, this.id); } catch (err) { this.manager.client.emit(Events.ERROR, err); return; From 905f1c326259b841e47f7bccdad1fe8c79e36e95 Mon Sep 17 00:00:00 2001 From: Souji Date: Mon, 5 Nov 2018 16:46:47 +0000 Subject: [PATCH 006/428] docs: add missing docstring for Client#userUpdate (#2930) * docs: add missing docstring for Client#userUpdate * docs: indentn't * docs: indentn't 2 --- src/client/actions/UserUpdate.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index 60adc174..b9c09748 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -9,6 +9,12 @@ class UserUpdateAction extends Action { const oldUser = newUser._update(data.user); if (!oldUser.equals(newUser)) { + /** + * Emitted whenever a user's details (e.g. username) are changed. + * @event Client#userUpdate + * @param {User} oldUser The user before the update + * @param {User} newUser The user after the update + */ client.emit(Events.USER_UPDATE, oldUser, newUser); return { old: oldUser, From b59c75e402b91d8a7ecec6f0b67180598e8ccdb5 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Mon, 5 Nov 2018 13:29:07 -0600 Subject: [PATCH 007/428] docs: add missing docstring for Client#guildDelete --- src/client/actions/GuildDelete.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 1bf237bd..27ea1b00 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -19,6 +19,12 @@ class GuildDeleteAction extends Action { if (guild.available && data.unavailable) { // Guild is unavailable guild.available = false; + + /** + * Emitted whenever a guild becomes unavailable, likely due to a server outage. + * @event Client#guildUnavailable + * @param {Guild} guild The guild that has become unavailable + */ client.emit(Events.GUILD_UNAVAILABLE, guild); // Stops the GuildDelete packet thinking a guild was actually deleted, @@ -34,7 +40,14 @@ class GuildDeleteAction extends Action { // Delete guild client.guilds.remove(guild.id); guild.deleted = true; + + /** + * Emitted whenever a guild kicks the client or the guild is deleted/left. + * @event Client#guildDelete + * @param {Guild} guild The guild that was deleted + */ client.emit(Events.GUILD_DELETE, guild); + this.deleted.set(guild.id, guild); this.scheduleForDeletion(guild.id); } else { @@ -49,10 +62,4 @@ class GuildDeleteAction extends Action { } } -/** - * Emitted whenever a guild becomes unavailable, likely due to a server outage. - * @event Client#guildUnavailable - * @param {Guild} guild The guild that has become unavailable - */ - module.exports = GuildDeleteAction; From 08002d0576c7dae89a625a8f11944ab81c4ff4c9 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Mon, 5 Nov 2018 18:24:50 -0600 Subject: [PATCH 008/428] fix: Client#userUpdate receiving wrong packet --- src/client/actions/PresenceUpdate.js | 2 +- src/client/actions/UserUpdate.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 6da03caf..53b18c80 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -8,7 +8,7 @@ class PresenceUpdateAction extends Action { if (!cached) return; if (data.user && data.user.username) { - if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data); + if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); } const guild = this.client.guilds.get(data.guild_id); diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index b9c09748..3ff40790 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -5,8 +5,8 @@ class UserUpdateAction extends Action { handle(data) { const client = this.client; - const newUser = client.users.get(data.user.id); - const oldUser = newUser._update(data.user); + const newUser = client.users.get(data.id); + const oldUser = newUser._update(data); if (!oldUser.equals(newUser)) { /** From 6b886b0abad86a314cbdf76ab85872b46ed2ef63 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Tue, 6 Nov 2018 11:23:17 -0600 Subject: [PATCH 009/428] typings: add Guild#shard and Guild#shardID --- typings/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index ddda0bfe..30ccf253 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -434,6 +434,8 @@ declare module 'discord.js' { public presences: PresenceStore; public region: string; public roles: RoleStore; + public shard: WebSocketShard; + public shardID: number; public splash: string; public readonly systemChannel: TextChannel; public systemChannelID: Snowflake; From 3418b5a1a2e2e424369da42f0a04dc3523f3d933 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Tue, 6 Nov 2018 14:59:02 -0600 Subject: [PATCH 010/428] docs: restore Client#error docs that went missing --- src/client/websocket/WebSocketShard.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 0058398e..e9c01e0b 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -303,6 +303,12 @@ class WebSocketShard extends EventEmitter { return; } this.emit(Events.INVALIDATED); + + /** + * Emitted whenever the client's WebSocket encounters a connection error. + * @event Client#error + * @param {Error} error The encountered error + */ this.manager.client.emit(Events.ERROR, error); } From 2d68e837e5003baf5ac86e79b7f7d0e0bcd4c26a Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 15 Nov 2018 21:38:02 +0000 Subject: [PATCH 011/428] voice: fix receiver null on immediate voiceStateUpdate --- src/client/voice/VoiceConnection.js | 3 +-- src/client/voice/networking/VoiceUDPClient.js | 2 ++ src/client/voice/receiver/Receiver.js | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 004cf201..6a4d4cda 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -113,7 +113,7 @@ class VoiceConnection extends EventEmitter { * The voice receiver of this connection * @type {VoiceReceiver} */ - this.receiver = null; + this.receiver = new VoiceReceiver(this); this.authenticate(); } @@ -418,7 +418,6 @@ class VoiceConnection extends EventEmitter { Object.assign(this.authentication, data); this.status = VoiceStatus.CONNECTED; clearTimeout(this.connectTimeout); - this.receiver = new VoiceReceiver(this); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, * the connection will already be ready. diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index f8668be4..fe34cab3 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -108,6 +108,8 @@ class VoiceConnectionUDPClient extends EventEmitter { }, }, }); + + socket.on('message', buffer => this.voiceConnection.receiver.packets.push(buffer)); }); const blankMessage = Buffer.alloc(70); diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index 8b626be7..1174f70b 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -21,7 +21,6 @@ class VoiceReceiver extends EventEmitter { * @param {Error|string} error The error or message to debug */ this.packets.on('error', err => this.emit('debug', err)); - this.connection.sockets.udp.socket.on('message', buffer => this.packets.push(buffer)); } /** From 54aff3191e3576dce519634fdbf31bbaabfd907c Mon Sep 17 00:00:00 2001 From: Kyra Date: Fri, 16 Nov 2018 16:55:15 +0100 Subject: [PATCH 012/428] feat(Constants): add error code 50020 (#2953) * feat(Constants): Add error code 50020 Which is throw when using the vanity-url endpoint: https://github.com/discordapp/discord-api-docs/pull/748/ * docs: Document the new code --- src/util/Constants.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index e956c8a4..01ff57d6 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -458,6 +458,7 @@ exports.Colors = { * * NOTE_TOO_LONG * * INVALID_BULK_DELETE_QUANTITY * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL + * * INVALID_OR_TAKEN_INVITE_CODE * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE * * BULK_DELETE_MESSAGE_TOO_OLD * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT @@ -505,6 +506,7 @@ exports.APIErrors = { NOTE_TOO_LONG: 50015, INVALID_BULK_DELETE_QUANTITY: 50016, CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, + INVALID_OR_TAKEN_INVITE_CODE: 50020, CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, BULK_DELETE_MESSAGE_TOO_OLD: 50034, INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, From e793338d7403c98589e92943fee28068c18d481c Mon Sep 17 00:00:00 2001 From: Yukine Date: Sat, 17 Nov 2018 05:46:02 +0100 Subject: [PATCH 013/428] fix: ShardClientUtil#count and ShardClientUtil#id typedef (#2956) --- src/sharding/ShardClientUtil.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 9a2a746b..201568ca 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -44,8 +44,8 @@ class ShardClientUtil { } /** - * ID of this shard - * @type {number} + * Shard ID or array of shard IDs of this client + * @type {number|number[]} * @readonly */ get id() { @@ -58,7 +58,7 @@ class ShardClientUtil { * @readonly */ get count() { - return this.client.options.shardCount; + return this.client.options.totalShardCount; } /** From 81ff5075e4e353c6f5e036f1ead7b85a5aec430e Mon Sep 17 00:00:00 2001 From: Charalampos Fanoulis <38255093+cfanoulis@users.noreply.github.com> Date: Sat, 17 Nov 2018 16:41:12 +0200 Subject: [PATCH 014/428] chore: remove user account checkbox from bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c497cb4f..d78410cc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,11 +28,11 @@ You won't receive any basic help here. - Priority this issue should have – please be realistic and elaborate if possible: -- [ ] I found this issue while running code on a __user account__ + - [ ] I have also tested the issue on latest master, commit hash: From d92ee2ff999954899c891c093f5f107dd686b64f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 17 Nov 2018 15:43:04 +0100 Subject: [PATCH 015/428] feat(GuildChannel): allow to set all options when cloning (#2937) --- src/structures/GuildChannel.js | 24 +++++++++++++----------- typings/index.d.ts | 13 +------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 3567ea4f..d75678b7 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -447,35 +447,37 @@ class GuildChannel extends Channel { return invites; } + /* eslint-disable max-len */ /** * Clones this channel. * @param {Object} [options] The options - * @param {string} [options.name=this.name] Optional name for the new channel, otherwise it has the name - * of this channel - * @param {boolean} [options.withPermissions=true] Whether to clone the channel with this channel's - * permission overwrites - * @param {boolean} [options.withTopic=true] Whether to clone the channel with this channel's topic + * @param {string} [options.name=this.name] Name of the new channel + * @param {OverwriteResolvable[]|Collection} [options.permissionOverwrites=this.permissionOverwrites] + * Permission overwrites of the new channel + * @param {string} [options.type=this.type] Type of the new channel + * @param {string} [options.topic=this.topic] Topic of the new channel (only text) * @param {boolean} [options.nsfw=this.nsfw] Whether the new channel is nsfw (only text) * @param {number} [options.bitrate=this.bitrate] Bitrate of the new channel in bits (only voice) * @param {number} [options.userLimit=this.userLimit] Maximum amount of users allowed in the new channel (only voice) - * @param {ChannelResolvable} [options.parent=this.parent] The parent of the new channel + * @param {number} [options.rateLimitPerUser=ThisType.rateLimitPerUser] Ratelimit per user for the new channel (only text) + * @param {ChannelResolvable} [options.parent=this.parent] Parent of the new channel * @param {string} [options.reason] Reason for cloning this channel * @returns {Promise} */ + /* eslint-enable max-len */ clone(options = {}) { - if (typeof options.withPermissions === 'undefined') options.withPermissions = true; - if (typeof options.withTopic === 'undefined') options.withTopic = true; Util.mergeDefault({ name: this.name, - permissionOverwrites: options.withPermissions ? this.permissionOverwrites : [], - topic: options.withTopic ? this.topic : undefined, + permissionOverwrites: this.permissionOverwrites, + topic: this.topic, + type: this.type, nsfw: this.nsfw, parent: this.parent, bitrate: this.bitrate, userLimit: this.userLimit, + rateLimitPerUser: this.rateLimitPerUser, reason: null, }, options); - options.type = this.type; return this.guild.channels.create(options.name, options); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 30ccf253..faed57ef 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -523,7 +523,7 @@ declare module 'discord.js' { public readonly permissionsLocked: boolean; public readonly position: number; public rawPosition: number; - public clone(options?: GuildChannelCloneOptions): Promise; + public clone(options?: GuildCreateChannelOptions): Promise; public createInvite(options?: InviteOptions): Promise; public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; public edit(data: ChannelData, reason?: string): Promise; @@ -1772,17 +1772,6 @@ declare module 'discord.js' { MESSAGE?: string; }; - type GuildChannelCloneOptions = { - bitrate?: number; - name?: string; - nsfw?: boolean; - parent?: ChannelResolvable; - reason?: string; - userLimit?: number; - withPermissions?: boolean; - withTopic?: boolean; - }; - type GuildChannelResolvable = Snowflake | GuildChannel; type GuildCreateChannelOptions = { From 377ecd73ea1c9c1f5dba47768d97a105075d6bac Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 20 Nov 2018 16:42:27 +0100 Subject: [PATCH 016/428] docs(GuildChannel): fix doc string for clone method --- src/structures/GuildChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index d75678b7..daf20a52 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -464,7 +464,6 @@ class GuildChannel extends Channel { * @param {string} [options.reason] Reason for cloning this channel * @returns {Promise} */ - /* eslint-enable max-len */ clone(options = {}) { Util.mergeDefault({ name: this.name, @@ -480,6 +479,7 @@ class GuildChannel extends Channel { }, options); return this.guild.channels.create(options.name, options); } + /* eslint-enable max-len */ /** * Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel. From 9085138f0d0845cb52442c62e4dbe67bd1d284aa Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Wed, 21 Nov 2018 21:42:37 +0200 Subject: [PATCH 017/428] fix: Sharding Issues & Cleanup (#2952) * fix: Sharding causing constant heartbeat / identify spam * misc: Remove wait param in connect * misc: Wait 2.5 seconds before sending identify again if session is resumable * misc: Remove useless destroy call * nit: Capitalization * fix: Identify on HELLO not connectionOpen * misc: Add different intervals for identify after invalid session - 2500 if we couldn't resume in time - 5000 if we didn't have a session ID (per the docs on identify, that a client can only connect every 5 seconds) - Otherwise, just identify again * misc: Only clear heartbeat if shard is fully dead Reconnect clears it otherwise * fix: Accessing .length on a Collection --- src/client/websocket/WebSocketManager.js | 27 +++++++------- src/client/websocket/WebSocketShard.js | 47 +++++++++++++++++++----- src/sharding/ShardingManager.js | 2 +- src/structures/ClientPresence.js | 4 +- src/structures/Guild.js | 2 +- typings/index.d.ts | 2 +- 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 4e4eec1e..891da3f4 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,3 +1,4 @@ +const Collection = require('../../util/Collection'); const WebSocketShard = require('./WebSocketShard'); const { Events, Status, WSEvents } = require('../../util/Constants'); const PacketHandlers = require('./handlers'); @@ -32,9 +33,9 @@ class WebSocketManager { /** * An array of shards spawned by this WebSocketManager. - * @type {WebSocketShard[]} + * @type {Collection} */ - this.shards = []; + this.shards = new Collection(); /** * An array of queued shards to be spawned by this WebSocketManager. @@ -80,7 +81,7 @@ class WebSocketManager { */ get ping() { const sum = this.shards.reduce((a, b) => a + b.ping, 0); - return sum / this.shards.length; + return sum / this.shards.size; } /** @@ -133,8 +134,8 @@ class WebSocketManager { if (typeof item === 'string' && !isNaN(item)) item = Number(item); if (typeof item === 'number') { - const shard = new WebSocketShard(this, item, this.shards[item]); - this.shards[item] = shard; + const shard = new WebSocketShard(this, item, this.shards.get(item)); + this.shards.set(item, shard); shard.once(Events.READY, () => { this.spawning = false; this.client.setTimeout(() => this._handleSessionLimit(shard), 5000); @@ -161,8 +162,8 @@ class WebSocketManager { this.spawn(this.client.options.shards); } else if (Array.isArray(this.client.options.shards)) { this.debug(`Spawning ${this.client.options.shards.length} shards`); - for (let i = 0; i < this.client.options.shards.length; i++) { - this.spawn(this.client.options.shards[i]); + for (const shard of this.client.options.shards) { + this.spawn(shard); } } else { this.debug(`Spawning ${this.client.options.shardCount} shards`); @@ -190,11 +191,11 @@ class WebSocketManager { if (this.packetQueue.length) { const item = this.packetQueue.shift(); this.client.setImmediate(() => { - this.handlePacket(item.packet, this.shards[item.shardID]); + this.handlePacket(item.packet, this.shards.get(item.shardID)); }); } - if (packet && PacketHandlers[packet.t]) { + if (packet && !this.client.options.disabledEvents.includes(packet.t) && PacketHandlers[packet.t]) { PacketHandlers[packet.t](this.client, packet, shard); } @@ -207,7 +208,7 @@ class WebSocketManager { * @private */ checkReady() { - if (this.shards.filter(s => s).length !== this.client.options.shardCount || + if (this.shards.size !== this.client.options.shardCount || this.shards.some(s => s && s.status !== Status.READY)) { return false; } @@ -257,8 +258,7 @@ class WebSocketManager { * @param {*} packet The packet to send */ broadcast(packet) { - for (const shard of this.shards) { - if (!shard) continue; + for (const shard of this.shards.values()) { shard.send(packet); } } @@ -273,8 +273,7 @@ class WebSocketManager { // Lock calls to spawn this.spawning = true; - for (const shard of this.shards) { - if (!shard) continue; + for (const shard of this.shards.values()) { shard.destroy(); } } diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index e9c01e0b..7aec37e2 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -1,6 +1,7 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); +const Util = require('../../util/Util'); let zlib; try { zlib = require('zlib-sync'); @@ -107,6 +108,12 @@ class WebSocketShard extends EventEmitter { */ this.inflate = null; + /** + * Whether or not the WebSocket is expected to be closed + * @type {boolean} + */ + this.expectingClose = false; + this.connect(); } @@ -143,6 +150,7 @@ class WebSocketShard extends EventEmitter { this.heartbeatInterval = null; } else { this.debug(`Setting a heartbeat interval for ${time}ms`); + if (this.heartbeatInterval) this.manager.client.clearInterval(this.heartbeatInterval); this.heartbeatInterval = this.manager.client.setInterval(() => this.heartbeat(), time); } return; @@ -193,7 +201,7 @@ class WebSocketShard extends EventEmitter { /** * Called whenever a packet is received * @param {Object} packet Packet received - * @returns {boolean} + * @returns {any} * @private */ onPacket(packet) { @@ -229,10 +237,18 @@ class WebSocketShard extends EventEmitter { case OPCodes.RECONNECT: return this.reconnect(); case OPCodes.INVALID_SESSION: - if (!packet.d) this.sessionID = null; this.sequence = -1; this.debug('Session invalidated'); - return this.reconnect(Events.INVALIDATED); + // If the session isn't resumable + if (!packet.d) { + // If we had a session ID before + if (this.sessionID) { + this.sessionID = null; + return this.identify(2500); + } + return this.identify(5000); + } + return this.identify(); case OPCodes.HEARTBEAT_ACK: return this.ackHeartbeat(); case OPCodes.HEARTBEAT: @@ -275,7 +291,7 @@ class WebSocketShard extends EventEmitter { this.manager.client.emit(Events.ERROR, err); return; } - if (packet.t === 'READY') { + if (packet.t === WSEvents.READY) { /** * Emitted when a shard becomes ready * @event WebSocketShard#ready @@ -320,6 +336,7 @@ class WebSocketShard extends EventEmitter { /** * Called whenever a connection to the gateway is closed. * @param {CloseEvent} event Close event that was received + * @returns {void} * @private */ onClose(event) { @@ -333,19 +350,22 @@ class WebSocketShard extends EventEmitter { * @param {number} shardID The shard that disconnected */ this.manager.client.emit(Events.DISCONNECT, event, this.id); - this.debug(WSCodes[event.code]); + this.heartbeat(-1); return; } - this.reconnect(Events.INVALIDATED); + this.expectingClose = false; + this.reconnect(Events.INVALIDATED, 5100); } /** * Identifies the client on a connection. + * @param {?number} [wait=0] Amount of time to wait before identifying * @returns {void} * @private */ - identify() { + identify(wait = 0) { + if (wait) return this.manager.client.setTimeout(this.identify.bind(this), wait); return this.sessionID ? this.identifyResume() : this.identifyNew(); } @@ -427,7 +447,7 @@ class WebSocketShard extends EventEmitter { if (this.ratelimit.remaining === 0) return; if (this.ratelimit.queue.length === 0) return; if (this.ratelimit.remaining === this.ratelimit.total) { - this.ratelimit.resetTimer = this.manager.client.setTimeout(() => { + this.ratelimit.timer = this.manager.client.setTimeout(() => { this.ratelimit.remaining = this.ratelimit.total; this.processQueue(); }, this.ratelimit.time); @@ -443,10 +463,11 @@ class WebSocketShard extends EventEmitter { /** * Triggers a shard reconnect. * @param {?string} [event] The event for the shard to emit - * @returns {void} + * @param {?number} [reconnectIn] Time to wait before reconnecting + * @returns {Promise} * @private */ - reconnect(event) { + async reconnect(event, reconnectIn) { this.heartbeat(-1); this.status = Status.RECONNECTING; @@ -457,6 +478,8 @@ class WebSocketShard extends EventEmitter { this.manager.client.emit(Events.RECONNECTING, this.id); if (event === Events.INVALIDATED) this.emit(event); + this.debug(reconnectIn ? `Reconnecting in ${reconnectIn}ms` : 'Reconnecting now'); + if (reconnectIn) await Util.delayFor(reconnectIn); this.manager.spawn(this.id); } @@ -472,6 +495,10 @@ class WebSocketShard extends EventEmitter { this.ws = null; this.status = Status.DISCONNECTED; this.ratelimit.remaining = this.ratelimit.total; + if (this.ratelimit.timer) { + this.manager.client.clearTimeout(this.ratelimit.timer); + this.ratelimit.timer = null; + } } } module.exports = WebSocketShard; diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 9bfce6fe..b7196229 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -27,7 +27,7 @@ class ShardingManager extends EventEmitter { /** * @param {string} file Path to your shard script file * @param {Object} [options] Options for the sharding manager - * @param {string|number[]} [options.totalShards='auto'] Number of total shards of all shard managers or "auto" + * @param {string|number} [options.totalShards='auto'] Number of total shards of all shard managers or "auto" * @param {string|number[]} [options.shardList='auto'] List of shards to spawn or "auto" * @param {ShardingManagerMode} [options.mode='process'] Which mode to use for shards * @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 06924589..68dda64c 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -15,10 +15,10 @@ class ClientPresence extends Presence { this.client.ws.broadcast({ op: OPCodes.STATUS_UPDATE, d: packet }); } else if (Array.isArray(presence.shardID)) { for (const shardID of presence.shardID) { - this.client.ws.shards[shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + this.client.ws.shards.get(shardID).send({ op: OPCodes.STATUS_UPDATE, d: packet }); } } else { - this.client.ws.shards[presence.shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + this.client.ws.shards.get(presence.shardID).send({ op: OPCodes.STATUS_UPDATE, d: packet }); } return this; } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index ffef8578..a194ea15 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -88,7 +88,7 @@ class Guild extends Base { * @readonly */ get shard() { - return this.client.ws.shards[this.shardID]; + return this.client.ws.shards.get(this.shardID); } /* eslint-disable complexity */ diff --git a/typings/index.d.ts b/typings/index.d.ts index faed57ef..71366d56 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1291,7 +1291,7 @@ declare module 'discord.js' { public readonly client: Client; public gateway: string | undefined; public readonly ping: number; - public shards: WebSocketShard[]; + public shards: Collection; public sessionStartLimit: { total: number; remaining: number; reset_after: number; }; public status: Status; public broadcast(packet: any): void; From a8b47a7a6cfa7a823dbb47f250fa5447f4c5320f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 23 Nov 2018 19:46:11 +0100 Subject: [PATCH 018/428] feat(GuildChannelStore): add support for create to accept a position --- src/stores/GuildChannelStore.js | 15 ++++++++++++++- typings/index.d.ts | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index e67bcc1c..d82ddc05 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -33,6 +33,7 @@ class GuildChannelStore extends DataStore { * @param {ChannelResolvable} [options.parent] Parent of the new channel * @param {OverwriteResolvable[]|Collection} [options.permissionOverwrites] * Permission overwrites of the new channel + * @param {number} [options.position] Position of the new channel * @param {number} [options.rateLimitPerUser] The ratelimit per user for the channel * @param {string} [options.reason] Reason for creating the channel * @returns {Promise} @@ -54,7 +55,18 @@ class GuildChannelStore extends DataStore { * }) */ async create(name, options = {}) { - let { type, topic, nsfw, bitrate, userLimit, parent, permissionOverwrites, rateLimitPerUser, reason } = options; + let { + type, + topic, + nsfw, + bitrate, + userLimit, + parent, + permissionOverwrites, + position, + rateLimitPerUser, + reason, + } = options; if (parent) parent = this.client.channels.resolveID(parent); if (permissionOverwrites) { permissionOverwrites = permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); @@ -69,6 +81,7 @@ class GuildChannelStore extends DataStore { bitrate, user_limit: userLimit, parent_id: parent, + position, permission_overwrites: permissionOverwrites, rate_limit_per_user: rateLimitPerUser, }, diff --git a/typings/index.d.ts b/typings/index.d.ts index 71366d56..ea8d55e7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1782,6 +1782,7 @@ declare module 'discord.js' { parent?: ChannelResolvable; permissionOverwrites?: OverwriteResolvable[] | Collection; rateLimitPerUser?: number; + position?: number; reason?: string }; From 1d1b3f25e1ede5207212f8aafe985b423bf9c464 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Tue, 27 Nov 2018 15:12:25 -0500 Subject: [PATCH 019/428] docs: add documentation for Client#channelCreate (#2967) --- src/client/actions/ChannelCreate.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 09e74f0f..b50d42aa 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -13,4 +13,10 @@ class ChannelCreateAction extends Action { } } +/** + * Emitted whenever a channel is created. + * @event Client#channelCreate + * @param {GroupDMChannel|GuildChannel} channel The channel that was created + */ + module.exports = ChannelCreateAction; From fd21bbb7bfbfe0b51111653e140248145525d95f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 27 Nov 2018 21:28:36 +0100 Subject: [PATCH 020/428] docs: move event docstrings to the emitting line of code --- src/client/actions/ChannelCreate.js | 11 +++++------ src/client/actions/ChannelDelete.js | 11 +++++------ src/client/actions/GuildBanRemove.js | 8 +++++++- src/client/actions/GuildEmojiCreate.js | 11 +++++------ src/client/actions/GuildEmojiDelete.js | 11 +++++------ src/client/actions/GuildEmojiUpdate.js | 13 ++++++------- src/client/actions/GuildIntegrationsUpdate.js | 10 +++++----- src/client/actions/GuildMemberRemove.js | 10 +++++----- src/client/actions/GuildRoleCreate.js | 10 +++++----- src/client/actions/GuildRoleDelete.js | 10 +++++----- src/client/actions/GuildRoleUpdate.js | 12 ++++++------ src/client/actions/GuildUpdate.js | 12 ++++++------ src/client/actions/MessageCreate.js | 10 +++++----- src/client/actions/MessageDelete.js | 10 +++++----- src/client/actions/MessageDeleteBulk.js | 10 +++++----- src/client/actions/MessageReactionAdd.js | 7 ------- src/client/actions/MessageReactionRemove.js | 12 ++++++------ src/client/actions/PresenceUpdate.js | 6 ++++++ src/client/actions/VoiceStateUpdate.js | 12 ++++++------ src/client/actions/WebhooksUpdate.js | 10 +++++----- .../websocket/handlers/MESSAGE_REACTION_ADD.js | 6 ++++++ 21 files changed, 109 insertions(+), 103 deletions(-) diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index b50d42aa..57b4e6df 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -7,16 +7,15 @@ class ChannelCreateAction extends Action { const existing = client.channels.has(data.id); const channel = client.channels.add(data); if (!existing && channel) { + /** + * Emitted whenever a channel is created. + * @event Client#channelCreate + * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was created + */ client.emit(Events.CHANNEL_CREATE, channel); } return { channel }; } } -/** - * Emitted whenever a channel is created. - * @event Client#channelCreate - * @param {GroupDMChannel|GuildChannel} channel The channel that was created - */ - module.exports = ChannelCreateAction; diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index f183bbc7..7f182e60 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -14,6 +14,11 @@ class ChannelDeleteAction extends Action { if (channel) { client.channels.remove(channel.id); channel.deleted = true; + /** + * Emitted whenever a channel is deleted. + * @event Client#channelDelete + * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was deleted + */ client.emit(Events.CHANNEL_DELETE, channel); } @@ -21,10 +26,4 @@ class ChannelDeleteAction extends Action { } } -/** - * Emitted whenever a channel is deleted. - * @event Client#channelDelete - * @param {GroupDMChannel|GuildChannel} channel The channel that was deleted - */ - module.exports = ChannelDeleteAction; diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 782b5fe2..7d19c75b 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -6,7 +6,13 @@ class GuildBanRemove extends Action { const client = this.client; const guild = client.guilds.get(data.guild_id); const user = client.users.add(data.user); - if (guild && user) client.emit(Events.GUILD_BAN_REMOVE, guild, user); + /** + * Emitted whenever a member is unbanned from a guild. + * @event Client#guildBanRemove + * @param {Guild} guild The guild that the unban occurred in + * @param {User} user The user that was unbanned + */ + if (guild && user) client.emit(Events.GUILD_BAN_REMOVEGUILD_BAN_REMOVE, guild, user); } } diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js index 7fc955a0..121e89ab 100644 --- a/src/client/actions/GuildEmojiCreate.js +++ b/src/client/actions/GuildEmojiCreate.js @@ -4,15 +4,14 @@ const { Events } = require('../../util/Constants'); class GuildEmojiCreateAction extends Action { handle(guild, createdEmoji) { const emoji = guild.emojis.add(createdEmoji); + /** + * Emitted whenever a custom emoji is created in a guild. + * @event Client#emojiCreate + * @param {GuildEmoji} emoji The emoji that was created + */ this.client.emit(Events.GUILD_EMOJI_CREATE, emoji); return { emoji }; } } -/** - * Emitted whenever a custom emoji is created in a guild. - * @event Client#emojiCreate - * @param {GuildEmoji} emoji The emoji that was created - */ - module.exports = GuildEmojiCreateAction; diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index 82df6ec8..7b94ff41 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -5,15 +5,14 @@ class GuildEmojiDeleteAction extends Action { handle(emoji) { emoji.guild.emojis.remove(emoji.id); emoji.deleted = true; + /** + * Emitted whenever a custom emoji is deleted in a guild. + * @event Client#emojiDelete + * @param {GuildEmoji} emoji The emoji that was deleted + */ this.client.emit(Events.GUILD_EMOJI_DELETE, emoji); return { emoji }; } } -/** - * Emitted whenever a custom emoji is deleted in a guild. - * @event Client#emojiDelete - * @param {GuildEmoji} emoji The emoji that was deleted - */ - module.exports = GuildEmojiDeleteAction; diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js index e6accf2c..34e235a0 100644 --- a/src/client/actions/GuildEmojiUpdate.js +++ b/src/client/actions/GuildEmojiUpdate.js @@ -4,16 +4,15 @@ const { Events } = require('../../util/Constants'); class GuildEmojiUpdateAction extends Action { handle(current, data) { const old = current._update(data); + /** + * Emitted whenever a custom emoji is updated in a guild. + * @event Client#emojiUpdate + * @param {GuildEmoji} oldEmoji The old emoji + * @param {GuildEmoji} newEmoji The new emoji + */ this.client.emit(Events.GUILD_EMOJI_UPDATE, old, current); return { emoji: current }; } } -/** - * Emitted whenever a custom emoji is updated in a guild. - * @event Client#emojiUpdate - * @param {GuildEmoji} oldEmoji The old emoji - * @param {GuildEmoji} newEmoji The new emoji - */ - module.exports = GuildEmojiUpdateAction; diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js index e9c3bdbf..8bb6eabf 100644 --- a/src/client/actions/GuildIntegrationsUpdate.js +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -5,14 +5,14 @@ class GuildIntegrationsUpdate extends Action { handle(data) { const client = this.client; const guild = client.guilds.get(data.guild_id); + /** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); } } module.exports = GuildIntegrationsUpdate; -/** - * Emitted whenever a guild integration is updated - * @event Client#guildIntegrationsUpdate - * @param {Guild} guild The guild whose integrations were updated - */ diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index febeb73a..62373e48 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -13,6 +13,11 @@ class GuildMemberRemoveAction extends Action { guild.voiceStates.delete(member.id); member.deleted = true; guild.members.remove(member.id); + /** + * Emitted whenever a member leaves a guild, or is kicked. + * @event Client#guildMemberRemove + * @param {GuildMember} member The member that has left/been kicked from the guild + */ if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } } @@ -20,10 +25,5 @@ class GuildMemberRemoveAction extends Action { } } -/** - * Emitted whenever a member leaves a guild, or is kicked. - * @event Client#guildMemberRemove - * @param {GuildMember} member The member that has left/been kicked from the guild - */ module.exports = GuildMemberRemoveAction; diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js index b4930399..e4657213 100644 --- a/src/client/actions/GuildRoleCreate.js +++ b/src/client/actions/GuildRoleCreate.js @@ -9,16 +9,16 @@ class GuildRoleCreate extends Action { if (guild) { const already = guild.roles.has(data.role.id); role = guild.roles.add(data.role); + /** + * Emitted whenever a role is created. + * @event Client#roleCreate + * @param {Role} role The role that was created + */ if (!already) client.emit(Events.GUILD_ROLE_CREATE, role); } return { role }; } } -/** - * Emitted whenever a role is created. - * @event Client#roleCreate - * @param {Role} role The role that was created - */ module.exports = GuildRoleCreate; diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js index ce6bcaed..0cecc8ef 100644 --- a/src/client/actions/GuildRoleDelete.js +++ b/src/client/actions/GuildRoleDelete.js @@ -12,6 +12,11 @@ class GuildRoleDeleteAction extends Action { if (role) { guild.roles.remove(data.role_id); role.deleted = true; + /** + * Emitted whenever a guild role is deleted. + * @event Client#roleDelete + * @param {Role} role The role that was deleted + */ client.emit(Events.GUILD_ROLE_DELETE, role); } } @@ -20,10 +25,5 @@ class GuildRoleDeleteAction extends Action { } } -/** - * Emitted whenever a guild role is deleted. - * @event Client#roleDelete - * @param {Role} role The role that was deleted - */ module.exports = GuildRoleDeleteAction; diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js index 2437264f..c2e082c7 100644 --- a/src/client/actions/GuildRoleUpdate.js +++ b/src/client/actions/GuildRoleUpdate.js @@ -12,6 +12,12 @@ class GuildRoleUpdateAction extends Action { const role = guild.roles.get(data.role.id); if (role) { old = role._update(data.role); + /** + * Emitted whenever a guild role is updated. + * @event Client#roleUpdate + * @param {Role} oldRole The role before the update + * @param {Role} newRole The role after the update + */ client.emit(Events.GUILD_ROLE_UPDATE, old, role); } @@ -28,11 +34,5 @@ class GuildRoleUpdateAction extends Action { } } -/** - * Emitted whenever a guild role is updated. - * @event Client#roleUpdate - * @param {Role} oldRole The role before the update - * @param {Role} newRole The role after the update - */ module.exports = GuildRoleUpdateAction; diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js index b828bb10..ac592d27 100644 --- a/src/client/actions/GuildUpdate.js +++ b/src/client/actions/GuildUpdate.js @@ -8,6 +8,12 @@ class GuildUpdateAction extends Action { const guild = client.guilds.get(data.id); if (guild) { const old = guild._update(data); + /** + * Emitted whenever a guild is updated - e.g. name change. + * @event Client#guildUpdate + * @param {Guild} oldGuild The guild before the update + * @param {Guild} newGuild The guild after the update + */ client.emit(Events.GUILD_UPDATE, old, guild); return { old, @@ -22,11 +28,5 @@ class GuildUpdateAction extends Action { } } -/** - * Emitted whenever a guild is updated - e.g. name change. - * @event Client#guildUpdate - * @param {Guild} oldGuild The guild before the update - * @param {Guild} newGuild The guild after the update - */ module.exports = GuildUpdateAction; diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index 9308aedb..e34eabc2 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -23,6 +23,11 @@ class MessageCreateAction extends Action { member.lastMessageChannelID = channel.id; } + /** + * Emitted whenever a message is created. + * @event Client#message + * @param {Message} message The created message + */ client.emit(Events.MESSAGE_CREATE, message); return { message }; } @@ -31,10 +36,5 @@ class MessageCreateAction extends Action { } } -/** - * Emitted whenever a message is created. - * @event Client#message - * @param {Message} message The created message - */ module.exports = MessageCreateAction; diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index c31c39cb..cbe555e8 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -12,6 +12,11 @@ class MessageDeleteAction extends Action { if (message) { channel.messages.delete(message.id); message.deleted = true; + /** + * Emitted whenever a message is deleted. + * @event Client#messageDelete + * @param {Message} message The deleted message + */ client.emit(Events.MESSAGE_DELETE, message); } } @@ -20,10 +25,5 @@ class MessageDeleteAction extends Action { } } -/** - * Emitted whenever a message is deleted. - * @event Client#messageDelete - * @param {Message} message The deleted message - */ module.exports = MessageDeleteAction; diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index e12eaa73..ac8d9b39 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -19,6 +19,11 @@ class MessageDeleteBulkAction extends Action { } } + /** + * Emitted whenever messages are deleted in bulk. + * @event Client#messageDeleteBulk + * @param {Collection} messages The deleted messages, mapped by their ID + */ if (messages.size > 0) client.emit(Events.MESSAGE_BULK_DELETE, messages); return { messages }; } @@ -26,10 +31,5 @@ class MessageDeleteBulkAction extends Action { } } -/** - * Emitted whenever messages are deleted in bulk. - * @event Client#messageDeleteBulk - * @param {Collection} messages The deleted messages, mapped by their ID - */ module.exports = MessageDeleteBulkAction; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 0708d338..06b146df 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -29,11 +29,4 @@ class MessageReactionAdd extends Action { } } -/** - * Emitted whenever a reaction is added to a cached message. - * @event Client#messageReactionAdd - * @param {MessageReaction} messageReaction The reaction object - * @param {User} user The user that applied the guild or reaction emoji - */ - module.exports = MessageReactionAdd; diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 63e349f7..af76c4fd 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -24,17 +24,17 @@ class MessageReactionRemove extends Action { const reaction = message.reactions.get(emojiID); if (!reaction) return false; reaction._remove(user); + /** + * Emitted whenever a reaction is removed from a cached message. + * @event Client#messageReactionRemove + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user whose emoji or reaction emoji was removed + */ this.client.emit(Events.MESSAGE_REACTION_REMOVE, reaction, user); return { message, reaction, user }; } } -/** - * Emitted whenever a reaction is removed from a cached message. - * @event Client#messageReactionRemove - * @param {MessageReaction} messageReaction The reaction object - * @param {User} user The user whose emoji or reaction emoji was removed - */ module.exports = MessageReactionRemove; diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 53b18c80..538e0e8a 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -28,6 +28,12 @@ class PresenceUpdateAction extends Action { const old = member._clone(); if (member.presence) old.frozenPresence = member.presence._clone(); guild.presences.add(data); + /** + * Emitted whenever a guild member's presence changes, or they change one of their details. + * @event Client#presenceUpdate + * @param {GuildMember} oldMember The member before the presence update + * @param {GuildMember} newMember The member after the presence update + */ this.client.emit(Events.PRESENCE_UPDATE, old, member); } else { guild.presences.add(data); diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 0eb9c5a5..80e120cb 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -27,16 +27,16 @@ class VoiceStateUpdate extends Action { client.emit('self.voiceStateUpdate', data); } + /** + * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. + * @event Client#voiceStateUpdate + * @param {?VoiceState} oldState The voice state before the update + * @param {VoiceState} newState The voice state after the update + */ client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); } } } -/** - * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. - * @event Client#voiceStateUpdate - * @param {?VoiceState} oldState The voice state before the update - * @param {VoiceState} newState The voice state after the update - */ module.exports = VoiceStateUpdate; diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 5ffc41a4..08ec838f 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -5,14 +5,14 @@ class WebhooksUpdate extends Action { handle(data) { const client = this.client; const channel = client.channels.get(data.channel_id); + /** + * Emitted whenever a guild text channel has its webhooks changed. + * @event Client#webhookUpdate + * @param {TextChannel} channel The channel that had a webhook update + */ if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); } } -/** - * Emitted whenever a guild text channel has its webhooks changed. - * @event Client#webhookUpdate - * @param {TextChannel} channel The channel that had a webhook update - */ module.exports = WebhooksUpdate; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js index d8176212..e2ac2c07 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -2,5 +2,11 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { const { user, reaction } = client.actions.MessageReactionAdd.handle(packet.d); + /** + * Emitted whenever a reaction is added to a cached message. + * @event Client#messageReactionAdd + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user that applied the guild or reaction emoji + */ if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); }; From 23a16c3a7369d021e6b76db938831599fd14fa78 Mon Sep 17 00:00:00 2001 From: Darqam Date: Tue, 27 Nov 2018 21:41:34 +0100 Subject: [PATCH 021/428] fix(GuildChannel): add explicit channel resolve error to member edit (#2958) * Add explicit error to setting invalid voice channel * restrict to guild Co-Authored-By: Darqam * add a more explicit error and channel type check * bad tab --- src/errors/Messages.js | 1 + src/structures/GuildMember.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 2e2ec97b..1abdd4ca 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -84,6 +84,7 @@ const Messages = { MESSAGE_SPLIT_MISSING: 'Message exceeds the max length and contains no split characters.', GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', + GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', GUILD_OWNED: 'Guild is owned by the client.', GUILD_RESTRICTED: (state = false) => `Guild is ${state ? 'already' : 'not'} restricted.`, diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index d910117d..a2e82d79 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -248,7 +248,11 @@ class GuildMember extends Base { */ edit(data, reason) { if (data.channel) { - data.channel_id = this.client.channels.resolve(data.channel).id; + data.channel = this.guild.channels.resolve(data.channel); + if (!data.channel || data.channel.type !== 'voice') { + throw new Error('GUILD_VOICE_CHANNEL_RESOLVE'); + } + data.channel_id = data.channel.id; data.channel = null; } if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); From ecaec29380d31dcd94a30b4242bf26c5e4c395f7 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 27 Nov 2018 21:42:28 +0100 Subject: [PATCH 022/428] fix(Util): throw an explicit error if a chunk exceeds the max length (#2936) * fix(Util): throw an explicit error if a chunk exceeds the max length * refactor(Util): consolidate both errors in splitMessage into one * revert(Messages): do not unnecessarily change the error code * revert(Messages): do not remove the word 'the' --- src/errors/Messages.js | 4 +--- src/util/Util.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 1abdd4ca..5f237ac5 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -73,7 +73,7 @@ const Messages = { TYPING_COUNT: 'Count must be at least 1', - SPLIT_MAX_LEN: 'Message exceeds the max length and contains no split characters.', + SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.', BAN_RESOLVE_ID: (ban = false) => `Couldn't resolve the user ID to ${ban ? 'ban' : 'unban'}.`, @@ -81,8 +81,6 @@ const Messages = { SEARCH_CHANNEL_TYPE: 'Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.', - MESSAGE_SPLIT_MISSING: 'Message exceeds the max length and contains no split characters.', - GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', diff --git a/src/util/Util.js b/src/util/Util.js index 119d0c7c..562b81a4 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -56,7 +56,7 @@ class Util { static splitMessage(text, { maxLength = 2000, 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.some(chunk => chunk.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN'); const messages = []; let msg = ''; for (const chunk of splitText) { From 42505b78c15306491ac3a605f86a3e010bc80958 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 3 Dec 2018 15:19:10 -0600 Subject: [PATCH 023/428] chore: add strict mode (#2974) --- .eslintrc.json | 1 + src/WebSocket.js | 2 ++ src/client/BaseClient.js | 2 ++ src/client/Client.js | 2 ++ src/client/WebhookClient.js | 2 ++ src/client/actions/Action.js | 2 ++ src/client/actions/ActionsManager.js | 2 ++ src/client/actions/ChannelCreate.js | 2 ++ src/client/actions/ChannelDelete.js | 2 ++ src/client/actions/ChannelUpdate.js | 2 ++ src/client/actions/GuildBanRemove.js | 2 ++ src/client/actions/GuildChannelsPositionUpdate.js | 2 ++ src/client/actions/GuildDelete.js | 2 ++ src/client/actions/GuildEmojiCreate.js | 2 ++ src/client/actions/GuildEmojiDelete.js | 2 ++ src/client/actions/GuildEmojiUpdate.js | 2 ++ src/client/actions/GuildEmojisUpdate.js | 2 ++ src/client/actions/GuildIntegrationsUpdate.js | 2 ++ src/client/actions/GuildMemberRemove.js | 2 ++ src/client/actions/GuildRoleCreate.js | 2 ++ src/client/actions/GuildRoleDelete.js | 2 ++ src/client/actions/GuildRoleUpdate.js | 2 ++ src/client/actions/GuildRolesPositionUpdate.js | 2 ++ src/client/actions/GuildUpdate.js | 2 ++ src/client/actions/MessageCreate.js | 2 ++ src/client/actions/MessageDelete.js | 2 ++ src/client/actions/MessageDeleteBulk.js | 2 ++ src/client/actions/MessageReactionAdd.js | 2 ++ src/client/actions/MessageReactionRemove.js | 2 ++ src/client/actions/MessageReactionRemoveAll.js | 2 ++ src/client/actions/MessageUpdate.js | 2 ++ src/client/actions/PresenceUpdate.js | 2 ++ src/client/actions/UserUpdate.js | 2 ++ src/client/actions/VoiceStateUpdate.js | 2 ++ src/client/actions/WebhooksUpdate.js | 2 ++ src/client/voice/ClientVoiceManager.js | 2 ++ src/client/voice/VoiceBroadcast.js | 2 ++ src/client/voice/VoiceConnection.js | 2 ++ src/client/voice/dispatcher/BroadcastDispatcher.js | 2 ++ src/client/voice/dispatcher/StreamDispatcher.js | 2 ++ src/client/voice/networking/VoiceUDPClient.js | 2 ++ src/client/voice/networking/VoiceWebSocket.js | 2 ++ src/client/voice/player/AudioPlayer.js | 2 ++ src/client/voice/player/BasePlayer.js | 2 ++ src/client/voice/player/BroadcastAudioPlayer.js | 2 ++ src/client/voice/receiver/PacketHandler.js | 2 ++ src/client/voice/receiver/Receiver.js | 2 ++ src/client/voice/util/DispatcherSet.js | 2 ++ src/client/voice/util/PlayInterface.js | 2 ++ src/client/voice/util/Secretbox.js | 2 ++ src/client/voice/util/Silence.js | 2 ++ src/client/voice/util/VolumeInterface.js | 2 ++ src/client/websocket/WebSocketManager.js | 2 ++ src/client/websocket/WebSocketShard.js | 2 ++ src/client/websocket/handlers/CHANNEL_CREATE.js | 2 ++ src/client/websocket/handlers/CHANNEL_DELETE.js | 2 ++ src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 ++ src/client/websocket/handlers/CHANNEL_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_BAN_ADD.js | 2 ++ src/client/websocket/handlers/GUILD_BAN_REMOVE.js | 2 ++ src/client/websocket/handlers/GUILD_CREATE.js | 2 ++ src/client/websocket/handlers/GUILD_DELETE.js | 2 ++ src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js | 2 ++ src/client/websocket/handlers/GUILD_MEMBER_ADD.js | 2 ++ src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js | 2 ++ src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_ROLE_CREATE.js | 2 ++ src/client/websocket/handlers/GUILD_ROLE_DELETE.js | 2 ++ src/client/websocket/handlers/GUILD_ROLE_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_SYNC.js | 2 ++ src/client/websocket/handlers/GUILD_UPDATE.js | 2 ++ src/client/websocket/handlers/MESSAGE_CREATE.js | 2 ++ src/client/websocket/handlers/MESSAGE_DELETE.js | 2 ++ src/client/websocket/handlers/MESSAGE_DELETE_BULK.js | 2 ++ src/client/websocket/handlers/MESSAGE_REACTION_ADD.js | 2 ++ src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js | 2 ++ src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js | 2 ++ src/client/websocket/handlers/MESSAGE_UPDATE.js | 2 ++ src/client/websocket/handlers/PRESENCE_UPDATE.js | 2 ++ src/client/websocket/handlers/READY.js | 2 ++ src/client/websocket/handlers/RESUMED.js | 2 ++ src/client/websocket/handlers/TYPING_START.js | 2 ++ src/client/websocket/handlers/USER_UPDATE.js | 2 ++ src/client/websocket/handlers/VOICE_SERVER_UPDATE.js | 2 ++ src/client/websocket/handlers/VOICE_STATE_UPDATE.js | 2 ++ src/client/websocket/handlers/WEBHOOKS_UPDATE.js | 2 ++ src/client/websocket/handlers/index.js | 2 ++ src/errors/DJSError.js | 2 ++ src/errors/Messages.js | 2 ++ src/errors/index.js | 2 ++ src/index.js | 2 ++ src/rest/APIRequest.js | 2 ++ src/rest/APIRouter.js | 2 ++ src/rest/DiscordAPIError.js | 2 ++ src/rest/HTTPError.js | 2 ++ src/rest/RESTManager.js | 2 ++ src/rest/RequestHandler.js | 2 ++ src/sharding/Shard.js | 2 ++ src/sharding/ShardClientUtil.js | 2 ++ src/sharding/ShardingManager.js | 2 ++ src/stores/ChannelStore.js | 2 ++ src/stores/DataStore.js | 2 ++ src/stores/GuildChannelStore.js | 2 ++ src/stores/GuildEmojiRoleStore.js | 2 ++ src/stores/GuildEmojiStore.js | 2 ++ src/stores/GuildMemberRoleStore.js | 2 ++ src/stores/GuildMemberStore.js | 2 ++ src/stores/GuildStore.js | 2 ++ src/stores/MessageStore.js | 2 ++ src/stores/PresenceStore.js | 2 ++ src/stores/ReactionStore.js | 2 ++ src/stores/ReactionUserStore.js | 2 ++ src/stores/RoleStore.js | 2 ++ src/stores/UserStore.js | 2 ++ src/stores/VoiceStateStore.js | 2 ++ src/structures/APIMessage.js | 2 ++ src/structures/Base.js | 2 ++ src/structures/CategoryChannel.js | 2 ++ src/structures/Channel.js | 2 ++ src/structures/ClientApplication.js | 2 ++ src/structures/ClientPresence.js | 2 ++ src/structures/ClientUser.js | 2 ++ src/structures/DMChannel.js | 2 ++ src/structures/Emoji.js | 2 ++ src/structures/GroupDMChannel.js | 2 ++ src/structures/Guild.js | 2 ++ src/structures/GuildAuditLogs.js | 2 ++ src/structures/GuildChannel.js | 2 ++ src/structures/GuildEmoji.js | 2 ++ src/structures/GuildMember.js | 2 ++ src/structures/Integration.js | 2 ++ src/structures/Invite.js | 2 ++ src/structures/Message.js | 2 ++ src/structures/MessageAttachment.js | 2 ++ src/structures/MessageCollector.js | 2 ++ src/structures/MessageEmbed.js | 2 ++ src/structures/MessageMentions.js | 2 ++ src/structures/MessageReaction.js | 2 ++ src/structures/PermissionOverwrites.js | 2 ++ src/structures/Presence.js | 2 ++ src/structures/ReactionCollector.js | 2 ++ src/structures/ReactionEmoji.js | 2 ++ src/structures/Role.js | 2 ++ src/structures/TextChannel.js | 2 ++ src/structures/User.js | 2 ++ src/structures/VoiceChannel.js | 2 ++ src/structures/VoiceRegion.js | 2 ++ src/structures/VoiceState.js | 2 ++ src/structures/Webhook.js | 2 ++ src/structures/interfaces/Collector.js | 2 ++ src/structures/interfaces/TextBasedChannel.js | 2 ++ src/util/ActivityFlags.js | 2 ++ src/util/BitField.js | 2 ++ src/util/Collection.js | 2 ++ src/util/Constants.js | 2 ++ src/util/DataResolver.js | 2 ++ src/util/Permissions.js | 2 ++ src/util/Snowflake.js | 2 ++ src/util/Speaking.js | 2 ++ src/util/Structures.js | 2 ++ src/util/Util.js | 2 ++ webpack.config.js | 2 ++ 164 files changed, 327 insertions(+) diff --git a/.eslintrc.json b/.eslintrc.json index 9b7c229b..a4f08d68 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,6 +11,7 @@ { "files": ["*.browser.js"], "env": { "browser": true } } ], "rules": { + "strict": ["error", "global"], "no-await-in-loop": "warn", "no-compare-neg-zero": "error", "no-extra-parens": ["warn", "all", { diff --git a/src/WebSocket.js b/src/WebSocket.js index c6f4209f..2a303cc4 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -1,3 +1,5 @@ +'use strict'; + const { browser } = require('./util/Constants'); const querystring = require('querystring'); try { diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index c9fce590..68621360 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -1,3 +1,5 @@ +'use strict'; + require('setimmediate'); const EventEmitter = require('events'); const RESTManager = require('../rest/RESTManager'); diff --git a/src/client/Client.js b/src/client/Client.js index 5d5deddf..1bc8232b 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,3 +1,5 @@ +'use strict'; + const BaseClient = require('./BaseClient'); const Permissions = require('../util/Permissions'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js index 0a848d80..94e17a71 100644 --- a/src/client/WebhookClient.js +++ b/src/client/WebhookClient.js @@ -1,3 +1,5 @@ +'use strict'; + const Webhook = require('../structures/Webhook'); const BaseClient = require('./BaseClient'); diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 8fdadc92..791eaa00 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -1,3 +1,5 @@ +'use strict'; + /* ABOUT ACTIONS diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index f349b9bc..268e4371 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -1,3 +1,5 @@ +'use strict'; + class ActionsManager { constructor(client) { this.client = client; diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 57b4e6df..4a9d17d4 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index 7f182e60..b9909f8b 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js index ac2d4f69..b610ea7c 100644 --- a/src/client/actions/ChannelUpdate.js +++ b/src/client/actions/ChannelUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class ChannelUpdateAction extends Action { diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 7d19c75b..62779137 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildChannelsPositionUpdate.js b/src/client/actions/GuildChannelsPositionUpdate.js index 863d7dc5..b2111594 100644 --- a/src/client/actions/GuildChannelsPositionUpdate.js +++ b/src/client/actions/GuildChannelsPositionUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class GuildChannelsPositionUpdate extends Action { diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 27ea1b00..7c61ebac 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js index 121e89ab..379c62e0 100644 --- a/src/client/actions/GuildEmojiCreate.js +++ b/src/client/actions/GuildEmojiCreate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index 7b94ff41..d5c973af 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js index 34e235a0..9fa59c96 100644 --- a/src/client/actions/GuildEmojiUpdate.js +++ b/src/client/actions/GuildEmojiUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildEmojisUpdate.js b/src/client/actions/GuildEmojisUpdate.js index aaa89847..d6902d53 100644 --- a/src/client/actions/GuildEmojisUpdate.js +++ b/src/client/actions/GuildEmojisUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class GuildEmojisUpdateAction extends Action { diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js index 8bb6eabf..a8e91077 100644 --- a/src/client/actions/GuildIntegrationsUpdate.js +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 62373e48..6a4ae397 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events, Status } = require('../../util/Constants'); diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js index e4657213..36111f06 100644 --- a/src/client/actions/GuildRoleCreate.js +++ b/src/client/actions/GuildRoleCreate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js index 0cecc8ef..31b13b81 100644 --- a/src/client/actions/GuildRoleDelete.js +++ b/src/client/actions/GuildRoleDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js index c2e082c7..bf61c787 100644 --- a/src/client/actions/GuildRoleUpdate.js +++ b/src/client/actions/GuildRoleUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildRolesPositionUpdate.js b/src/client/actions/GuildRolesPositionUpdate.js index af29702a..f09f1143 100644 --- a/src/client/actions/GuildRolesPositionUpdate.js +++ b/src/client/actions/GuildRolesPositionUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class GuildRolesPositionUpdate extends Action { diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js index ac592d27..6d7cf9b4 100644 --- a/src/client/actions/GuildUpdate.js +++ b/src/client/actions/GuildUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index e34eabc2..d4ef6400 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index cbe555e8..d66d10a2 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index ac8d9b39..53f4ba05 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const Collection = require('../../util/Collection'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 06b146df..a800c3c7 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); /* diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index af76c4fd..7ab5be28 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js index ac03c6e8..f3273097 100644 --- a/src/client/actions/MessageReactionRemoveAll.js +++ b/src/client/actions/MessageReactionRemoveAll.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js index 4fbdce4e..be26b269 100644 --- a/src/client/actions/MessageUpdate.js +++ b/src/client/actions/MessageUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class MessageUpdateAction extends Action { diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 538e0e8a..649bf6ce 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index 3ff40790..a762511f 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 80e120cb..e4efa129 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); const VoiceState = require('../../structures/VoiceState'); diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 08ec838f..69e28aec 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index d55a809f..a9081efd 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../../util/Collection'); const { VoiceStatus } = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 1bc4e282..602f5920 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer'); const DispatcherSet = require('./util/DispatcherSet'); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6a4d4cda..078781b5 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -1,3 +1,5 @@ +'use strict'; + const VoiceWebSocket = require('./networking/VoiceWebSocket'); const VoiceUDP = require('./networking/VoiceUDPClient'); const Util = require('../../util/Util'); diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index 90cff6a4..e5c7650c 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -1,3 +1,5 @@ +'use strict'; + const StreamDispatcher = require('./StreamDispatcher'); /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 7bb0ef1b..93c6df81 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -1,3 +1,5 @@ +'use strict'; + const VolumeInterface = require('../util/VolumeInterface'); const { Writable } = require('stream'); diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index fe34cab3..c1dd9e3d 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -1,3 +1,5 @@ +'use strict'; + const udp = require('dgram'); const { VoiceOPCodes } = require('../../../util/Constants'); const EventEmitter = require('events'); diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index bd03ea54..d556145b 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -1,3 +1,5 @@ +'use strict'; + const { OPCodes, VoiceOPCodes } = require('../../../util/Constants'); const EventEmitter = require('events'); const { Error } = require('../../../errors'); diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index e3381f5c..3ce94d81 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -1,3 +1,5 @@ +'use strict'; + const BasePlayer = require('./BasePlayer'); /** diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index e19984df..c9c6cf0d 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events').EventEmitter; const { Readable: ReadableStream } = require('stream'); const prism = require('prism-media'); diff --git a/src/client/voice/player/BroadcastAudioPlayer.js b/src/client/voice/player/BroadcastAudioPlayer.js index 052c9ea0..c1cab9f4 100644 --- a/src/client/voice/player/BroadcastAudioPlayer.js +++ b/src/client/voice/player/BroadcastAudioPlayer.js @@ -1,3 +1,5 @@ +'use strict'; + const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher'); const BasePlayer = require('./BasePlayer'); diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 1ea702c1..7189f112 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -1,3 +1,5 @@ +'use strict'; + const secretbox = require('../util/Secretbox'); const EventEmitter = require('events'); diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index 1174f70b..605d9927 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); const prism = require('prism-media'); const PacketHandler = require('./PacketHandler'); diff --git a/src/client/voice/util/DispatcherSet.js b/src/client/voice/util/DispatcherSet.js index a1ab7e94..38a7c8b6 100644 --- a/src/client/voice/util/DispatcherSet.js +++ b/src/client/voice/util/DispatcherSet.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); /** diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 58d31141..ac66eb29 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -1,3 +1,5 @@ +'use strict'; + const { Readable } = require('stream'); const prism = require('prism-media'); const { Error } = require('../../../errors'); diff --git a/src/client/voice/util/Secretbox.js b/src/client/voice/util/Secretbox.js index fe1eebe8..1b30eeb6 100644 --- a/src/client/voice/util/Secretbox.js +++ b/src/client/voice/util/Secretbox.js @@ -1,3 +1,5 @@ +'use strict'; + const libs = { sodium: sodium => ({ open: sodium.api.crypto_secretbox_open_easy, diff --git a/src/client/voice/util/Silence.js b/src/client/voice/util/Silence.js index b9643da1..2068d763 100644 --- a/src/client/voice/util/Silence.js +++ b/src/client/voice/util/Silence.js @@ -1,3 +1,5 @@ +'use strict'; + const { Readable } = require('stream'); const SILENCE_FRAME = Buffer.from([0xF8, 0xFF, 0xFE]); diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index 95645f91..a631e6ca 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); /** diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 891da3f4..fd749968 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../../util/Collection'); const WebSocketShard = require('./WebSocketShard'); const { Events, Status, WSEvents } = require('../../util/Constants'); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 7aec37e2..6237b051 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); diff --git a/src/client/websocket/handlers/CHANNEL_CREATE.js b/src/client/websocket/handlers/CHANNEL_CREATE.js index 3074254f..d6d560d8 100644 --- a/src/client/websocket/handlers/CHANNEL_CREATE.js +++ b/src/client/websocket/handlers/CHANNEL_CREATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.ChannelCreate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/CHANNEL_DELETE.js b/src/client/websocket/handlers/CHANNEL_DELETE.js index 158ccb35..cb9f3d8c 100644 --- a/src/client/websocket/handlers/CHANNEL_DELETE.js +++ b/src/client/websocket/handlers/CHANNEL_DELETE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.ChannelDelete.handle(packet.d); }; diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index 1272dbbb..dfc854e3 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js index 46d9037a..b437ebbd 100644 --- a/src/client/websocket/handlers/CHANNEL_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js index 00772c8c..4fa89edc 100644 --- a/src/client/websocket/handlers/GUILD_BAN_ADD.js +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { diff --git a/src/client/websocket/handlers/GUILD_BAN_REMOVE.js b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js index 08483a83..8389e46e 100644 --- a/src/client/websocket/handlers/GUILD_BAN_REMOVE.js +++ b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildBanRemove.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index 05250ed6..9f13c658 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events, Status } = require('../../../util/Constants'); module.exports = async (client, { d: data }, shard) => { diff --git a/src/client/websocket/handlers/GUILD_DELETE.js b/src/client/websocket/handlers/GUILD_DELETE.js index 19d1b3b0..27a32562 100644 --- a/src/client/websocket/handlers/GUILD_DELETE.js +++ b/src/client/websocket/handlers/GUILD_DELETE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildDelete.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js index 5fa5a9d5..e23b6713 100644 --- a/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildEmojisUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js index 6c1a0cfd..e90a72c2 100644 --- a/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildIntegrationsUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js index 7178264d..0738eaa6 100644 --- a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); const Collection = require('../../../util/Collection'); diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js index 367058a5..796b6c37 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events, Status } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { diff --git a/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js index b00da0e6..72432af1 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet, shard) => { client.actions.GuildMemberRemove.handle(packet.d, shard); }; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js index be4f573a..9341329a 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Status, Events } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { diff --git a/src/client/websocket/handlers/GUILD_ROLE_CREATE.js b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js index b6ea8038..da9e7bc4 100644 --- a/src/client/websocket/handlers/GUILD_ROLE_CREATE.js +++ b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildRoleCreate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_ROLE_DELETE.js b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js index d1093cb2..cdc63531 100644 --- a/src/client/websocket/handlers/GUILD_ROLE_DELETE.js +++ b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildRoleDelete.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js index c1f526c5..3a9b62e8 100644 --- a/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildRoleUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_SYNC.js b/src/client/websocket/handlers/GUILD_SYNC.js index f27da424..b7e7d1b2 100644 --- a/src/client/websocket/handlers/GUILD_SYNC.js +++ b/src/client/websocket/handlers/GUILD_SYNC.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildSync.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_UPDATE.js b/src/client/websocket/handlers/GUILD_UPDATE.js index 0f3e24f7..fd0012ad 100644 --- a/src/client/websocket/handlers/GUILD_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_CREATE.js b/src/client/websocket/handlers/MESSAGE_CREATE.js index bc9303fd..c9b79a8f 100644 --- a/src/client/websocket/handlers/MESSAGE_CREATE.js +++ b/src/client/websocket/handlers/MESSAGE_CREATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageCreate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE.js b/src/client/websocket/handlers/MESSAGE_DELETE.js index 09062196..85ae2bc7 100644 --- a/src/client/websocket/handlers/MESSAGE_DELETE.js +++ b/src/client/websocket/handlers/MESSAGE_DELETE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageDelete.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js index a927b3b1..fbcf80fd 100644 --- a/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js +++ b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageDeleteBulk.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js index e2ac2c07..6d0bbb05 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js index 8b9f22a1..2980e695 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageReactionRemove.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js index 2323cfe0..ead80f75 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageReactionRemoveAll.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_UPDATE.js b/src/client/websocket/handlers/MESSAGE_UPDATE.js index 9be750c7..7428e90c 100644 --- a/src/client/websocket/handlers/MESSAGE_UPDATE.js +++ b/src/client/websocket/handlers/MESSAGE_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { diff --git a/src/client/websocket/handlers/PRESENCE_UPDATE.js b/src/client/websocket/handlers/PRESENCE_UPDATE.js index 89b9f0e2..bde36297 100644 --- a/src/client/websocket/handlers/PRESENCE_UPDATE.js +++ b/src/client/websocket/handlers/PRESENCE_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.PresenceUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index f22968d3..74b1e1b9 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -1,3 +1,5 @@ +'use strict'; + let ClientUser; module.exports = (client, { d: data }, shard) => { diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js index 6cc355e9..bd00ec88 100644 --- a/src/client/websocket/handlers/RESUMED.js +++ b/src/client/websocket/handlers/RESUMED.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, packet, shard) => { diff --git a/src/client/websocket/handlers/TYPING_START.js b/src/client/websocket/handlers/TYPING_START.js index ac01d30d..9df76dc7 100644 --- a/src/client/websocket/handlers/TYPING_START.js +++ b/src/client/websocket/handlers/TYPING_START.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { diff --git a/src/client/websocket/handlers/USER_UPDATE.js b/src/client/websocket/handlers/USER_UPDATE.js index 3c5b859c..a02bf588 100644 --- a/src/client/websocket/handlers/USER_UPDATE.js +++ b/src/client/websocket/handlers/USER_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.UserUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js index c8ac3883..2663d99a 100644 --- a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.emit('self.voiceServer', packet.d); }; diff --git a/src/client/websocket/handlers/VOICE_STATE_UPDATE.js b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js index a9527ada..dbff6ea2 100644 --- a/src/client/websocket/handlers/VOICE_STATE_UPDATE.js +++ b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.VoiceStateUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/WEBHOOKS_UPDATE.js b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js index b1afb91c..46cacee0 100644 --- a/src/client/websocket/handlers/WEBHOOKS_UPDATE.js +++ b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.WebhooksUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js index 77bcf8fd..b253cefe 100644 --- a/src/client/websocket/handlers/index.js +++ b/src/client/websocket/handlers/index.js @@ -1,3 +1,5 @@ +'use strict'; + const { WSEvents } = require('../../../util/Constants'); const handlers = {}; diff --git a/src/errors/DJSError.js b/src/errors/DJSError.js index 1d5aea12..157ca660 100644 --- a/src/errors/DJSError.js +++ b/src/errors/DJSError.js @@ -1,3 +1,5 @@ +'use strict'; + // Heavily inspired by node's `internal/errors` module const kCode = Symbol('code'); diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 5f237ac5..792c2b8b 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -1,3 +1,5 @@ +'use strict'; + const { register } = require('./DJSError'); const Messages = { diff --git a/src/errors/index.js b/src/errors/index.js index 39b7582d..c94ddc78 100644 --- a/src/errors/index.js +++ b/src/errors/index.js @@ -1,2 +1,4 @@ +'use strict'; + module.exports = require('./DJSError'); module.exports.Messages = require('./Messages'); diff --git a/src/index.js b/src/index.js index a124108f..1747f68c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('./util/Util'); module.exports = { diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index ec8375dd..7b0a73e4 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -1,3 +1,5 @@ +'use strict'; + const querystring = require('querystring'); const FormData = require('form-data'); const https = require('https'); diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 06ebeb3f..715b1c7c 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -1,3 +1,5 @@ +'use strict'; + const noop = () => {}; // eslint-disable-line no-empty-function const methods = ['get', 'post', 'delete', 'patch', 'put']; const reflectors = [ diff --git a/src/rest/DiscordAPIError.js b/src/rest/DiscordAPIError.js index c90ee4f5..559194f2 100644 --- a/src/rest/DiscordAPIError.js +++ b/src/rest/DiscordAPIError.js @@ -1,3 +1,5 @@ +'use strict'; + /** * Represents an error from the Discord API. * @extends Error diff --git a/src/rest/HTTPError.js b/src/rest/HTTPError.js index 32b3e6bd..2911467f 100644 --- a/src/rest/HTTPError.js +++ b/src/rest/HTTPError.js @@ -1,3 +1,5 @@ +'use strict'; + /** * Represents a HTTP error from a request. * @extends Error diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 53a8047f..306fa246 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -1,3 +1,5 @@ +'use strict'; + const RequestHandler = require('./RequestHandler'); const APIRequest = require('./APIRequest'); const routeBuilder = require('./APIRouter'); diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 3f66fd43..44fca4fb 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -1,3 +1,5 @@ +'use strict'; + const DiscordAPIError = require('./DiscordAPIError'); const HTTPError = require('./HTTPError'); const Util = require('../util/Util'); diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 3b25c1d0..adc5caeb 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); const path = require('path'); const Util = require('../util/Util'); diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 201568ca..026c97ef 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); const { Events } = require('../util/Constants'); diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index b7196229..aca3ebed 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -1,3 +1,5 @@ +'use strict'; + const path = require('path'); const fs = require('fs'); const EventEmitter = require('events'); diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 6e2e4081..8d491c83 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const Channel = require('../structures/Channel'); const { Events } = require('../util/Constants'); diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 3708893c..fc841899 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); let Structures; diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index e67bcc1c..e538889d 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Channel = require('../structures/Channel'); const { ChannelTypes } = require('../util/Constants'); const DataStore = require('./DataStore'); diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js index e2fe5ecd..8c097f56 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/stores/GuildEmojiRoleStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const Util = require('../util/Util'); const { TypeError } = require('../errors'); diff --git a/src/stores/GuildEmojiStore.js b/src/stores/GuildEmojiStore.js index 925f79ab..b992a68c 100644 --- a/src/stores/GuildEmojiStore.js +++ b/src/stores/GuildEmojiStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const DataStore = require('./DataStore'); const GuildEmoji = require('../structures/GuildEmoji'); diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 205de123..8789df21 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const Util = require('../util/Util'); const { TypeError } = require('../errors'); diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 6f95aec9..039fb4c7 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const GuildMember = require('../structures/GuildMember'); const { Events, OPCodes } = require('../util/Constants'); diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index 0cf9a115..90e4358d 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const DataResolver = require('../util/DataResolver'); const { Events } = require('../util/Constants'); diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index 2d704979..1a8a7083 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const Collection = require('../util/Collection'); const Message = require('../structures/Message'); diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js index 15fc9f12..061d3f1e 100644 --- a/src/stores/PresenceStore.js +++ b/src/stores/PresenceStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const { Presence } = require('../structures/Presence'); diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index 38c467b7..b3910ba4 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const MessageReaction = require('../structures/MessageReaction'); diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js index 1b027579..976b7991 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/stores/ReactionUserStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const DataStore = require('./DataStore'); const { Error } = require('../errors'); diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 3c3a5b35..14956c30 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const Role = require('../structures/Role'); const { resolveColor } = require('../util/Util'); diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index d700877c..8fd2d9d5 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const User = require('../structures/User'); const GuildMember = require('../structures/GuildMember'); diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js index 2de49249..ece3c2bb 100644 --- a/src/stores/VoiceStateStore.js +++ b/src/stores/VoiceStateStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const VoiceState = require('../structures/VoiceState'); diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 8b714fcd..deca5d26 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -1,3 +1,5 @@ +'use strict'; + const DataResolver = require('../util/DataResolver'); const MessageEmbed = require('./MessageEmbed'); const MessageAttachment = require('./MessageAttachment'); diff --git a/src/structures/Base.js b/src/structures/Base.js index 37633757..4850a39d 100644 --- a/src/structures/Base.js +++ b/src/structures/Base.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); /** diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index 5766a4a7..60be408c 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildChannel = require('./GuildChannel'); /** diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 8466d19a..92914118 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -1,3 +1,5 @@ +'use strict'; + const Snowflake = require('../util/Snowflake'); const Base = require('./Base'); const { ChannelTypes } = require('../util/Constants'); diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 7ea36020..d2bcd17a 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -1,3 +1,5 @@ +'use strict'; + const Snowflake = require('../util/Snowflake'); const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants'); const Base = require('./Base'); diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 68dda64c..a92dfb30 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -1,3 +1,5 @@ +'use strict'; + const { Presence } = require('./Presence'); const Collection = require('../util/Collection'); const { ActivityTypes, OPCodes } = require('../util/Constants'); diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 1b8d90d9..79d7ad0e 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -1,3 +1,5 @@ +'use strict'; + const Structures = require('../util/Structures'); const DataResolver = require('../util/DataResolver'); diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 1f45f490..5549618a 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const Channel = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const MessageStore = require('../stores/MessageStore'); diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index ed844f18..803fc500 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -1,3 +1,5 @@ +'use strict'; + const Snowflake = require('../util/Snowflake'); const Base = require('./Base'); diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index b2fe7200..6f9aca39 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const Channel = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Collection = require('../util/Collection'); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a194ea15..aef0b311 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1,3 +1,5 @@ +'use strict'; + const Invite = require('./Invite'); const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index a5212d5b..f78078a7 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const Snowflake = require('../util/Snowflake'); const Webhook = require('./Webhook'); diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index daf20a52..dc0a4e41 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const Channel = require('./Channel'); const Role = require('./Role'); const Invite = require('./Invite'); diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 93e8d275..24fd7c02 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildEmojiRoleStore = require('../stores/GuildEmojiRoleStore'); const Permissions = require('../util/Permissions'); const { Error } = require('../errors'); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index a2e82d79..d91071c9 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -1,3 +1,5 @@ +'use strict'; + const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Role = require('./Role'); const Permissions = require('../util/Permissions'); diff --git a/src/structures/Integration.js b/src/structures/Integration.js index 2782008b..5ff760dc 100644 --- a/src/structures/Integration.js +++ b/src/structures/Integration.js @@ -1,3 +1,5 @@ +'use strict'; + const Base = require('./Base'); /** diff --git a/src/structures/Invite.js b/src/structures/Invite.js index b142d696..fec3f2d7 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -1,3 +1,5 @@ +'use strict'; + const { Endpoints } = require('../util/Constants'); const Base = require('./Base'); diff --git a/src/structures/Message.js b/src/structures/Message.js index 88a1707a..1947705e 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -1,3 +1,5 @@ +'use strict'; + const Mentions = require('./MessageMentions'); const MessageAttachment = require('./MessageAttachment'); const Embed = require('./MessageEmbed'); diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index cce6e487..a4c05303 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); /** diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 3d9fcf72..59120b9d 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -1,3 +1,5 @@ +'use strict'; + const Collector = require('./interfaces/Collector'); const { Events } = require('../util/Constants'); diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 49ddf30a..213a27a1 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); const { RangeError } = require('../errors'); diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index bd99397d..b4414a3b 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const Util = require('../util/Util'); const GuildMember = require('./GuildMember'); diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index faf3bfbf..69138fda 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildEmoji = require('./GuildEmoji'); const Util = require('../util/Util'); const ReactionEmoji = require('./ReactionEmoji'); diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index b2851786..cf11eaa9 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -1,3 +1,5 @@ +'use strict'; + const Role = require('./Role'); const Permissions = require('../util/Permissions'); const Util = require('../util/Util'); diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 33998134..122b461d 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); const ActivityFlags = require('../util/ActivityFlags'); const { ActivityTypes } = require('../util/Constants'); diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index 5c4532e2..b4826100 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -1,3 +1,5 @@ +'use strict'; + const Collector = require('./interfaces/Collector'); const Collection = require('../util/Collection'); const { Events } = require('../util/Constants'); diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js index c6ea8d68..2661fa91 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); const Emoji = require('./Emoji'); diff --git a/src/structures/Role.js b/src/structures/Role.js index 18f68f2f..cbae5696 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -1,3 +1,5 @@ +'use strict'; + const Snowflake = require('../util/Snowflake'); const Permissions = require('../util/Permissions'); const Util = require('../util/Util'); diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 6e053e5d..47c8e19c 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildChannel = require('./GuildChannel'); const Webhook = require('./Webhook'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); diff --git a/src/structures/User.js b/src/structures/User.js index 67366323..22ea5c81 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -1,3 +1,5 @@ +'use strict'; + const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { Presence } = require('./Presence'); const Snowflake = require('../util/Snowflake'); diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index aee01b6f..cf9b1ee9 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildChannel = require('./GuildChannel'); const { browser } = require('../util/Constants'); const Permissions = require('../util/Permissions'); diff --git a/src/structures/VoiceRegion.js b/src/structures/VoiceRegion.js index b4db2177..9626195d 100644 --- a/src/structures/VoiceRegion.js +++ b/src/structures/VoiceRegion.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); /** diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 88e0ed19..82f9eec7 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -1,3 +1,5 @@ +'use strict'; + const Base = require('./Base'); /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index c12cc67f..7b851c77 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,3 +1,5 @@ +'use strict'; + const DataResolver = require('../util/DataResolver'); const Channel = require('./Channel'); const APIMessage = require('./APIMessage'); diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index c490ebb3..1dbd6142 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../../util/Collection'); const Util = require('../../util/Util'); const EventEmitter = require('events'); diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index f9c668de..075bc675 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const MessageCollector = require('../MessageCollector'); const Snowflake = require('../../util/Snowflake'); const Collection = require('../../util/Collection'); diff --git a/src/util/ActivityFlags.js b/src/util/ActivityFlags.js index 49fd7a67..f6f5aac9 100644 --- a/src/util/ActivityFlags.js +++ b/src/util/ActivityFlags.js @@ -1,3 +1,5 @@ +'use strict'; + const BitField = require('./BitField'); /** diff --git a/src/util/BitField.js b/src/util/BitField.js index fa506a2e..96b07b82 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -1,3 +1,5 @@ +'use strict'; + const { RangeError } = require('../errors'); /** diff --git a/src/util/Collection.js b/src/util/Collection.js index 7f0570d6..3d01ffef 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('./Util'); /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 01ff57d6..aac3f5b3 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1,3 +1,5 @@ +'use strict'; + const Package = exports.Package = require('../../package.json'); const { Error, RangeError } = require('../errors'); const browser = exports.browser = typeof window !== 'undefined'; diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index 5fcfd0d4..1b905727 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -1,3 +1,5 @@ +'use strict'; + const path = require('path'); const fs = require('fs'); const fetch = require('node-fetch'); diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 295346b1..9f01df39 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -1,3 +1,5 @@ +'use strict'; + const BitField = require('./BitField'); /** diff --git a/src/util/Snowflake.js b/src/util/Snowflake.js index 00f22d06..06612c30 100644 --- a/src/util/Snowflake.js +++ b/src/util/Snowflake.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); // Discord epoch (2015-01-01T00:00:00.000Z) diff --git a/src/util/Speaking.js b/src/util/Speaking.js index e0e557ea..c706d995 100644 --- a/src/util/Speaking.js +++ b/src/util/Speaking.js @@ -1,3 +1,5 @@ +'use strict'; + const BitField = require('./BitField'); /** diff --git a/src/util/Structures.js b/src/util/Structures.js index 8b1edd8b..5bdf5c86 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -1,3 +1,5 @@ +'use strict'; + /** * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}. */ diff --git a/src/util/Util.js b/src/util/Util.js index 562b81a4..1d7b7aee 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -1,3 +1,5 @@ +'use strict'; + const { Colors, DefaultOptions, Endpoints } = require('./Constants'); const fetch = require('node-fetch'); const { Error: DiscordError, RangeError, TypeError } = require('../errors'); diff --git a/webpack.config.js b/webpack.config.js index c7416852..c95745f1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,5 @@ +'use strict'; + const path = require('path'); const webpack = require('webpack'); const TerserJSPlugin = require('terser-webpack-plugin'); From b5d5c699e65bf14a77065a75aabd24e4972988a9 Mon Sep 17 00:00:00 2001 From: August Date: Sun, 9 Dec 2018 01:30:46 -0700 Subject: [PATCH 024/428] fix: guildBanRemove event name (#2983) - "Events.GUILD_BAN_REMOVEGUILD_BAN_REMOVE" -> "Events.GUILD_BAN_REMOVE" --- src/client/actions/GuildBanRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 62779137..5a4c0a90 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -14,7 +14,7 @@ class GuildBanRemove extends Action { * @param {Guild} guild The guild that the unban occurred in * @param {User} user The user that was unbanned */ - if (guild && user) client.emit(Events.GUILD_BAN_REMOVEGUILD_BAN_REMOVE, guild, user); + if (guild && user) client.emit(Events.GUILD_BAN_REMOVE, guild, user); } } From 5cbdf380289f0fd1d16330325dab71f8815396fc Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Fri, 21 Dec 2018 23:49:56 -0800 Subject: [PATCH 025/428] fix(WebSocketShard): add websocket send error handling (#2981) * websocket send error handling * fix: emit only when error is present * refactor: use an if instead --- src/client/websocket/WebSocketShard.js | 4 +++- test/tester1000.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 6237b051..ea4595b4 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -437,7 +437,9 @@ class WebSocketShard extends EventEmitter { this.debug(`Tried to send packet ${data} but no WebSocket is available!`); return; } - this.ws.send(WebSocket.pack(data)); + this.ws.send(WebSocket.pack(data), err => { + if (err) this.manager.client.emit(Events.ERROR, err); + }); } /** diff --git a/test/tester1000.js b/test/tester1000.js index d726188c..f9d7f668 100644 --- a/test/tester1000.js +++ b/test/tester1000.js @@ -13,6 +13,7 @@ client.on('ready', () => { log('READY', client.user.tag, client.user.id); }); client.on('rateLimit', log); +client.on('error', console.error); const commands = { eval: message => { From 8286d1a0fc3e86a10b7d5f977836b0e0fe611c27 Mon Sep 17 00:00:00 2001 From: Lucas Kellar Date: Sun, 23 Dec 2018 16:16:28 -0600 Subject: [PATCH 026/428] typings(SnowflakeUtil): add optional "timestamp" parameter to generate (#2998) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index ea8d55e7..59e7bf51 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -991,7 +991,7 @@ declare module 'discord.js' { export class SnowflakeUtil { public static deconstruct(snowflake: Snowflake): DeconstructedSnowflake; - public static generate(): Snowflake; + public static generate(timestamp?: number | Date): Snowflake; } const VolumeMixin: (base: Constructable) => Constructable; From 8a76cc5c725ffcfe8131c652de3d16be1f51439a Mon Sep 17 00:00:00 2001 From: Kyra Date: Tue, 25 Dec 2018 22:00:46 +0100 Subject: [PATCH 027/428] typings(TextBasedChannel): add `Snowflake[]` overload to bulkDelete (#3001) * typings: Add `string[]` overload to bulkDelete * misc: Requested changes --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 59e7bf51..a97225b7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1441,7 +1441,7 @@ declare module 'discord.js' { typing: boolean; typingCount: number; awaitMessages(filter: CollectorFilter, options?: AwaitMessagesOptions): Promise>; - bulkDelete(messages: Collection | Message[] | number, filterOld?: boolean): Promise>; + bulkDelete(messages: Collection | Message[] | Snowflake[] | number, filterOld?: boolean): Promise>; createMessageCollector(filter: CollectorFilter, options?: MessageCollectorOptions): MessageCollector; startTyping(count?: number): Promise; stopTyping(force?: boolean): void; From 0bde9ca2c58785f2e1093698268aa9bdc525f5a5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 27 Dec 2018 18:05:54 +0000 Subject: [PATCH 028/428] voice: set seek parameter before input (#2993) --- src/client/voice/player/BasePlayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index c9c6cf0d..763762ef 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -48,7 +48,7 @@ class BasePlayer extends EventEmitter { const isStream = input instanceof ReadableStream; const args = isStream ? FFMPEG_ARGUMENTS.slice() : ['-i', input, ...FFMPEG_ARGUMENTS]; - if (options.seek) args.push('-ss', String(options.seek)); + if (options.seek) args.unshift('-ss', String(options.seek)); const ffmpeg = new prism.FFmpeg({ args }); const streams = { ffmpeg }; From 840d22f8920f7648f97109d3df6f8a9b1da9f20f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 30 Dec 2018 12:52:33 +0100 Subject: [PATCH 029/428] docs(Webhook): add mising '@name' to Webhook#token's docstring --- src/structures/Webhook.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 7b851c77..472de07e 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -28,6 +28,7 @@ class Webhook { /** * The token for the webhook + * @name Webhook#token * @type {string} */ Object.defineProperty(this, 'token', { value: data.token, writable: true, configurable: true }); From 28db5273708af039ef6a5387273d4407d55f178c Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 10 Jan 2019 16:54:02 +0100 Subject: [PATCH 030/428] docs(Guild): use 'updated' variable in example for setRegion --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index aef0b311..66cf9871 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -725,7 +725,7 @@ class Guild extends Base { * @example * // Edit the guild region * guild.setRegion('london') - * .then(updated => console.log(`Updated guild region to ${guild.region}`)) + * .then(updated => console.log(`Updated guild region to ${updated.region}`)) * .catch(console.error); */ setRegion(region, reason) { From 8230255c68b94d68a4e8ffc559a98d08d1a08a7c Mon Sep 17 00:00:00 2001 From: Isabella Date: Tue, 15 Jan 2019 01:45:29 -0600 Subject: [PATCH 031/428] fix(ShardClientUtil#id): erroneously reporting as an array --- src/client/Client.js | 4 ++-- src/util/Constants.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 1bc8232b..4e5f5e24 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -55,7 +55,7 @@ class Client extends BaseClient { this.options.totalShardCount = this.options.shardCount; } } - if (!this.options.shards && this.options.shardCount) { + if (typeof this.options.shards === 'undefined' && this.options.shardCount) { this.options.shards = []; for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); } @@ -233,7 +233,7 @@ class Client extends BaseClient { this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); this.options.shardCount = res.shards; this.options.totalShardCount = res.shards; - if (!this.options.shards || !this.options.shards.length) { + if (typeof this.options.shards === 'undefined' || !this.options.shards.length) { this.options.shards = []; for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); } diff --git a/src/util/Constants.js b/src/util/Constants.js index aac3f5b3..508b5f97 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -7,7 +7,7 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number|number[]} [shards=0] ID of the shard to run, or an array of shard IDs + * @property {number|number[]} [shards] ID of the shard to run, or an array of shard IDs * @property {number} [shardCount=1] Total number of shards that will be spawned by this Client * @property {number} [totalShardCount=1] The total amount of shards used by all processes of this bot * (e.g. recommended shard count, shard count of the ShardingManager) @@ -37,7 +37,6 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {HTTPOptions} [http] HTTP options */ exports.DefaultOptions = { - shards: 0, shardCount: 1, totalShardCount: 1, messageCacheMaxSize: 200, From 3dff5058f0beae01cec0abe27e3a43c8fd7c60ce Mon Sep 17 00:00:00 2001 From: Anthony Collier Date: Mon, 21 Jan 2019 11:22:24 -0500 Subject: [PATCH 032/428] docs(Examples): fix usage of removed overload of Collection#find (#3027) --- docs/examples/greeting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/greeting.js b/docs/examples/greeting.js index 14288922..8fc1dfad 100644 --- a/docs/examples/greeting.js +++ b/docs/examples/greeting.js @@ -19,7 +19,7 @@ client.on('ready', () => { // Create an event listener for new guild members client.on('guildMemberAdd', member => { // Send the message to a designated channel on a server: - const channel = member.guild.channels.find('name', 'member-log'); + const channel = member.guild.channels.find(ch => ch.name === 'member-log'); // Do nothing if the channel wasn't found on this server if (!channel) return; // Send the message, mentioning the member From 2dcdc798ace9bace7fb6800a245f0f3802d0477d Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 2 Feb 2019 10:42:13 +0100 Subject: [PATCH 033/428] typings: add missing ImageSize numbers (#3045) To match the JS typedef: https://discord.js.org/#/docs/main/master/typedef/ImageURLOptions --- typings/index.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index a97225b7..d6b95ba6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1846,7 +1846,10 @@ declare module 'discord.js' { | 'jpg' | 'gif'; - type ImageSize = 128 + type ImageSize = 16 + | 32 + | 64 + | 128 | 256 | 512 | 1024 From f2ed93c08aba970a94671a0fec819035ad989195 Mon Sep 17 00:00:00 2001 From: Marcel Menzel Date: Sat, 2 Feb 2019 11:28:45 +0100 Subject: [PATCH 034/428] fix(WebSocketShard): report correct resumed event count (#3019) This PR attempts to fix the reported resumed event count in the debug output (where it is always displayed only as 1 event replayed) and in the emitted `resumed` event, where it passed the current sequence instead of passing the actual replayed event count (which was an utopic high number for smaller bots on resume). --- src/client/websocket/WebSocketShard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index ea4595b4..1cf0ef1c 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -49,7 +49,7 @@ class WebSocketShard extends EventEmitter { * @type {number} * @private */ - this.closeSequence = 0; + this.closeSequence = oldShard ? oldShard.closeSequence : 0; /** * The current session id of the WebSocket @@ -223,7 +223,7 @@ class WebSocketShard extends EventEmitter { case WSEvents.RESUMED: { this.trace = packet.d._trace; this.status = Status.READY; - const replayed = packet.s - this.sequence; + const replayed = packet.s - this.closeSequence; this.debug(`RESUMED ${this.trace.join(' -> ')} | replayed ${replayed} events.`); this.heartbeat(); break; From db3ae0159bf2d8a44e7ecda4b9835a08b9210e8b Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 2 Feb 2019 20:28:24 +0100 Subject: [PATCH 035/428] docs(Structures): note about extending prior to instantiating client (#2884) --- src/util/Structures.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/Structures.js b/src/util/Structures.js index 5bdf5c86..a742e292 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -20,6 +20,8 @@ class Structures { /** * Extends a structure. + * Make sure to extend all structures before instantiating your client. + * Extending after doing so may not work as expected. * @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 From 1db78994dde894e796934758c7701e7bb160af00 Mon Sep 17 00:00:00 2001 From: "Rattmann (fallen)" <8607699+PlayTheFallen@users.noreply.github.com> Date: Sat, 2 Feb 2019 19:29:10 +0000 Subject: [PATCH 036/428] feat: MessageEmbed#length (#3003) * add MessageEmbed#length * update typings (+MessageEmbed#length) * eslint: L181 (max line length), L183 (missing semi) * eslint: L181 (trailing space) --- src/structures/MessageEmbed.js | 14 ++++++++++++++ typings/index.d.ts | 1 + 2 files changed, 15 insertions(+) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 213a27a1..0c925d27 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -169,6 +169,20 @@ class MessageEmbed { return this.color ? `#${this.color.toString(16).padStart(6, '0')}` : null; } + /** + * The accumulated length for the embed title, description, fields and footer text + * @type {number} + * @readonly + */ + get length() { + return ( + (this.title ? this.title.length : 0) + + (this.description ? this.description.length : 0) + + (this.fields.length >= 1 ? this.fields.reduce((prev, curr) => + prev + curr.name.length + curr.value.length, 0) : 0) + + (this.footer ? this.footer.text.length : 0)); + } + /** * Adds a field to the embed (max 25). * @param {StringResolvable} name The name of the field diff --git a/typings/index.d.ts b/typings/index.d.ts index d6b95ba6..b167be1f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -729,6 +729,7 @@ declare module 'discord.js' { public footer: { text?: string; iconURL?: string; proxyIconURL?: string }; public readonly hexColor: string; public image: { url: string; proxyURL?: string; height?: number; width?: number; }; + public readonly length: number; public provider: { name: string; url: string; }; public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; }; public timestamp: number; From 75e264da5744b7258d92865856f5f238947c1f47 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sat, 2 Feb 2019 13:29:47 -0600 Subject: [PATCH 037/428] feat: Presence#clientStatus (#2997) * feat: Presence#clientStatus * fix Presence#equals check * fix typings * vlad changes * presence consistency docs * fix docs * fix big docs fail --- src/structures/ClientUser.js | 6 ++--- src/structures/Presence.js | 45 ++++++++++++++++++++++++++++-------- typings/index.d.ts | 19 +++++++++++---- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 79d7ad0e..3ec32e9d 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -80,7 +80,7 @@ class ClientUser extends Structures.get('User') { /** * Data resembling a raw Discord presence. * @typedef {Object} PresenceData - * @property {PresenceStatus} [status] Status of the user + * @property {PresenceStatusData} [status] Status of the user * @property {boolean} [afk] Whether the user is AFK * @property {Object} [activity] Activity the user is playing * @property {Object|string} [activity.application] An application object or application id @@ -111,12 +111,12 @@ class ClientUser extends Structures.get('User') { * * `idle` * * `invisible` * * `dnd` (do not disturb) - * @typedef {string} PresenceStatus + * @typedef {string} PresenceStatusData */ /** * Sets the status of the client user. - * @param {PresenceStatus} status Status to change to + * @param {PresenceStatusData} status Status to change to * @param {?number|number[]} [shardID] Shard ID(s) to have the activity set on * @returns {Promise} * @example diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 122b461d..baacab13 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -11,14 +11,34 @@ const { ActivityTypes } = require('../util/Constants'); * @property {number} [type] Type of activity sent */ +/** + * The status of this presence: + * + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`offline`** - user is offline or invisible + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} PresenceStatus + */ + /** * Represents a user's presence. */ class Presence { constructor(client, data = {}) { Object.defineProperty(this, 'client', { value: client }); + /** + * The user ID of this presence + * @type {Snowflake} + */ this.userID = data.user.id; + + /** + * The guild of this presence + * @type {?Guild} + */ this.guild = data.guild; + this.patch(data); } @@ -40,23 +60,27 @@ class Presence { patch(data) { /** - * The status of the presence: - * - * * **`online`** - user is online - * * **`offline`** - user is offline or invisible - * * **`idle`** - user is AFK - * * **`dnd`** - user is in Do Not Disturb - * @type {string} + * The status of this presence + * @type {PresenceStatus} */ this.status = data.status || this.status || 'offline'; const activity = data.game || data.activity; /** - * The activity of the presence + * The activity of this presence * @type {?Activity} */ this.activity = activity ? new Activity(this, activity) : null; + /** + * The devices this presence is on + * @type {?object} + * @property {PresenceStatus} web + * @property {PresenceStatus} mobile + * @property {PresenceStatus} desktop + */ + this.clientStatus = data.client_status || null; + return this; } @@ -75,7 +99,10 @@ class Presence { return this === presence || ( presence && this.status === presence.status && - this.activity ? this.activity.equals(presence.activity) : !presence.activity + this.activity ? this.activity.equals(presence.activity) : !presence.activity && + this.clientStatus.web === presence.clientStatus.web && + this.clientStatus.mobile === presence.clientStatus.mobile && + this.clientStatus.desktop === presence.clientStatus.desktop ); } diff --git a/typings/index.d.ts b/typings/index.d.ts index b167be1f..4910e354 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -270,7 +270,7 @@ declare module 'discord.js' { public setAFK(afk: boolean): Promise; public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; public setPresence(data: PresenceData): Promise; - public setStatus(status: PresenceStatus, shardID?: number | number[]): Promise; + public setStatus(status: PresenceStatusData, shardID?: number | number[]): Promise; public setUsername(username: string): Promise; } @@ -820,7 +820,8 @@ declare module 'discord.js' { constructor(client: Client, data?: object); public activity: Activity; public flags: Readonly; - public status: 'online' | 'offline' | 'idle' | 'dnd'; + public status: PresenceStatus; + public clientStatus: ClientPresenceStatusData; public readonly user: User; public readonly member?: GuildMember; public equals(presence: Presence): boolean; @@ -1994,7 +1995,7 @@ declare module 'discord.js' { }; type PresenceData = { - status?: PresenceStatus; + status?: PresenceStatusData; afk?: boolean; activity?: { name?: string; @@ -2006,7 +2007,17 @@ declare module 'discord.js' { type PresenceResolvable = Presence | UserResolvable | Snowflake; - type PresenceStatus = 'online' | 'idle' | 'invisible' | 'dnd'; + type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; + + type ClientPresenceStatusData = { + web?: ClientPresenceStatus, + mobile?: ClientPresenceStatus, + desktop?: ClientPresenceStatus + }; + + type PresenceStatus = ClientPresenceStatus | 'offline'; + + type PresenceStatusData = ClientPresenceStatus | 'invisible'; type RateLimitData = { timeout: number; From dd8ba00af4e4e4f64526cf476467fb0ded0257ff Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 4 Feb 2019 16:44:21 +0100 Subject: [PATCH 038/428] misc(index): export HTTPError class (#3051) --- src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.js b/src/index.js index 1747f68c..d9b8cb45 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ module.exports = { DataResolver: require('./util/DataResolver'), DataStore: require('./stores/DataStore'), DiscordAPIError: require('./rest/DiscordAPIError'), + HTTPError: require('./rest/HTTPError'), Permissions: require('./util/Permissions'), Speaking: require('./util/Speaking'), Snowflake: require('./util/Snowflake'), From d98d464d748a75e242b6e95a1308235bf4016e1b Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 4 Feb 2019 17:57:13 +0100 Subject: [PATCH 039/428] typings(GuildCreateChannelOptions): added missing properties (#3052) * typings(GuildCreateChannelOptions): Added missing properties * typings: Added `GuildChannelCloneOptions` --- typings/index.d.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4910e354..a2607c18 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -523,7 +523,7 @@ declare module 'discord.js' { public readonly permissionsLocked: boolean; public readonly position: number; public rawPosition: number; - public clone(options?: GuildCreateChannelOptions): Promise; + public clone(options?: GuildChannelCloneOptions): Promise; public createInvite(options?: InviteOptions): Promise; public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; public edit(data: ChannelData, reason?: string): Promise; @@ -1777,15 +1777,20 @@ declare module 'discord.js' { type GuildChannelResolvable = Snowflake | GuildChannel; type GuildCreateChannelOptions = { - type?: 'text' | 'voice' | 'category' + permissionOverwrites?: OverwriteResolvable[] | Collection; + topic?: string; + type?: 'text' | 'voice' | 'category'; nsfw?: boolean; + parent?: ChannelResolvable; bitrate?: number; userLimit?: number; - parent?: ChannelResolvable; - permissionOverwrites?: OverwriteResolvable[] | Collection; rateLimitPerUser?: number; position?: number; - reason?: string + reason?: string; + }; + + type GuildChannelCloneOptions = GuildCreateChannelOptions & { + name?: string; }; type GuildEmojiCreateOptions = { From f826c9c75e976a8dca3fb4c88c130a1e12147a80 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 5 Feb 2019 10:26:16 +0000 Subject: [PATCH 040/428] voice: workaround for receiving audio (#2929 and discordapp/discord-api-docs#808) --- src/client/voice/VoiceConnection.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 078781b5..0242d541 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -10,6 +10,15 @@ const EventEmitter = require('events'); const { Error } = require('../../errors'); const PlayInterface = require('./util/PlayInterface'); const Speaking = require('../../util/Speaking'); +const Silence = require('./util/Silence'); + +// Workaround for Discord now requiring silence to be sent before being able to receive audio +class SingleSilence extends Silence { + _read() { + super._read(); + this.push(null); + } +} const SUPPORTED_MODES = [ 'xsalsa20_poly1305_lite', @@ -419,13 +428,16 @@ class VoiceConnection extends EventEmitter { onSessionDescription(data) { Object.assign(this.authentication, data); this.status = VoiceStatus.CONNECTED; - clearTimeout(this.connectTimeout); - /** - * Emitted once the connection is ready, when a promise to join a voice channel resolves, - * the connection will already be ready. - * @event VoiceConnection#ready - */ - this.emit('ready'); + const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); + dispatcher.on('finish', () => { + clearTimeout(this.connectTimeout); + /** + * Emitted once the connection is ready, when a promise to join a voice channel resolves, + * the connection will already be ready. + * @event VoiceConnection#ready + */ + this.emit('ready'); + }); } /** From ae7269088b99dfff614ea66ba9c67baeb1bd9169 Mon Sep 17 00:00:00 2001 From: Kyra Date: Wed, 6 Feb 2019 18:15:05 +0100 Subject: [PATCH 041/428] typings(ShardClientUtil): fix `id` property type (#3054) Ref: https://github.com/discordjs/discord.js/blob/d98d464d748a75e242b6e95a1308235bf4016e1b/src/sharding/ShardClientUtil.js#L50 --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index a2607c18..56f94107 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -951,7 +951,7 @@ declare module 'discord.js' { public client: Client; public readonly count: number; - public readonly id: number; + public readonly id: number | number[]; public mode: ShardingManagerMode; public parentPort: any; public broadcastEval(script: string): Promise; From c4325911130f30072246dd1b2e381c91293b3e6a Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 9 Feb 2019 16:07:31 +0100 Subject: [PATCH 042/428] feat(RoleStore, ChannelStore): `fetch()` method (#3071) * feat({Role,Channel}Store): fetch method * docs: Add usage examples to the new methods * misc: Add note of why we are fetching all roles even for a single one --- src/stores/ChannelStore.js | 18 ++++++++++++++++++ src/stores/RoleStore.js | 28 ++++++++++++++++++++++++++++ typings/index.d.ts | 3 +++ 3 files changed, 49 insertions(+) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 8d491c83..9ce6465c 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -74,6 +74,24 @@ class ChannelStore extends DataStore { super.remove(id); } + /** + * Obtains a channel from Discord, or the channel cache if it's already available. + * @param {Snowflake} id ID of the channel + * @param {boolean} [cache=true] Whether to cache the new channel object if it isn't already + * @returns {Promise} + * @example + * // Fetch a channel by its id + * client.channels.fetch('222109930545610754') + * .then(channel => console.log(channel.name)) + * .catch(console.error); + */ + fetch(id, cache = true) { + const existing = this.get(id); + if (existing) return Promise.resolve(existing); + + return this.client.api.channels(id).get().then(data => this.add(data, null, cache)); + } + /** * Data that can be resolved to give a Channel object. This can be: * * A Channel object diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 14956c30..6f97d277 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -19,6 +19,34 @@ class RoleStore extends DataStore { return super.add(data, cache, { extras: [this.guild] }); } + /** + * Obtains one or more roles from Discord, or the role cache if they're already available. + * @param {Snowflake} [id] ID or IDs of the role(s) + * @param {boolean} [cache=true] Whether to cache the new roles objects if it weren't already + * @returns {Promise} + * @example + * // Fetch all roles from the guild + * message.guild.roles.fetch() + * .then(roles => console.log(`There are ${roles.size} roles.`)) + * .catch(console.error); + * @example + * // Fetch a single role + * message.guild.roles.fetch('222078108977594368') + * .then(role => console.log(`The role color is: ${role.color}`)) + * .catch(console.error); + */ + async fetch(id, cache = true) { + if (id) { + const existing = this.get(id); + if (existing) return existing; + } + + // We cannot fetch a single role, as of this commit's date, Discord API throws with 405 + const roles = await this.client.api.guilds(this.guild.id).roles.get(); + for (const role of roles) this.add(role, cache); + return id ? this.get(id) || null : this; + } + /** * Data that can be resolved to a Role object. This can be: * * A Role diff --git a/typings/index.d.ts b/typings/index.d.ts index 56f94107..60a19f3d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1320,6 +1320,7 @@ declare module 'discord.js' { export class ChannelStore extends DataStore { constructor(client: Client, iterable: Iterable, options?: { lru: boolean }); constructor(client: Client, options?: { lru: boolean }); + public fetch(id: Snowflake, cache?: boolean): Promise; } export class DataStore, R = any> extends Collection { @@ -1410,6 +1411,8 @@ declare module 'discord.js' { public readonly highest: Role; public create(options?: { data?: RoleData, reason?: string }): Promise; + public fetch(id?: Snowflake, cache?: boolean): Promise; + public fetch(id: Snowflake, cache?: boolean): Promise; } export class UserStore extends DataStore { From 9449f8df8f3c65b350ad7844376651f7fd06ba52 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 9 Feb 2019 18:20:10 +0100 Subject: [PATCH 043/428] cleanup(GuildMember{RoleStore}): Rewrite to async/await (#3072) - Fixed a bug where `GuildMemberRoleStore#{add,remove}` would create a rejected promise inside a promise, instead of rejecting the first one. - Fixed a bug where `GuildMember#edit` with a specified unknown channel would throw synchronously, instead of rejecting asynchronously. --- src/stores/GuildMemberRoleStore.js | 16 ++++++++-------- src/structures/GuildMember.js | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 8789df21..9ab340d6 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -67,8 +67,8 @@ class GuildMemberRoleStore extends Collection { if (roleOrRoles instanceof Collection || roleOrRoles instanceof Array) { roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { - return Promise.reject(new TypeError('INVALID_TYPE', 'roles', - 'Array or Collection of Roles or Snowflakes', true)); + throw new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true); } const newRoles = [...new Set(roleOrRoles.concat(...this.values()))]; @@ -76,8 +76,8 @@ class GuildMemberRoleStore extends Collection { } else { roleOrRoles = this.guild.roles.resolve(roleOrRoles); if (roleOrRoles === null) { - return Promise.reject(new TypeError('INVALID_TYPE', 'roles', - 'Array or Collection of Roles or Snowflakes', true)); + throw new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true); } await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason }); @@ -98,8 +98,8 @@ class GuildMemberRoleStore extends Collection { if (roleOrRoles instanceof Collection || roleOrRoles instanceof Array) { roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { - return Promise.reject(new TypeError('INVALID_TYPE', 'roles', - 'Array or Collection of Roles or Snowflakes', true)); + throw new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true); } const newRoles = this.filter(role => !roleOrRoles.includes(role)); @@ -107,8 +107,8 @@ class GuildMemberRoleStore extends Collection { } else { roleOrRoles = this.guild.roles.resolve(roleOrRoles); if (roleOrRoles === null) { - return Promise.reject(new TypeError('INVALID_TYPE', 'roles', - 'Array or Collection of Roles or Snowflakes', true)); + throw new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true); } await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].delete({ reason }); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index d91071c9..73adb69a 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -248,7 +248,7 @@ class GuildMember extends Base { * @param {string} [reason] Reason for editing this user * @returns {Promise} */ - edit(data, reason) { + async edit(data, reason) { if (data.channel) { data.channel = this.guild.channels.resolve(data.channel); if (!data.channel || data.channel.type !== 'voice') { @@ -266,12 +266,12 @@ class GuildMember extends Base { } else { endpoint = endpoint.members(this.id); } - return endpoint.patch({ data, reason }).then(() => { - const clone = this._clone(); - data.user = this.user; - clone._patch(data); - return clone; - }); + await endpoint.patch({ data, reason }); + + const clone = this._clone(); + data.user = this.user; + clone._patch(data); + return clone; } /** From d057a5040cc372946a341c0b211cd93ea1beb299 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sun, 10 Feb 2019 00:51:23 +0100 Subject: [PATCH 044/428] ci: Test in Node.js 10 and 11, deploy with Node.js 10 (#3069) --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3b89d2af..2215629f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ language: node_js node_js: - - 8 - - 9 + - 10 + - 11 install: npm install script: bash ./travis/test.sh jobs: include: - stage: deploy - node_js: 9 + node_js: 10 script: bash ./travis/deploy.sh env: - ENCRYPTION_LABEL="af862fa96d3e" From ff95e587cb5a49921eb206dd2cd034d692e99bff Mon Sep 17 00:00:00 2001 From: Atlas <41349879+cloudrex@users.noreply.github.com> Date: Sat, 9 Feb 2019 18:53:47 -0500 Subject: [PATCH 045/428] Better wording (#3032) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c4cb7c5e..faaa2bfd 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ ## About -discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to interact with the -[Discord API](https://discordapp.com/developers/docs/intro) very easily. +discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the +[Discord API](https://discordapp.com/developers/docs/intro). - Object-oriented - Predictable abstractions From 7324a993edf0c92c94ff70f3e5462535bfa08455 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 10 Feb 2019 16:21:59 +0100 Subject: [PATCH 046/428] fix: ensure VIEW_CHANNEL permissions before trying to join (#3046) * fix: ensure VIEW_CHANNEL permissions before joining * nit(GuildChannel): remove the redundant truthy check --- src/structures/GuildChannel.js | 13 ++++++++++++- src/structures/VoiceChannel.js | 7 ++++--- typings/index.d.ts | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index dc0a4e41..9904e4c1 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -523,10 +523,21 @@ class GuildChannel extends Channel { * @readonly */ get manageable() { + if (this.client.user.id === this.guild.ownerID) return true; + if (!this.viewable) return false; + return this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false); + } + + /** + * Whether the channel is viewable by the client user + * @type {boolean} + * @readonly + */ + get viewable() { if (this.client.user.id === this.guild.ownerID) return true; const permissions = this.permissionsFor(this.client.user); if (!permissions) return false; - return permissions.has([Permissions.FLAGS.MANAGE_CHANNELS, Permissions.FLAGS.VIEW_CHANNEL], false); + return permissions.has(Permissions.FLAGS.VIEW_CHANNEL, false); } /** diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index cf9b1ee9..2d1fa956 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -71,14 +71,15 @@ class VoiceChannel extends GuildChannel { } /** - * Checks if the client has permission join the voice channel + * Whether the channel is joinable by the client user * @type {boolean} * @readonly */ get joinable() { if (browser) return false; - if (!this.permissionsFor(this.client.user).has('CONNECT', false)) return false; - if (this.full && !this.permissionsFor(this.client.user).has('MOVE_MEMBERS', false)) return false; + if (!this.viewable) return false; + if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) return false; + if (this.full && !this.permissionsFor(this.client.user).has(Permissions.FLAGS.MOVE_MEMBERS, false)) return false; return true; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 60a19f3d..dccd3304 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -523,6 +523,7 @@ declare module 'discord.js' { public readonly permissionsLocked: boolean; public readonly position: number; public rawPosition: number; + public readonly viewable: boolean; public clone(options?: GuildChannelCloneOptions): Promise; public createInvite(options?: InviteOptions): Promise; public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; From 793341dbb401f2956d60fd6b9f94a611cf539126 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 10 Feb 2019 18:28:03 +0200 Subject: [PATCH 047/428] fix: Sharding issues, silent disconnects and code cleanup (#2976) * fix: Sharding bugs, silent disconnects and cleanup code * typings * fix: Destroy connecting with close code different from 1000 Per `If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume.` * misc: Wait x ms before reconnecting Per https://discordapp.com/developers/docs/topics/gateway#resuming * docs * nit: docs * misc: Prevent multiple calls to WebSocketManager#destroy * fix: Implement destroying if you reset the token * misc: Clear the WS packet queue on WebSocketShard#destroy You can't send those packets anywhere anymore, so no point in keeping them * fix: Handle session limits when reconnecting a full shard, cleanup * misc: No need to create a new shard instance * fix: closeSequence being null, thus emitting null on Client#resumed * misc: Remove GUILD_SYNC Gateway handler and add missing dot to string * misc: Close WS with code 4000 if we didn't get a heartbeat in time As said in the Discord API server * fix: Handle ready emitting in onPacket Doesn't allow broken packets * misc: Close the connection if Discord asks for a reconnect Prevents double triggers * testing: Prevent multiple reconnect attempts on a shard Should fix some issues some people have had. * fix: Prevent multiple reconnect calls on the shard, re-use conn to identify, remove reconnect function Note: Closing the WS with 1000 makes the session invalid * misc: Forgot to remove 2 unneeded setters * docs: Wrong param docstring for WebSocketShard#destroy * misc: Set status to reconnecting after destroying * misc: Close connection with code 1000 on session invalidated Allows us to cleanup the shard and do a full reconnect Also remove identify wait delay, not used anywhere * fix: Fix zlib crash on node And with that, the PR is done! * misc: Implement a reconnect queue And that is all there was to be done in this PR. Shards now queue up for a reconnect * nit: Debug the queue after destroying * docs: Make the invalidated event clearer * lint: I'm good at my job * docs * docs: Make description for isReconnectingShards accurate *can I stop finding issues, this PR is meant to be done* * misc: Remove shard from bind params * misc: Code re-ordering and cleanup Resumes do not need to be queued up, as they do not count to the identify limit, and after some testing, they don't have the 5 second delay required, like in identify * fix: Issues with token regeneration and shards not properly handling them We close the ws connection with code 1000 if we get an invalid session payload, that way we can queue the reconnects and handle any issues * misc: Remove useless delays on session invalidated They get handled by the rest of the code already * lint * misc: reset the sequence on Shard#destroy This especially is a problem if you need to re-identify, as the sequence doesn't get set to the current one, causing the sequence to be wrong * fix: GitHub rebase and minor tweak * Implement a 15 second timeout if shards don't connect till then Should prevent shards that never reconnect * revert: Make WebSocketShard#send and WebSocketManager#broadcast public * typings: Set type to void instead of undefined * docs: Requested Changes --- src/client/websocket/WebSocketManager.js | 217 ++++++---- src/client/websocket/WebSocketShard.js | 416 ++++++++++---------- src/client/websocket/handlers/GUILD_SYNC.js | 5 - typings/index.d.ts | 12 +- 4 files changed, 353 insertions(+), 297 deletions(-) delete mode 100644 src/client/websocket/handlers/GUILD_SYNC.js diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index fd749968..bc34b84b 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,6 +1,7 @@ 'use strict'; const Collection = require('../../util/Collection'); +const Util = require('../../util/Util'); const WebSocketShard = require('./WebSocketShard'); const { Events, Status, WSEvents } = require('../../util/Constants'); const PacketHandlers = require('./handlers'); @@ -16,7 +17,7 @@ const BeforeReadyWhitelist = [ ]; /** - * WebSocket Manager of the client. + * The WebSocket manager for this client. */ class WebSocketManager { constructor(client) { @@ -28,52 +29,60 @@ class WebSocketManager { Object.defineProperty(this, 'client', { value: client }); /** - * The gateway this WebSocketManager uses. + * The gateway this manager uses * @type {?string} */ this.gateway = undefined; /** - * An array of shards spawned by this WebSocketManager. + * A collection of all shards this manager handles * @type {Collection} */ this.shards = new Collection(); /** - * An array of queued shards to be spawned by this WebSocketManager. - * @type {Array} + * An array of shards to be spawned or reconnected + * @type {Array} * @private */ - this.spawnQueue = []; + this.shardQueue = []; /** - * Whether or not this WebSocketManager is currently spawning shards. - * @type {boolean} - * @private - */ - this.spawning = false; - - /** - * An array of queued events before this WebSocketManager became ready. + * An array of queued events before this WebSocketManager became ready * @type {object[]} * @private */ this.packetQueue = []; /** - * The current status of this WebSocketManager. + * The current status of this WebSocketManager * @type {number} */ this.status = Status.IDLE; /** - * The current session limit of the client. + * If this manager is expected to close + * @type {boolean} + * @private + */ + this.expectingClose = false; + + /** + * The current session limit of the client * @type {?Object} + * @private * @prop {number} total Total number of identifies available * @prop {number} remaining Number of identifies remaining * @prop {number} reset_after Number of milliseconds after which the limit resets */ this.sessionStartLimit = null; + + /** + * If the manager is currently reconnecting shards + * @type {boolean} + * @private + */ + this.isReconnectingShards = false; } /** @@ -89,103 +98,147 @@ class WebSocketManager { /** * Emits a debug event. * @param {string} message Debug message - * @returns {void} * @private */ debug(message) { - this.client.emit(Events.DEBUG, `[connection] ${message}`); + this.client.emit(Events.DEBUG, message); } /** - * Handles the session identify rate limit for a shard. - * @param {WebSocketShard} shard Shard to handle + * Checks if a new identify payload can be sent. * @private + * @returns {Promise} */ - async _handleSessionLimit(shard) { + async _checkSessionLimit() { this.sessionStartLimit = await this.client.api.gateway.bot.get().then(r => r.session_start_limit); const { remaining, reset_after } = this.sessionStartLimit; - if (remaining !== 0) { - this.spawn(); - } else { - shard.debug(`Exceeded identify threshold, setting a timeout for ${reset_after} ms`); - setTimeout(() => this.spawn(), this.sessionStartLimit.reset_after); - } + if (remaining !== 0) return true; + return reset_after; } /** - * Used to spawn WebSocketShards. - * @param {?WebSocketShard|WebSocketShard[]|number|string} query The WebSocketShards to be spawned - * @returns {void} + * Handles the session identify rate limit for creating a shard. * @private */ - spawn(query) { - if (query !== undefined) { - if (Array.isArray(query)) { - for (const item of query) { - if (!this.spawnQueue.includes(item)) this.spawnQueue.push(item); - } - } else if (!this.spawnQueue.includes(query)) { - this.spawnQueue.push(query); - } - } - - if (this.spawning || !this.spawnQueue.length) return; - - this.spawning = true; - let item = this.spawnQueue.shift(); - - if (typeof item === 'string' && !isNaN(item)) item = Number(item); - if (typeof item === 'number') { - const shard = new WebSocketShard(this, item, this.shards.get(item)); - this.shards.set(item, shard); - shard.once(Events.READY, () => { - this.spawning = false; - this.client.setTimeout(() => this._handleSessionLimit(shard), 5000); - }); - shard.once(Events.INVALIDATED, () => { - this.spawning = false; - }); - } else if (item instanceof WebSocketShard) { - item.reconnect(); + async _handleSessionLimit() { + const canSpawn = await this._checkSessionLimit(); + if (typeof canSpawn === 'number') { + this.debug(`Exceeded identify threshold, setting a timeout for ${canSpawn}ms`); + await Util.delayFor(canSpawn); } + this.create(); } /** * Creates a connection to a gateway. * @param {string} [gateway=this.gateway] The gateway to connect to - * @returns {void} * @private */ connect(gateway = this.gateway) { this.gateway = gateway; if (typeof this.client.options.shards === 'number') { - this.debug('Spawning 1 shard'); - this.spawn(this.client.options.shards); + this.debug(`Spawning shard with ID ${this.client.options.shards}`); + this.shardQueue.push(this.client.options.shards); } else if (Array.isArray(this.client.options.shards)) { this.debug(`Spawning ${this.client.options.shards.length} shards`); - for (const shard of this.client.options.shards) { - this.spawn(shard); - } + this.shardQueue.push(...this.client.options.shards); } else { this.debug(`Spawning ${this.client.options.shardCount} shards`); - for (let i = 0; i < this.client.options.shardCount; i++) { - this.spawn(i); + this.shardQueue.push(...Array.from({ length: this.client.options.shardCount }, (_, index) => index)); + } + this.create(); + } + + /** + * Creates or reconnects a shard. + * @private + */ + create() { + // Nothing to create + if (!this.shardQueue.length) return; + + let item = this.shardQueue.shift(); + if (typeof item === 'string' && !isNaN(item)) item = Number(item); + + if (item instanceof WebSocketShard) { + const timeout = setTimeout(() => { + this.debug(`[Shard ${item.id}] Failed to connect in 15s... Destroying and trying again`); + item.destroy(); + if (!this.shardQueue.includes(item)) this.shardQueue.push(item); + this.reconnect(true); + }, 15000); + item.once(Events.READY, this._shardReady.bind(this, timeout)); + item.once(Events.RESUMED, this._shardReady.bind(this, timeout)); + item.connect(); + return; + } + + const shard = new WebSocketShard(this, item); + this.shards.set(item, shard); + shard.once(Events.READY, this._shardReady.bind(this)); + } + + /** + * Shared handler for shards turning ready or resuming. + * @param {Timeout} [timeout=null] Optional timeout to clear if shard didn't turn ready in time + * @private + */ + _shardReady(timeout = null) { + if (timeout) clearTimeout(timeout); + if (this.shardQueue.length) { + this.client.setTimeout(this._handleSessionLimit.bind(this), 5000); + } else { + this.isReconnectingShards = false; + } + } + + /** + * Handles the reconnect of a shard. + * @param {WebSocketShard|boolean} shard The shard to reconnect, or a boolean to indicate an immediate reconnect + * @private + */ + async reconnect(shard) { + // If the item is a shard, add it to the queue + if (shard instanceof WebSocketShard) this.shardQueue.push(shard); + if (typeof shard === 'boolean') { + // If a boolean is passed, force a reconnect right now + } else if (this.isReconnectingShards) { + // If we're already reconnecting shards, and no boolean was provided, return + return; + } + this.isReconnectingShards = true; + try { + await this._handleSessionLimit(); + } catch (error) { + // If we get an error at this point, it means we cannot reconnect anymore + if (this.client.listenerCount(Events.INVALIDATED)) { + /** + * Emitted when the client's session becomes invalidated. + * You are expected to handle closing the process gracefully and preventing a boot loop + * if you are listening to this event. + * @event Client#invalidated + */ + this.client.emit(Events.INVALIDATED); + // Destroy just the shards. This means you have to handle the cleanup yourself + this.destroy(); + } else { + this.client.destroy(); } } } /** * Processes a packet and queues it if this WebSocketManager is not ready. - * @param {Object} packet The packet to be handled - * @param {WebSocketShard} shard The shard that will handle this packet + * @param {Object} [packet] The packet to be handled + * @param {WebSocketShard} [shard] The shard that will handle this packet * @returns {boolean} * @private */ handlePacket(packet, shard) { if (packet && this.status !== Status.READY) { if (!BeforeReadyWhitelist.includes(packet.t)) { - this.packetQueue.push({ packet, shardID: shard.id }); + this.packetQueue.push({ packet, shard }); return false; } } @@ -193,7 +246,7 @@ class WebSocketManager { if (this.packetQueue.length) { const item = this.packetQueue.shift(); this.client.setImmediate(() => { - this.handlePacket(item.packet, this.shards.get(item.shardID)); + this.handlePacket(item.packet, item.shard); }); } @@ -201,7 +254,7 @@ class WebSocketManager { PacketHandlers[packet.t](this.client, packet, shard); } - return false; + return true; } /** @@ -211,7 +264,7 @@ class WebSocketManager { */ checkReady() { if (this.shards.size !== this.client.options.shardCount || - this.shards.some(s => s && s.status !== Status.READY)) { + this.shards.some(s => s.status !== Status.READY)) { return false; } @@ -258,26 +311,22 @@ class WebSocketManager { /** * Broadcasts a message to every shard in this WebSocketManager. * @param {*} packet The packet to send + * @private */ broadcast(packet) { - for (const shard of this.shards.values()) { - shard.send(packet); - } + for (const shard of this.shards.values()) shard.send(packet); } /** * Destroys all shards. - * @returns {void} * @private */ destroy() { - this.gateway = undefined; - // Lock calls to spawn - this.spawning = true; - - for (const shard of this.shards.values()) { - shard.destroy(); - } + if (this.expectingClose) return; + this.expectingClose = true; + this.isReconnectingShards = false; + this.shardQueue.length = 0; + for (const shard of this.shards.values()) shard.destroy(); } } diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 1cf0ef1c..96ab06a4 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -4,6 +4,7 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); const Util = require('../../util/Util'); + let zlib; try { zlib = require('zlib-sync'); @@ -13,10 +14,10 @@ try { } /** - * Represents a Shard's Websocket connection. + * Represents a Shard's WebSocket connection */ class WebSocketShard extends EventEmitter { - constructor(manager, id, oldShard) { + constructor(manager, id) { super(); /** @@ -26,7 +27,7 @@ class WebSocketShard extends EventEmitter { this.manager = manager; /** - * The id of the this shard. + * The ID of the this shard * @type {number} */ this.id = id; @@ -38,28 +39,28 @@ class WebSocketShard extends EventEmitter { this.status = Status.IDLE; /** - * The current sequence of the WebSocket + * The current sequence of the shard * @type {number} * @private */ - this.sequence = oldShard ? oldShard.sequence : -1; + this.sequence = -1; /** - * The sequence on WebSocket close + * The sequence of the shard after close * @type {number} * @private */ - this.closeSequence = oldShard ? oldShard.closeSequence : 0; + this.closeSequence = 0; /** - * The current session id of the WebSocket - * @type {?string} + * The current session ID of the shard + * @type {string} * @private */ - this.sessionID = oldShard && oldShard.sessionID; + this.sessionID = undefined; /** - * Previous heartbeat pings of the websocket (most recent first, limited to three elements) + * The previous 3 heartbeat pings of the shard (most recent first) * @type {number[]} */ this.pings = []; @@ -71,6 +72,13 @@ class WebSocketShard extends EventEmitter { */ this.lastPingTimestamp = -1; + /** + * If we received a heartbeat ack back. Used to identify zombie connections + * @type {boolean} + * @private + */ + this.lastHeartbeatAcked = true; + /** * List of servers the shard is connected to * @type {string[]} @@ -96,7 +104,7 @@ class WebSocketShard extends EventEmitter { * @type {?WebSocket} * @private */ - this.ws = null; + this.connection = null; /** * @external Inflate @@ -110,13 +118,7 @@ class WebSocketShard extends EventEmitter { */ this.inflate = null; - /** - * Whether or not the WebSocket is expected to be closed - * @type {boolean} - */ - this.expectingClose = false; - - this.connect(); + if (this.manager.gateway) this.connect(); } /** @@ -135,35 +137,42 @@ class WebSocketShard extends EventEmitter { * @private */ debug(message) { - this.manager.debug(`[shard ${this.id}] ${message}`); + this.manager.debug(`[Shard ${this.id}] ${message}`); } /** - * Sends a heartbeat or sets an interval for sending heartbeats. - * @param {number} [time] If -1, clears the interval, any other number sets an interval - * If no value is given, a heartbeat will be sent instantly + * Sends a heartbeat to the WebSocket. + * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect * @private */ - heartbeat(time) { - if (!isNaN(time)) { - if (time === -1) { + sendHeartbeat() { + if (!this.lastHeartbeatAcked) { + this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting."); + this.connection.close(4000); + return; + } + this.debug('Sending a heartbeat'); + this.lastHeartbeatAcked = false; + this.lastPingTimestamp = Date.now(); + this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }); + } + + /** + * Sets the heartbeat timer for this shard. + * @param {number} time If -1, clears the interval, any other number sets an interval + * @private + */ + setHeartbeatTimer(time) { + if (time === -1) { + if (this.heartbeatInterval) { this.debug('Clearing heartbeat interval'); this.manager.client.clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; - } else { - this.debug(`Setting a heartbeat interval for ${time}ms`); - if (this.heartbeatInterval) this.manager.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = this.manager.client.setInterval(() => this.heartbeat(), time); } return; } - - this.debug('Sending a heartbeat'); - this.lastPingTimestamp = Date.now(); - this.send({ - op: OPCodes.HEARTBEAT, - d: this.sequence, - }); + this.debug(`Setting a heartbeat interval for ${time}ms`); + this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); } /** @@ -171,6 +180,7 @@ class WebSocketShard extends EventEmitter { * @private */ ackHeartbeat() { + this.lastHeartbeatAcked = true; const latency = Date.now() - this.lastPingTimestamp; this.debug(`Heartbeat acknowledged, latency of ${latency}ms`); this.pings.unshift(latency); @@ -178,18 +188,19 @@ class WebSocketShard extends EventEmitter { } /** - * Connects the shard to a gateway. + * Connects this shard to the gateway. * @private */ connect() { + const { expectingClose, gateway } = this.manager; + if (expectingClose) return; this.inflate = new zlib.Inflate({ chunkSize: 65535, flush: zlib.Z_SYNC_FLUSH, to: WebSocket.encoding === 'json' ? 'string' : '', }); - const gateway = this.manager.gateway; this.debug(`Connecting to ${gateway}`); - const ws = this.ws = WebSocket.create(gateway, { + const ws = this.connection = WebSocket.create(gateway, { v: this.manager.client.options.ws.version, compress: 'zlib-stream', }); @@ -200,73 +211,12 @@ class WebSocketShard extends EventEmitter { this.status = Status.CONNECTING; } - /** - * Called whenever a packet is received - * @param {Object} packet Packet received - * @returns {any} - * @private - */ - onPacket(packet) { - if (!packet) { - this.debug('Received null packet'); - return false; - } - - switch (packet.t) { - case WSEvents.READY: - this.sessionID = packet.d.session_id; - this.trace = packet.d._trace; - this.status = Status.READY; - this.debug(`READY ${this.trace.join(' -> ')} ${this.sessionID}`); - this.heartbeat(); - break; - case WSEvents.RESUMED: { - this.trace = packet.d._trace; - this.status = Status.READY; - const replayed = packet.s - this.closeSequence; - this.debug(`RESUMED ${this.trace.join(' -> ')} | replayed ${replayed} events.`); - this.heartbeat(); - break; - } - } - - if (packet.s > this.sequence) this.sequence = packet.s; - - switch (packet.op) { - case OPCodes.HELLO: - this.identify(); - return this.heartbeat(packet.d.heartbeat_interval); - case OPCodes.RECONNECT: - return this.reconnect(); - case OPCodes.INVALID_SESSION: - this.sequence = -1; - this.debug('Session invalidated'); - // If the session isn't resumable - if (!packet.d) { - // If we had a session ID before - if (this.sessionID) { - this.sessionID = null; - return this.identify(2500); - } - return this.identify(5000); - } - return this.identify(); - case OPCodes.HEARTBEAT_ACK: - return this.ackHeartbeat(); - case OPCodes.HEARTBEAT: - return this.heartbeat(); - default: - return this.manager.handlePacket(packet, this); - } - } - /** * Called whenever a connection is opened to the gateway. - * @param {Event} event Received open event * @private */ onOpen() { - this.debug('Connection open'); + this.debug('Connected to the gateway'); } /** @@ -293,87 +243,102 @@ class WebSocketShard extends EventEmitter { this.manager.client.emit(Events.ERROR, err); return; } - if (packet.t === WSEvents.READY) { - /** - * Emitted when a shard becomes ready - * @event WebSocketShard#ready - */ - this.emit(Events.READY); - - /** - * Emitted when a shard becomes ready - * @event Client#shardReady - * @param {number} shardID The id of the shard - */ - this.manager.client.emit(Events.SHARD_READY, this.id); - } this.onPacket(packet); } /** - * Called whenever an error occurs with the WebSocket. - * @param {Error} error The error that occurred + * Called whenever a packet is received. + * @param {Object} packet Packet received * @private */ - onError(error) { - if (error && error.message === 'uWs client connection error') { - this.reconnect(); + onPacket(packet) { + if (!packet) { + this.debug('Received null or broken packet'); return; } - this.emit(Events.INVALIDATED); - /** - * Emitted whenever the client's WebSocket encounters a connection error. - * @event Client#error - * @param {Error} error The encountered error - */ - this.manager.client.emit(Events.ERROR, error); - } + switch (packet.t) { + case WSEvents.READY: + /** + * Emitted when a shard becomes ready. + * @event WebSocketShard#ready + */ + this.emit(Events.READY); + /** + * Emitted when a shard becomes ready. + * @event Client#shardReady + * @param {number} shardID The ID of the shard + */ + this.manager.client.emit(Events.SHARD_READY, this.id); - /** - * @external CloseEvent - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} - */ - - /** - * Called whenever a connection to the gateway is closed. - * @param {CloseEvent} event Close event that was received - * @returns {void} - * @private - */ - onClose(event) { - this.closeSequence = this.sequence; - this.emit('close', event); - if (event.code === 1000 ? this.expectingClose : WSCodes[event.code]) { - /** - * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. - * @event Client#disconnect - * @param {CloseEvent} event The WebSocket close event - * @param {number} shardID The shard that disconnected - */ - this.manager.client.emit(Events.DISCONNECT, event, this.id); - this.debug(WSCodes[event.code]); - this.heartbeat(-1); - return; + this.sessionID = packet.d.session_id; + this.trace = packet.d._trace; + this.status = Status.READY; + this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}`); + this.lastHeartbeatAcked = true; + this.sendHeartbeat(); + break; + case WSEvents.RESUMED: { + this.emit(Events.RESUMED); + this.trace = packet.d._trace; + this.status = Status.READY; + const replayed = packet.s - this.closeSequence; + this.debug(`RESUMED ${this.trace.join(' -> ')} | Replayed ${replayed} events.`); + this.lastHeartbeatAcked = true; + this.sendHeartbeat(); + break; + } + } + + if (packet.s > this.sequence) this.sequence = packet.s; + + switch (packet.op) { + case OPCodes.HELLO: + this.setHeartbeatTimer(packet.d.heartbeat_interval); + this.identify(); + break; + case OPCodes.RECONNECT: + this.connection.close(1001); + break; + case OPCodes.INVALID_SESSION: + this.debug(`Session was invalidated. Resumable: ${packet.d}.`); + // If the session isn't resumable + if (!packet.d) { + // Reset the sequence, since it isn't valid anymore + this.sequence = -1; + // If we had a session ID before + if (this.sessionID) { + this.sessionID = null; + this.connection.close(1000); + return; + } + this.connection.close(1000); + return; + } + this.identifyResume(); + break; + case OPCodes.HEARTBEAT_ACK: + this.ackHeartbeat(); + break; + case OPCodes.HEARTBEAT: + this.sendHeartbeat(); + break; + default: + this.manager.handlePacket(packet, this); } - this.expectingClose = false; - this.reconnect(Events.INVALIDATED, 5100); } /** * Identifies the client on a connection. - * @param {?number} [wait=0] Amount of time to wait before identifying * @returns {void} * @private */ - identify(wait = 0) { - if (wait) return this.manager.client.setTimeout(this.identify.bind(this), wait); + identify() { return this.sessionID ? this.identifyResume() : this.identifyNew(); } /** * Identifies as a new connection on the gateway. - * @returns {void} * @private */ identifyNew() { @@ -382,10 +347,11 @@ class WebSocketShard extends EventEmitter { return; } // Clone the generic payload and assign the token - const d = Object.assign({ token: this.manager.client.token }, this.manager.client.options.ws); - - const { totalShardCount } = this.manager.client.options; - d.shard = [this.id, Number(totalShardCount)]; + const d = { + ...this.manager.client.options.ws, + token: this.manager.client.token, + shard: [this.id, Number(this.manager.client.options.totalShardCount)], + }; // Send the payload this.debug('Identifying as a new session'); @@ -402,23 +368,90 @@ class WebSocketShard extends EventEmitter { this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead'); return this.identifyNew(); } - this.debug(`Attempting to resume session ${this.sessionID}`); + + this.debug(`Attempting to resume session ${this.sessionID} at sequence ${this.closeSequence}`); const d = { token: this.manager.client.token, session_id: this.sessionID, - seq: this.sequence, + seq: this.closeSequence, }; - return this.send({ - op: OPCodes.RESUME, - d, - }); + return this.send({ op: OPCodes.RESUME, d }); + } + + /** + * Called whenever an error occurs with the WebSocket. + * @param {Error} error The error that occurred + * @private + */ + onError(error) { + if (error && error.message === 'uWs client connection error') { + this.connection.close(4000); + return; + } + + /** + * Emitted whenever the client's WebSocket encounters a connection error. + * @event Client#error + * @param {Error} error The encountered error + * @param {number} shardID The shard that encountered this error + */ + this.manager.client.emit(Events.ERROR, error, this.id); + } + + /** + * @external CloseEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} + */ + + /** + * Called whenever a connection to the gateway is closed. + * @param {CloseEvent} event Close event that was received + * @private + */ + onClose(event) { + this.closeSequence = this.sequence; + this.debug(`WebSocket was closed. + Event Code: ${event.code} + Reason: ${event.reason}`); + + if (event.code === 1000 ? this.manager.expectingClose : WSCodes[event.code]) { + /** + * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. + * @event Client#disconnect + * @param {CloseEvent} event The WebSocket close event + * @param {number} shardID The shard that disconnected + */ + this.manager.client.emit(Events.DISCONNECT, event, this.id); + this.debug(WSCodes[event.code]); + return; + } + + this.destroy(); + + this.status = Status.RECONNECTING; + + /** + * Emitted whenever a shard tries to reconnect to the WebSocket. + * @event Client#reconnecting + * @param {number} shardID The shard ID that is reconnecting + */ + this.manager.client.emit(Events.RECONNECTING, this.id); + + this.debug(`${this.sessionID ? `Reconnecting in 3500ms` : 'Queueing a reconnect'} to the gateway...`); + + if (this.sessionID) { + Util.delayFor(3500).then(() => this.connect()); + } else { + this.manager.reconnect(this); + } } /** * Adds data to the queue to be sent. * @param {Object} data Packet to send + * @private * @returns {void} */ send(data) { @@ -433,11 +466,12 @@ class WebSocketShard extends EventEmitter { * @private */ _send(data) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${data} but no WebSocket is available!`); + if (!this.connection || this.connection.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`); return; } - this.ws.send(WebSocket.pack(data), err => { + + this.connection.send(WebSocket.pack(data), err => { if (err) this.manager.client.emit(Events.ERROR, err); }); } @@ -465,44 +499,22 @@ class WebSocketShard extends EventEmitter { } /** - * Triggers a shard reconnect. - * @param {?string} [event] The event for the shard to emit - * @param {?number} [reconnectIn] Time to wait before reconnecting - * @returns {Promise} - * @private - */ - async reconnect(event, reconnectIn) { - this.heartbeat(-1); - this.status = Status.RECONNECTING; - - /** - * Emitted whenever a shard tries to reconnect to the WebSocket. - * @event Client#reconnecting - */ - this.manager.client.emit(Events.RECONNECTING, this.id); - - if (event === Events.INVALIDATED) this.emit(event); - this.debug(reconnectIn ? `Reconnecting in ${reconnectIn}ms` : 'Reconnecting now'); - if (reconnectIn) await Util.delayFor(reconnectIn); - this.manager.spawn(this.id); - } - - /** - * Destroys the current shard and terminates its connection. - * @returns {void} + * Destroys this shard and closes its connection. * @private */ destroy() { - this.heartbeat(-1); - this.expectingClose = true; - if (this.ws) this.ws.close(1000); - this.ws = null; + this.setHeartbeatTimer(-1); + if (this.connection) this.connection.close(1000); + this.connection = null; this.status = Status.DISCONNECTED; this.ratelimit.remaining = this.ratelimit.total; + this.ratelimit.queue.length = 0; if (this.ratelimit.timer) { this.manager.client.clearTimeout(this.ratelimit.timer); this.ratelimit.timer = null; } + this.sequence = -1; } } + module.exports = WebSocketShard; diff --git a/src/client/websocket/handlers/GUILD_SYNC.js b/src/client/websocket/handlers/GUILD_SYNC.js deleted file mode 100644 index b7e7d1b2..00000000 --- a/src/client/websocket/handlers/GUILD_SYNC.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = (client, packet) => { - client.actions.GuildSync.handle(packet.d); -}; diff --git a/typings/index.d.ts b/typings/index.d.ts index dccd3304..1b1d17fc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -434,7 +434,7 @@ declare module 'discord.js' { public presences: PresenceStore; public region: string; public roles: RoleStore; - public shard: WebSocketShard; + public readonly shard: WebSocketShard; public shardID: number; public splash: string; public readonly systemChannel: TextChannel; @@ -1295,22 +1295,22 @@ declare module 'discord.js' { public gateway: string | undefined; public readonly ping: number; public shards: Collection; - public sessionStartLimit: { total: number; remaining: number; reset_after: number; }; public status: Status; - public broadcast(packet: any): void; + + public broadcast(packet: object): void; } export class WebSocketShard extends EventEmitter { - constructor(manager: WebSocketManager, id: number, oldShard?: WebSocketShard); + constructor(manager: WebSocketManager, id: number); public id: number; public readonly ping: number; public pings: number[]; public status: Status; public manager: WebSocketManager; - public send(data: object): void; + + public send(packet: object): void; public on(event: 'ready', listener: () => void): this; - public once(event: 'ready', listener: () => void): this; } From aab3523fb5b60d21d20cbf2be01c32bdf21aa8ae Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 10 Feb 2019 20:18:08 +0000 Subject: [PATCH 048/428] voice: more debug information #3074, #2979, #3044 --- src/client/voice/ClientVoiceManager.js | 2 ++ src/client/voice/VoiceConnection.js | 3 +++ src/client/voice/networking/VoiceUDPClient.js | 13 +++++++++++-- src/client/voice/networking/VoiceWebSocket.js | 8 ++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index a9081efd..fce2d3b6 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -72,6 +72,8 @@ class ClientVoiceManager { reject(reason); }); + connection.on('debug', msg => this.client.emit('debug', `[VOICE (${channel.guild.id})]: ${msg}`)); + connection.once('authenticated', () => { connection.once('ready', () => { resolve(connection); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 0242d541..4d5c23a8 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -396,6 +396,8 @@ class VoiceConnection extends EventEmitter { const { ws, udp } = this.sockets; + ws.on('debug', msg => this.emit('debug', msg)); + udp.on('debug', msg => this.emit('debug', msg)); ws.on('error', err => this.emit('error', err)); udp.on('error', err => this.emit('error', err)); ws.on('ready', this.onReady.bind(this)); @@ -431,6 +433,7 @@ class VoiceConnection extends EventEmitter { const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); dispatcher.on('finish', () => { clearTimeout(this.connectTimeout); + this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, * the connection will already be ready. diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index c1dd9e3d..6c5182d9 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -48,6 +48,7 @@ class VoiceConnectionUDPClient extends EventEmitter { } shutdown() { + this.emit('debug', `[UDP] shutdown requested`); if (this.socket) { this.socket.removeAllListeners('message'); try { @@ -77,7 +78,12 @@ class VoiceConnectionUDPClient extends EventEmitter { if (!this.socket) throw new Error('UDP_SEND_FAIL'); if (!this.discordAddress || !this.discordPort) throw new Error('UDP_ADDRESS_MALFORMED'); this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { - if (error) reject(error); else resolve(packet); + if (error) { + this.emit('debug', `[UDP] >> ERROR: ${error}`); + reject(error); + } else { + resolve(packet); + } }); }); } @@ -85,13 +91,14 @@ class VoiceConnectionUDPClient extends EventEmitter { createUDPSocket(address) { this.discordAddress = address; const socket = this.socket = udp.createSocket('udp4'); - + this.emit('debug', `[UDP] created socket`); socket.once('message', message => { // Stop if the sockets have been deleted because the connection has been closed already if (!this.voiceConnection.sockets.ws) return; const packet = parseLocalPacket(message); if (packet.error) { + this.emit('debug', `[UDP] ERROR: ${packet.error}`); this.emit('error', packet.error); return; } @@ -111,6 +118,8 @@ class VoiceConnectionUDPClient extends EventEmitter { }, }); + this.emit('debug', `[UDP] << ${JSON.stringify(packet)}`); + socket.on('message', buffer => this.voiceConnection.receiver.packets.push(buffer)); }); diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index d556145b..ea2db439 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -39,6 +39,7 @@ class VoiceWebSocket extends EventEmitter { } shutdown() { + this.emit('debug', `[WS] shutdown requested`); this.dead = true; this.reset(); } @@ -47,6 +48,7 @@ class VoiceWebSocket extends EventEmitter { * Resets the current WebSocket. */ reset() { + this.emit('debug', `[WS] reset requested`); if (this.ws) { if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close(); this.ws = null; @@ -58,6 +60,7 @@ class VoiceWebSocket extends EventEmitter { * Starts connecting to the Voice WebSocket Server. */ connect() { + this.emit('debug', `[WS] connect requested`); if (this.dead) return; if (this.ws) this.reset(); if (this.attempts >= 5) { @@ -66,6 +69,7 @@ class VoiceWebSocket extends EventEmitter { } this.attempts++; + this.emit('debug', `[WS] connecting with ${this.attempts} attempts`); /** * The actual WebSocket used to connect to the Voice WebSocket Server. @@ -84,6 +88,7 @@ class VoiceWebSocket extends EventEmitter { * @returns {Promise} */ send(data) { + this.emit('debug', `[WS] >> ${data}`); return new Promise((resolve, reject) => { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data); this.ws.send(data, null, error => { @@ -110,6 +115,7 @@ class VoiceWebSocket extends EventEmitter { * Called whenever the WebSocket opens. */ onOpen() { + this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`); this.sendPacket({ op: OPCodes.DISPATCH, d: { @@ -140,6 +146,7 @@ class VoiceWebSocket extends EventEmitter { * Called whenever the connection to the WebSocket server is lost. */ onClose() { + this.emit('debug', `[WS] closed`); if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000); } @@ -156,6 +163,7 @@ class VoiceWebSocket extends EventEmitter { * @param {Object} packet The received packet */ onPacket(packet) { + this.emit('debug', `[WS] << ${JSON.stringify(packet)}`); switch (packet.op) { case VoiceOPCodes.HELLO: this.setHeartbeat(packet.d.heartbeat_interval); From 0c5b9c687058d035277450f5404e7e2e465d1282 Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 11 Feb 2019 08:41:37 +0100 Subject: [PATCH 049/428] chore(deps): Updated all required, peer, and development dependencies (#3073) * chore(deps): Updated all required, peer, and development dependencies And engines.node to require 10 * chore(peerDeps): Updated uws to 11.149.1 Clarified my doubts with iCrawl --- package.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index c3b4b40c..c1db1698 100644 --- a/package.json +++ b/package.json @@ -35,36 +35,36 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { - "form-data": "^2.3.2", - "node-fetch": "^2.1.2", - "pako": "^1.0.0", + "form-data": "^2.3.3", + "node-fetch": "^2.3.0", + "pako": "^1.0.8", "prism-media": "amishshah/prism-media", "setimmediate": "^1.0.5", - "tweetnacl": "^1.0.0", - "ws": "^6.0.0" + "tweetnacl": "^1.0.1", + "ws": "^6.1.3" }, "peerDependencies": { - "@discordjs/uws": "^10.149.0", - "bufferutil": "^4.0.0", + "@discordjs/uws": "^11.149.1", + "bufferutil": "^4.0.1", "erlpack": "discordapp/erlpack", - "libsodium-wrappers": "^0.7.3", - "sodium": "^2.0.3", + "libsodium-wrappers": "^0.7.4", + "sodium": "^3.0.2", "zlib-sync": "^0.1.4" }, "devDependencies": { - "@types/node": "^10.7.1", + "@types/node": "^10.12.24", "discord.js-docgen": "discordjs/docgen", - "eslint": "^5.4.0", + "eslint": "^5.13.0", "json-filter-loader": "^1.0.0", - "terser-webpack-plugin": "^1.1.0", - "tslint": "^5.11.0", + "terser-webpack-plugin": "^1.2.2", + "tslint": "^5.12.1", "tslint-config-typings": "^0.3.1", - "typescript": "^3.0.1", - "webpack": "^4.17.0", - "webpack-cli": "^3.1.0" + "typescript": "^3.3.3", + "webpack": "^4.29.3", + "webpack-cli": "^3.2.3" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" }, "browser": { "https": false, From ab55acffdbbb50c6a28c456984165cd1fb99f4c2 Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 11 Feb 2019 17:29:20 +0100 Subject: [PATCH 050/428] webpack: Fix module warning when building (#3076) Warning: ``` WARNING in ./src/client/Client.js Module not found: Error: Can't resolve 'worker_threads' in 'D:\Repositories\discordjs\discord.js\src\client' @ ./src/client/Client.js @ ./src/index.js ``` --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c1db1698..41385f9d 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "node-opus": false, "tweetnacl": false, "sodium": false, + "worker_threads": false, "zlib-sync": false, "src/sharding/Shard.js": false, "src/sharding/ShardClientUtil.js": false, From a705edfd0df4de0a7e6df59cf709132d7e0e8da2 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 17:22:17 +0000 Subject: [PATCH 051/428] voice: more debug information --- src/client/actions/VoiceStateUpdate.js | 1 + src/client/voice/ClientVoiceManager.js | 4 ++-- src/client/voice/VoiceConnection.js | 5 +++-- src/client/websocket/handlers/VOICE_SERVER_UPDATE.js | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index e4efa129..7e4d3514 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -26,6 +26,7 @@ class VoiceStateUpdate extends Action { // Emit event if (member && member.user.id === client.user.id && data.channel_id) { + client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); client.emit('self.voiceStateUpdate', data); } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index fce2d3b6..ef3bb132 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -64,6 +64,8 @@ class ClientVoiceManager { return; } else { connection = new VoiceConnection(this, channel); + connection.on('debug', msg => this.client.emit('debug', `[VOICE (${channel.guild.id})]: ${msg}`)); + connection.authenticate(); this.connections.set(channel.guild.id, connection); } @@ -72,8 +74,6 @@ class ClientVoiceManager { reject(reason); }); - connection.on('debug', msg => this.client.emit('debug', `[VOICE (${channel.guild.id})]: ${msg}`)); - connection.once('authenticated', () => { connection.once('ready', () => { resolve(connection); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 4d5c23a8..da3f8f16 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -125,8 +125,6 @@ class VoiceConnection extends EventEmitter { * @type {VoiceReceiver} */ this.receiver = new VoiceReceiver(this); - - this.authenticate(); } /** @@ -180,6 +178,9 @@ class VoiceConnection extends EventEmitter { self_deaf: false, }, options); + const queueLength = this.channel.guild.shard.ratelimit.queue.length; + this.emit('debug', `Sending voice state update (queue length is ${queueLength}): ${JSON.stringify(options)}`); + this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js index 2663d99a..563db1ea 100644 --- a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -1,5 +1,6 @@ 'use strict'; module.exports = (client, packet) => { + client.emit('debug', `[VOICE] received voice server: ${JSON.stringify(packet)}`); client.emit('self.voiceServer', packet.d); }; From 283fc54ce49b5a0869bf7dece5e788d738b465d1 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 17:39:19 +0000 Subject: [PATCH 052/428] More debug information --- src/client/voice/ClientVoiceManager.js | 3 ++- src/client/voice/VoiceConnection.js | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index ef3bb132..52b14ed1 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -64,7 +64,8 @@ class ClientVoiceManager { return; } else { connection = new VoiceConnection(this, channel); - connection.on('debug', msg => this.client.emit('debug', `[VOICE (${channel.guild.id})]: ${msg}`)); + connection.on('debug', msg => + this.client.emit('debug', `[VOICE (${channel.guild.id}:${connection.status})]: ${msg}`)); connection.authenticate(); this.connections.set(channel.guild.id, connection); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index da3f8f16..6e07c92d 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -199,6 +199,7 @@ class VoiceConnection extends EventEmitter { // Signifies awaiting endpoint stage return; } + this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`); if (!token) { this.authenticateFailed('VOICE_TOKEN_ABSENT'); @@ -206,6 +207,7 @@ class VoiceConnection extends EventEmitter { } endpoint = endpoint.match(/([^:]*)/)[0]; + this.emit('debug', `Endpoint resolved as ${endpoint}`); if (!endpoint) { this.authenticateFailed('VOICE_INVALID_ENDPOINT'); @@ -252,7 +254,7 @@ class VoiceConnection extends EventEmitter { */ checkAuthenticated() { const { token, endpoint, sessionID } = this.authentication; - + this.emit('debug', `Authenticated with sessionID ${sessionID}`); if (token && endpoint && sessionID) { this.status = VoiceStatus.CONNECTING; /** @@ -271,6 +273,7 @@ class VoiceConnection extends EventEmitter { */ authenticateFailed(reason) { clearTimeout(this.connectTimeout); + this.emit('debug', `Authenticate failed - ${reason}`); if (this.status === VoiceStatus.AUTHENTICATING) { /** * Emitted when we fail to initiate a voice connection. @@ -320,6 +323,7 @@ class VoiceConnection extends EventEmitter { this.authentication.endpoint = endpoint; this.speaking = new Speaking().freeze(); this.status = VoiceStatus.RECONNECTING; + this.emit('debug', `Reconnecting to ${endpoint}`); /** * Emitted when the voice connection is reconnecting (typically after a region change). * @event VoiceConnection#reconnecting @@ -333,6 +337,7 @@ class VoiceConnection extends EventEmitter { */ disconnect() { this.emit('closing'); + this.emit('debug', 'disconnect() triggered'); clearTimeout(this.connectTimeout); const conn = this.voiceManager.connections.get(this.channel.guild.id); if (conn === this) this.voiceManager.connections.delete(this.channel.guild.id); @@ -366,6 +371,8 @@ class VoiceConnection extends EventEmitter { this.speaking = new Speaking().freeze(); const { ws, udp } = this.sockets; + this.emit('debug', 'Connection clean up'); + if (ws) { ws.removeAllListeners('error'); ws.removeAllListeners('ready'); @@ -384,6 +391,7 @@ class VoiceConnection extends EventEmitter { * @private */ connect() { + this.emit('debug', `Connect triggered`); if (this.status !== VoiceStatus.RECONNECTING) { if (this.sockets.ws) throw new Error('WS_CONNECTION_EXISTS'); if (this.sockets.udp) throw new Error('UDP_CONNECTION_EXISTS'); From 810b5a5f7175cb916ed3b9fea23ebf623eed8147 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 17:57:56 +0000 Subject: [PATCH 053/428] voice: more debug info --- src/client/voice/ClientVoiceManager.js | 2 ++ src/client/voice/VoiceConnection.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 52b14ed1..e65effdd 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -28,12 +28,14 @@ class ClientVoiceManager { } onVoiceServer({ guild_id, token, endpoint }) { + this.client.emit('debug', `[VOICE] voiceServer guild: ${guild_id} token: ${token} endpoint: ${endpoint}`); const connection = this.connections.get(guild_id); if (connection) connection.setTokenAndEndpoint(token, endpoint); } onVoiceStateUpdate({ guild_id, session_id, channel_id }) { const connection = this.connections.get(guild_id); + this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`); if (!connection) return; if (!channel_id && connection.status !== VoiceStatus.DISCONNECTED) { connection._disconnect(); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6e07c92d..6a945731 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -195,11 +195,11 @@ class VoiceConnection extends EventEmitter { * @returns {void} */ setTokenAndEndpoint(token, endpoint) { + this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`); if (!endpoint) { // Signifies awaiting endpoint stage return; } - this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`); if (!token) { this.authenticateFailed('VOICE_TOKEN_ABSENT'); @@ -229,6 +229,7 @@ class VoiceConnection extends EventEmitter { * @private */ setSessionID(sessionID) { + this.emit('debug', `Setting sessionID ${sessionID} (stored as "${this.authentication.sessionID}")`); if (!sessionID) { this.authenticateFailed('VOICE_SESSION_ABSENT'); return; From 375706beac3219ae20885681112289bb2e0c6604 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 18:03:35 +0000 Subject: [PATCH 054/428] voice: replace self.xyz events --- src/client/actions/VoiceStateUpdate.js | 2 +- src/client/voice/ClientVoiceManager.js | 3 --- src/client/websocket/handlers/VOICE_SERVER_UPDATE.js | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 7e4d3514..045ca931 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -27,7 +27,7 @@ class VoiceStateUpdate extends Action { // Emit event if (member && member.user.id === client.user.id && data.channel_id) { client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); - client.emit('self.voiceStateUpdate', data); + client.voice.onVoiceStateUpdate(data); } /** diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index e65effdd..466b6d66 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -22,9 +22,6 @@ class ClientVoiceManager { * @type {Collection} */ this.connections = new Collection(); - - this.client.on('self.voiceServer', this.onVoiceServer.bind(this)); - this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); } onVoiceServer({ guild_id, token, endpoint }) { diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js index 563db1ea..f9cf5343 100644 --- a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -2,5 +2,5 @@ module.exports = (client, packet) => { client.emit('debug', `[VOICE] received voice server: ${JSON.stringify(packet)}`); - client.emit('self.voiceServer', packet.d); + client.voice.onVoiceServer(packet.d); }; From fe51b4e89b2f4ea256fb20ad44810e7f64ac8b53 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 18:28:37 +0000 Subject: [PATCH 055/428] voice: more debug information, correctly listen to vws --- src/client/voice/VoiceConnection.js | 2 ++ src/client/voice/networking/VoiceWebSocket.js | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6a945731..d0922ae8 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -413,6 +413,8 @@ class VoiceConnection extends EventEmitter { ws.on('ready', this.onReady.bind(this)); ws.on('sessionDescription', this.onSessionDescription.bind(this)); ws.on('speaking', this.onSpeaking.bind(this)); + + this.sockets.ws.connect(); } /** diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index ea2db439..e238c62b 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -25,7 +25,6 @@ class VoiceWebSocket extends EventEmitter { */ this.attempts = 0; - this.connect(); this.dead = false; this.connection.on('closing', this.shutdown.bind(this)); } @@ -69,7 +68,7 @@ class VoiceWebSocket extends EventEmitter { } this.attempts++; - this.emit('debug', `[WS] connecting with ${this.attempts} attempts`); + this.emit('debug', `[WS] connecting, ${this.attempts} attempts, wss://${this.connection.authentication.endpoint}/`); /** * The actual WebSocket used to connect to the Voice WebSocket Server. From c362ba0dd549ac0a2be850b81966ffa29b030db5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 18:37:33 +0000 Subject: [PATCH 056/428] voice: more debug --- src/client/voice/networking/VoiceWebSocket.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index e238c62b..3720afa7 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -68,13 +68,13 @@ class VoiceWebSocket extends EventEmitter { } this.attempts++; - this.emit('debug', `[WS] connecting, ${this.attempts} attempts, wss://${this.connection.authentication.endpoint}/`); /** * The actual WebSocket used to connect to the Voice WebSocket Server. * @type {WebSocket} */ this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 4 }); + this.emit('debug', `[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`); this.ws.onopen = this.onOpen.bind(this); this.ws.onmessage = this.onMessage.bind(this); this.ws.onclose = this.onClose.bind(this); @@ -154,6 +154,7 @@ class VoiceWebSocket extends EventEmitter { * @param {Error} error The error that occurred */ onError(error) { + this.emit('debug', `[WS] Error: ${error}`); this.emit('error', error); } From 0c7a618f1017bf6cf807562799c606ec72fce140 Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 11 Feb 2019 20:33:13 +0100 Subject: [PATCH 057/428] cleanup: remove unused members of classes (#3078) * cleanup: Remove unused members of classes * typings: Remove hit from Message --- src/errors/Messages.js | 2 -- src/structures/DMChannel.js | 1 - src/structures/GroupDMChannel.js | 1 - src/structures/Message.js | 6 ------ src/structures/TextChannel.js | 1 - typings/index.d.ts | 1 - 6 files changed, 12 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 792c2b8b..443315a7 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -81,8 +81,6 @@ const Messages = { PRUNE_DAYS_TYPE: 'Days must be a number', - SEARCH_CHANNEL_TYPE: 'Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.', - GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 5549618a..7c3db02e 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -59,7 +59,6 @@ class DMChannel extends Channel { get lastMessage() {} get lastPinAt() {} send() {} - search() {} startTyping() {} stopTyping() {} get typing() {} diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 6f9aca39..39fb5646 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -229,7 +229,6 @@ class GroupDMChannel extends Channel { get lastMessage() {} get lastPinAt() {} send() {} - search() {} startTyping() {} stopTyping() {} get typing() {} diff --git a/src/structures/Message.js b/src/structures/Message.js index 1947705e..310706e1 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -153,12 +153,6 @@ class Message extends Base { type: data.activity.type, } : null; - /** - * Whether this message is a hit in a search - * @type {?boolean} - */ - this.hit = typeof data.hit === 'boolean' ? data.hit : null; - /** * The previous versions of the message, sorted with the most recent first * @type {Message[]} diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 47c8e19c..a643815e 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -127,7 +127,6 @@ class TextChannel extends GuildChannel { get lastMessage() {} get lastPinAt() {} send() {} - search() {} startTyping() {} stopTyping() {} get typing() {} diff --git a/typings/index.d.ts b/typings/index.d.ts index 1b1d17fc..8c606400 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -661,7 +661,6 @@ declare module 'discord.js' { public readonly edits: Message[]; public embeds: MessageEmbed[]; public readonly guild: Guild; - public hit: boolean; public id: Snowflake; public readonly member: GuildMember; public mentions: MessageMentions; From 5c19ad72525188b44d1c26ba59424a43e763c266 Mon Sep 17 00:00:00 2001 From: Kyra Date: Tue, 12 Feb 2019 09:13:15 +0100 Subject: [PATCH 058/428] cleanup: Error messages cleanup (#3079) Removed keys that return zero occurrences in the library's source code --- src/errors/Messages.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 443315a7..5945c167 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -15,12 +15,8 @@ const Messages = { BITFIELD_INVALID: 'Invalid bitfield flag or number.', - RATELIMIT_INVALID_METHOD: 'Unknown rate limiting method.', - SHARDING_INVALID: 'Invalid shard settings were provided.', 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.`, @@ -41,7 +37,6 @@ const Messages = { VOICE_INVALID_HEARTBEAT: 'Tried to set voice heartbeat but no valid interval was specified.', VOICE_USER_MISSING: 'Couldn\'t resolve the user to create stream.', - VOICE_STREAM_EXISTS: 'There is already an existing stream for that user.', VOICE_JOIN_CHANNEL: (full = false) => `You do not have permission to join this voice channel${full ? '; it is full.' : '.'}`, VOICE_CONNECTION_TIMEOUT: 'Connection not established within 15 seconds.', @@ -57,19 +52,15 @@ const Messages = { VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.', - OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', - UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', UDP_ADDRESS_MALFORMED: 'Malformed UDP address or port.', UDP_CONNECTION_EXISTS: 'There is already an existing UDP connection.', - REQ_BODY_TYPE: 'The response body isn\'t a Buffer.', REQ_RESOURCE_TYPE: 'The resource must be a string, Buffer or a valid file stream.', IMAGE_FORMAT: format => `Invalid image format: ${format}`, IMAGE_SIZE: size => `Invalid image size: ${size}`, - MESSAGE_MISSING: 'Message not found', MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.', MESSAGE_NONCE_TYPE: 'Message nonce must fit in an unsigned 64-bit integer.', @@ -85,7 +76,6 @@ const Messages = { GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', GUILD_OWNED: 'Guild is owned by the client.', - GUILD_RESTRICTED: (state = false) => `Guild is ${state ? 'already' : 'not'} restricted.`, GUILD_MEMBERS_TIMEOUT: 'Members didn\'t arrive in time.', INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, From 8910fed7298d5fb3d9b97d979bd1f0e0bd901145 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 12 Feb 2019 16:29:11 +0000 Subject: [PATCH 059/428] voice: debug UDP (#3044) --- src/client/voice/networking/VoiceUDPClient.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index 6c5182d9..3c493506 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -88,11 +88,19 @@ class VoiceConnectionUDPClient extends EventEmitter { }); } - createUDPSocket(address) { + async createUDPSocket(address) { this.discordAddress = address; const socket = this.socket = udp.createSocket('udp4'); + socket.on('error', e => { + this.emit('debug', `[UDP] Error: ${e}`); + this.emit('error', e); + }); + socket.on('close', () => { + this.emit('debug', '[UDP] socket closed'); + }); this.emit('debug', `[UDP] created socket`); socket.once('message', message => { + this.emit('debug', `[UDP] message: [${[...message]}] (${message})`); // Stop if the sockets have been deleted because the connection has been closed already if (!this.voiceConnection.sockets.ws) return; @@ -125,7 +133,9 @@ class VoiceConnectionUDPClient extends EventEmitter { const blankMessage = Buffer.alloc(70); blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4); - this.send(blankMessage); + this.emit('debug', `Sending IP discovery packet: [${[...blankMessage]}]`); + await this.send(blankMessage); + this.emit('debug', `Successfully sent IP discovery packet`); } } From 5c3f5d704840fca3888264e19eb38d88977304ea Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 13 Feb 2019 17:39:39 +0000 Subject: [PATCH 060/428] Partials (#3070) * Remove GroupDMChannels they sparked no joy * Start partials for message deletion * MessageUpdate partials * Add partials as an opt-in client option * Add fetch() to Message * Message.author should never be undefined * Fix channels being the wrong type * Allow fetching channels * Refactor and add reaction add partials * Reaction remove partials * Check for emoji first * fix message fetching janky * User partials in audit logs * refactor overwrite code * guild member partials * partials as a whitelist * document GuildMember#fetch * fix: check whether a structure is a partial, not whether cache is true * typings: Updated for latest commit (#3075) * partials: fix messageUpdate behaviour (now "old" message can be partial) * partials: add warnings and docs * partials: add partials to index.yml * partials: tighten "partial" definitions * partials: fix embed-only messages counting as partials --- docs/index.yml | 2 + docs/topics/partials.md | 61 +++++ src/client/Client.js | 3 + src/client/actions/Action.js | 23 ++ src/client/actions/ChannelCreate.js | 2 +- src/client/actions/ChannelDelete.js | 2 +- src/client/actions/MessageDelete.js | 5 +- src/client/actions/MessageReactionAdd.js | 10 +- src/client/actions/MessageReactionRemove.js | 10 +- .../actions/MessageReactionRemoveAll.js | 6 +- src/client/actions/MessageUpdate.js | 7 +- .../websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 +- .../websocket/handlers/CHANNEL_UPDATE.js | 4 +- src/index.js | 1 - src/stores/ChannelStore.js | 10 +- src/stores/DataStore.js | 1 + src/stores/GuildMemberStore.js | 2 +- src/stores/MessageStore.js | 15 +- src/stores/UserStore.js | 8 +- src/structures/APIMessage.js | 2 +- src/structures/Channel.js | 14 +- src/structures/ClientUser.js | 36 --- src/structures/DMChannel.js | 22 +- src/structures/GroupDMChannel.js | 245 ------------------ src/structures/Guild.js | 6 +- src/structures/GuildAuditLogs.js | 15 +- src/structures/GuildMember.js | 18 +- src/structures/Message.js | 32 ++- src/structures/MessageCollector.js | 2 +- src/structures/User.js | 18 ++ src/util/Constants.js | 21 ++ src/util/Structures.js | 1 - src/util/Util.js | 4 +- test/voice.js | 11 +- typings/index.d.ts | 57 ++-- 35 files changed, 295 insertions(+), 383 deletions(-) create mode 100644 docs/topics/partials.md delete mode 100644 src/structures/GroupDMChannel.js diff --git a/docs/index.yml b/docs/index.yml index 7c5b3b28..17578057 100644 --- a/docs/index.yml +++ b/docs/index.yml @@ -12,6 +12,8 @@ path: voice.md - name: Web builds path: web.md + - name: Partials + path: partials.md - name: Examples files: - name: Ping diff --git a/docs/topics/partials.md b/docs/topics/partials.md new file mode 100644 index 00000000..2566f3de --- /dev/null +++ b/docs/topics/partials.md @@ -0,0 +1,61 @@ +# Partials + +Partials allow you to receive events that contain uncached instances, providing structures that contain very minimal +data. For example, if you were to receive a `messageDelete` event with an uncached message, normally Discord.js would +discard the event. With partials, you're able to receive the event, with a Message object that contains just an ID. + +## Opting in + +Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType): + +```js +// Accept partial messages and DM channels when emitting events +new Client({ partials: ['MESSAGE', 'CHANNEL'] }); +``` + +## Usage & warnings + +The only guaranteed data a partial structure can store is its ID. All other properties/methods should be +considered invalid/defunct while accessing a partial structure. + +After opting-in with the above, you begin to allow partial messages and channels in your caches, so it's important +to check whether they're safe to access whenever you encounter them, whether it be in events or through normal cache +usage. + +All instance of structures that you opted-in for will have a `partial` property. As you'd expect, this value is `true` +when the instance is partial. Partial structures are only guaranteed to contain an ID, any other properties and methods +no longer carry their normal type guarantees. + +This means you have to take time to consider possible parts of your program that might need checks put in place to +prevent accessing partial data: + +```js +client.on('messageDelete', message => { + console.log(`${message.id} was deleted!`); + // Partial messages do not contain any content so skip them + if (!message.partial) { + console.log(`It had content: "${message.content}"`); + } +}) + +// You can also try to upgrade partials to full instances: +client.on('messageReactionAdd', async (reaction, user) => { + // If a message gains a reaction and it is uncached, fetch and cache the message + // You should account for any errors while fetching, it could return API errors if the resource is missing + if (reaction.message.partial) await reaction.message.fetch(); + // Now the message has been cached and is fully available: + console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`); +}); +``` + +If a message is deleted and both the message and channel are uncached, you must enable both 'MESSAGE' and +'CHANNEL' in the client options to receive the messageDelete event. + +## Why? + +This allows developers to listen to events that contain uncached data, which is useful if you're running a moderation +bot or any bot that relies on still receiving updates to resources you don't have cached -- message reactions are a +good example. + +Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should +not be cached. \ No newline at end of file diff --git a/src/client/Client.js b/src/client/Client.js index 4e5f5e24..b1dc1557 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -434,6 +434,9 @@ class Client extends BaseClient { if (typeof options.disableEveryone !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean'); } + if (!(options.partials instanceof Array)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); + } if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number'); } diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 791eaa00..09faae9e 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -1,5 +1,7 @@ 'use strict'; +const { PartialTypes } = require('../../util/Constants'); + /* ABOUT ACTIONS @@ -20,6 +22,27 @@ class GenericAction { handle(data) { return data; } + + getChannel(data) { + const id = data.channel_id || data.id; + return data.channel || (this.client.options.partials.includes(PartialTypes.CHANNEL) ? + this.client.channels.add({ + id, + guild_id: data.guild_id, + }) : + this.client.channels.get(id)); + } + + getMessage(data, channel) { + const id = data.message_id || data.id; + return data.message || (this.client.options.partials.includes(PartialTypes.MESSAGE) ? + channel.messages.add({ + id, + channel_id: channel.id, + guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), + }) : + channel.messages.get(id)); + } } module.exports = GenericAction; diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 4a9d17d4..6830f2ab 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -12,7 +12,7 @@ class ChannelCreateAction extends Action { /** * Emitted whenever a channel is created. * @event Client#channelCreate - * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was created + * @param {DMChannel|GuildChannel} channel The channel that was created */ client.emit(Events.CHANNEL_CREATE, channel); } diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index b9909f8b..9fc0e6d9 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -19,7 +19,7 @@ class ChannelDeleteAction extends Action { /** * Emitted whenever a channel is deleted. * @event Client#channelDelete - * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was deleted + * @param {DMChannel|GuildChannel} channel The channel that was deleted */ client.emit(Events.CHANNEL_DELETE, channel); } diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index d66d10a2..feb118c1 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -6,11 +6,10 @@ const { Events } = require('../../util/Constants'); class MessageDeleteAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); + const channel = this.getChannel(data); let message; - if (channel) { - message = channel.messages.get(data.id); + message = this.getMessage(data, channel); if (message) { channel.messages.delete(message.id); message.deleted = true; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index a800c3c7..b7400cf7 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -11,15 +11,19 @@ const Action = require('./Action'); class MessageReactionAdd extends Action { handle(data) { + if (!data.emoji) return false; + const user = data.user || this.client.users.get(data.user_id); if (!user) return false; + // Verify channel - const channel = data.channel || this.client.channels.get(data.channel_id); + const channel = this.getChannel(data); if (!channel || channel.type === 'voice') return false; + // Verify message - const message = data.message || channel.messages.get(data.message_id); + const message = this.getMessage(data, channel); if (!message) return false; - if (!data.emoji) return false; + // Verify reaction const reaction = message.reactions.add({ emoji: data.emoji, diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 7ab5be28..e5d4f841 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -12,15 +12,19 @@ const { Events } = require('../../util/Constants'); class MessageReactionRemove extends Action { handle(data) { + if (!data.emoji) return false; + const user = this.client.users.get(data.user_id); if (!user) return false; + // Verify channel - const channel = this.client.channels.get(data.channel_id); + const channel = this.getChannel(data); if (!channel || channel.type === 'voice') return false; + // Verify message - const message = channel.messages.get(data.message_id); + const message = this.getMessage(data, channel); if (!message) return false; - if (!data.emoji) return false; + // Verify reaction const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); const reaction = message.reactions.get(emojiID); diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js index f3273097..0921ce50 100644 --- a/src/client/actions/MessageReactionRemoveAll.js +++ b/src/client/actions/MessageReactionRemoveAll.js @@ -5,10 +5,12 @@ const { Events } = require('../../util/Constants'); class MessageReactionRemoveAll extends Action { handle(data) { - const channel = this.client.channels.get(data.channel_id); + // Verify channel + const channel = this.getChannel(data); if (!channel || channel.type === 'voice') return false; - const message = channel.messages.get(data.message_id); + // Verify message + const message = this.getMessage(data, channel); if (!message) return false; message.reactions.clear(); diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js index be26b269..07e2aacb 100644 --- a/src/client/actions/MessageUpdate.js +++ b/src/client/actions/MessageUpdate.js @@ -4,11 +4,10 @@ const Action = require('./Action'); class MessageUpdateAction extends Action { handle(data) { - const client = this.client; - - const channel = client.channels.get(data.channel_id); + const channel = this.getChannel(data); if (channel) { - const message = channel.messages.get(data.id); + const { id, channel_id, guild_id, author, timestamp, type } = data; + const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel); if (message) { message.patch(data); return { diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index dfc854e3..11154674 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -14,7 +14,7 @@ module.exports = (client, { d: data }) => { * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, * not much information can be provided easily here - you need to manually check the pins yourself. * @event Client#channelPinsUpdate - * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occured in + * @param {DMChannel|TextChannel} channel The channel that the pins update occured in * @param {Date} time The time of the pins update */ client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js index b437ebbd..7a0df486 100644 --- a/src/client/websocket/handlers/CHANNEL_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -8,8 +8,8 @@ module.exports = (client, packet) => { /** * Emitted whenever a channel is updated - e.g. name change, topic change. * @event Client#channelUpdate - * @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update - * @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update + * @param {DMChannel|GuildChannel} oldChannel The channel before the update + * @param {DMChannel|GuildChannel} newChannel The channel after the update */ client.emit(Events.CHANNEL_UPDATE, old, updated); } diff --git a/src/index.js b/src/index.js index d9b8cb45..b3a92f60 100644 --- a/src/index.js +++ b/src/index.js @@ -65,7 +65,6 @@ module.exports = { Collector: require('./structures/interfaces/Collector'), DMChannel: require('./structures/DMChannel'), Emoji: require('./structures/Emoji'), - GroupDMChannel: require('./structures/GroupDMChannel'), Guild: require('./structures/Guild'), GuildAuditLogs: require('./structures/GuildAuditLogs'), GuildChannel: require('./structures/GuildChannel'), diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 9ce6465c..88c5a0bb 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -5,7 +5,7 @@ const Channel = require('../structures/Channel'); const { Events } = require('../util/Constants'); const kLru = Symbol('LRU'); -const lruable = ['group', 'dm']; +const lruable = ['dm']; /** * Stores channels. @@ -54,6 +54,7 @@ class ChannelStore extends DataStore { add(data, guild, cache = true) { const existing = this.get(data.id); + if (existing && existing.partial && cache) existing._patch(data); if (existing) return existing; const channel = Channel.create(this.client, data, guild); @@ -85,11 +86,12 @@ class ChannelStore extends DataStore { * .then(channel => console.log(channel.name)) * .catch(console.error); */ - fetch(id, cache = true) { + async fetch(id, cache = true) { const existing = this.get(id); - if (existing) return Promise.resolve(existing); + if (existing && !existing.partial) return existing; - return this.client.api.channels(id).get().then(data => this.add(data, null, cache)); + const data = await this.client.api.channels(id).get(); + return this.add(data, null, cache); } /** diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index fc841899..ade8c70f 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -18,6 +18,7 @@ class DataStore extends Collection { add(data, cache = true, { id, extras = [] } = {}) { const existing = this.get(id || data.id); + if (existing && existing.partial && cache && existing._patch) existing._patch(data); if (existing) return existing; const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 039fb4c7..397c119b 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -180,7 +180,7 @@ class GuildMemberStore extends DataStore { _fetchSingle({ user, cache }) { const existing = this.get(user); - if (existing && existing.joinedTimestamp) return Promise.resolve(existing); + if (existing && !existing.partial) return Promise.resolve(existing); return this.client.api.guilds(this.guild.id).members(user).get() .then(data => this.add(data, cache)); } diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index 1a8a7083..52bf7fdd 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -40,6 +40,7 @@ class MessageStore extends DataStore { * The returned Collection does not contain reaction users of the messages if they were not cached. * Those need to be fetched separately in such a case. * @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters. + * @param {boolean} [cache=true] Whether to cache the message(s) * @returns {Promise|Promise>} * @example * // Get message @@ -57,8 +58,8 @@ class MessageStore extends DataStore { * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) * .catch(console.error); */ - fetch(message) { - return typeof message === 'string' ? this._fetchId(message) : this._fetchMany(message); + fetch(message, cache = true) { + return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache); } /** @@ -80,15 +81,17 @@ class MessageStore extends DataStore { }); } - async _fetchId(messageID) { + async _fetchId(messageID, cache) { + const existing = this.get(messageID); + if (existing && !existing.partial) return existing; const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); - return this.add(data); + return this.add(data, cache); } - async _fetchMany(options = {}) { + async _fetchMany(options = {}, cache) { const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message)); + for (const message of data) messages.set(message.id, this.add(message, cache)); return messages; } diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 8fd2d9d5..20c25d05 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -51,11 +51,11 @@ class UserStore extends DataStore { * @param {boolean} [cache=true] Whether to cache the new user object if it isn't already * @returns {Promise} */ - fetch(id, cache = true) { + async fetch(id, cache = true) { const existing = this.get(id); - if (existing) return Promise.resolve(existing); - - return this.client.api.users(id).get().then(data => this.add(data, cache)); + if (existing && !existing.partial) return existing; + const data = await this.client.api.users(id).get(); + return this.add(data, cache); } } diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index deca5d26..41f98383 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -330,7 +330,7 @@ module.exports = APIMessage; /** * A target for a message. - * @typedef {TextChannel|DMChannel|GroupDMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget + * @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget */ /** diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 92914118..b4341194 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -16,7 +16,6 @@ class Channel extends Base { /** * The type of the channel, either: * * `dm` - a DM channel - * * `group` - a Group DM channel * * `text` - a guild text channel * * `voice` - a guild voice channel * * `category` - a guild category channel @@ -84,15 +83,20 @@ class Channel extends Base { return this.client.api.channels(this.id).delete().then(() => this); } + /** + * Fetches this channel. + * @returns {Promise} + */ + fetch() { + return this.client.channels.fetch(this.id, true); + } + static create(client, data, guild) { const Structures = require('../util/Structures'); let channel; - if (data.type === ChannelTypes.DM) { + if (data.type === ChannelTypes.DM || (data.type !== ChannelTypes.GROUP && !data.guild_id && !guild)) { 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) { diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 3ec32e9d..a86a6cb5 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -164,42 +164,6 @@ class ClientUser extends Structures.get('User') { setAFK(afk) { return this.setPresence({ afk }); } - - /** - * An object containing either a user or access token, and an optional nickname. - * @typedef {Object} GroupDMRecipientOptions - * @property {UserResolvable} [user] User to add to the Group DM - * @property {string} [accessToken] Access token to use to add a user to the Group DM - * (only available if a bot is creating the DM) - * @property {string} [nick] Permanent nickname (only available if a bot is creating the DM) - * @property {string} [id] If no user resolvable is provided and you want to assign nicknames - * you must provide user ids instead - */ - - /** - * Creates a Group DM. - * @param {GroupDMRecipientOptions[]} recipients The recipients - * @returns {Promise} - * @example - * // Create a Group DM with a token provided from OAuth - * client.user.createGroupDM([{ - * user: '66564597481480192', - * accessToken: token - * }]) - * .then(console.log) - * .catch(console.error); - */ - createGroupDM(recipients) { - const data = this.bot ? { - access_tokens: recipients.map(u => u.accessToken), - nicks: recipients.reduce((o, r) => { - if (r.nick) o[r.user ? r.user.id : r.id] = r.nick; - return o; - }, {}), - } : { recipients: recipients.map(u => this.client.users.resolveID(u.user || u.id)) }; - return this.client.api.users('@me').channels.post({ data }) - .then(res => this.client.channels.add(res)); - } } module.exports = ClientUser; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 7c3db02e..f1280e80 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -12,6 +12,8 @@ const MessageStore = require('../stores/MessageStore'); class DMChannel extends Channel { constructor(client, data) { super(client, data); + // Override the channel type so partials have a known type + this.type = 'dm'; /** * A collection containing the messages sent to this channel * @type {MessageStore} @@ -23,11 +25,13 @@ class DMChannel extends Channel { _patch(data) { super._patch(data); - /** - * The recipient on the other end of the DM - * @type {User} - */ - this.recipient = this.client.users.add(data.recipients[0]); + if (data.recipients) { + /** + * The recipient on the other end of the DM + * @type {User} + */ + this.recipient = this.client.users.add(data.recipients[0]); + } /** * The ID of the last message in the channel, if one was sent @@ -42,6 +46,14 @@ class DMChannel extends Channel { this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; } + /** + * Whether this DMChannel is a partial + * @type {boolean} + */ + get partial() { + return !this.recipient; + } + /** * When concatenated with a string, this automatically returns the recipient's mention instead of the * DMChannel object. diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js deleted file mode 100644 index 39fb5646..00000000 --- a/src/structures/GroupDMChannel.js +++ /dev/null @@ -1,245 +0,0 @@ -'use strict'; - -const Channel = require('./Channel'); -const TextBasedChannel = require('./interfaces/TextBasedChannel'); -const Collection = require('../util/Collection'); -const DataResolver = require('../util/DataResolver'); -const MessageStore = require('../stores/MessageStore'); - -/* -{ type: 3, - recipients: - [ { username: 'Charlie', - id: '123', - discriminator: '6631', - avatar: '123' }, - { username: 'Ben', - id: '123', - discriminator: '2055', - avatar: '123' }, - { username: 'Adam', - id: '123', - discriminator: '2406', - avatar: '123' } ], - owner_id: '123', - name: null, - last_message_id: '123', - id: '123', - icon: null } -*/ - -/** - * Represents a Group DM on Discord. - * @extends {Channel} - * @implements {TextBasedChannel} - */ -class GroupDMChannel extends Channel { - constructor(client, data) { - super(client, data); - /** - * A collection containing the messages sent to this channel - * @type {MessageStore} - */ - this.messages = new MessageStore(this); - this._typing = new Map(); - } - - _patch(data) { - super._patch(data); - - /** - * The name of this Group DM, can be null if one isn't set - * @type {string} - */ - this.name = data.name; - - /** - * A hash of this Group DM icon - * @type {?string} - */ - this.icon = data.icon; - - /** - * The user ID of this Group DM's owner - * @type {Snowflake} - */ - this.ownerID = data.owner_id; - - /** - * If the DM is managed by an application - * @type {boolean} - */ - this.managed = data.managed; - - /** - * Application ID of the application that made this Group DM, if applicable - * @type {?Snowflake} - */ - this.applicationID = data.application_id; - - if (data.nicks) { - /** - * Nicknames for group members - * @type {?Collection} - */ - this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick])); - } - - if (!this.recipients) { - /** - * A collection of the recipients of this DM, mapped by their ID - * @type {Collection} - */ - this.recipients = new Collection(); - } - - if (data.recipients) { - for (const recipient of data.recipients) { - const user = this.client.users.add(recipient); - this.recipients.set(user.id, user); - } - } - - /** - * The ID of the last message in the channel, if one was sent - * @type {?Snowflake} - */ - this.lastMessageID = data.last_message_id; - - /** - * The timestamp when the last pinned message was pinned, if there was one - * @type {?number} - */ - this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; - } - - /** - * The owner of this Group DM - * @type {?User} - * @readonly - */ - get owner() { - return this.client.users.get(this.ownerID) || null; - } - - /** - * Gets the URL to this Group DM's icon. - * @param {ImageURLOptions} [options={}] Options for the Image URL - * @returns {?string} - */ - iconURL({ format, size } = {}) { - if (!this.icon) return null; - return this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size); - } - - /** - * Whether this channel equals another channel. It compares all properties, so for most operations - * it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often - * what most users need. - * @param {GroupDMChannel} channel Channel to compare with - * @returns {boolean} - */ - equals(channel) { - const equal = channel && - this.id === channel.id && - this.name === channel.name && - this.icon === channel.icon && - this.ownerID === channel.ownerID; - - if (equal) { - return this.recipients.equals(channel.recipients); - } - - return equal; - } - - /** - * Edits this Group DM. - * @param {Object} data New data for this Group DM - * @param {string} [reason] Reason for editing this Group DM - * @returns {Promise} - */ - edit(data, reason) { - return this.client.api.channels[this.id].patch({ - data: { - icon: data.icon, - name: data.name === null ? null : data.name || this.name, - }, - reason, - }).then(() => this); - } - - /** - * Sets a new icon for this Group DM. - * @param {Base64Resolvable|BufferResolvable} icon The new icon of this Group DM - * @returns {Promise} - */ - async setIcon(icon) { - return this.edit({ icon: await DataResolver.resolveImage(icon) }); - } - - /** - * Sets a new name for this Group DM. - * @param {string} name New name for this Group DM - * @returns {Promise} - */ - setName(name) { - return this.edit({ name }); - } - - /** - * Adds a user to this Group DM. - * @param {Object} options Options for this method - * @param {UserResolvable} options.user User to add to this Group DM - * @param {string} [options.accessToken] Access token to use to add the user to this Group DM - * @param {string} [options.nick] Permanent nickname to give the user - * @returns {Promise} - */ - addUser({ user, accessToken, nick }) { - const id = this.client.users.resolveID(user); - return this.client.api.channels[this.id].recipients[id].put({ nick, access_token: accessToken }) - .then(() => this); - } - - /** - * Removes a user from this Group DM. - * @param {UserResolvable} user User to remove - * @returns {Promise} - */ - removeUser(user) { - const id = this.client.users.resolveID(user); - return this.client.api.channels[this.id].recipients[id].delete() - .then(() => this); - } - - /** - * When concatenated with a string, this automatically returns the channel's name instead of the - * GroupDMChannel object. - * @returns {string} - * @example - * // Logs: Hello from My Group DM! - * console.log(`Hello from ${channel}!`); - */ - toString() { - return this.name; - } - - // These are here only for documentation purposes - they are implemented by TextBasedChannel - /* eslint-disable no-empty-function */ - get lastMessage() {} - get lastPinAt() {} - send() {} - startTyping() {} - stopTyping() {} - get typing() {} - get typingCount() {} - createMessageCollector() {} - awaitMessages() {} - // Doesn't work on Group DMs; bulkDelete() {} - acknowledge() {} - _cacheMessage() {} -} - -TextBasedChannel.applyToClass(GroupDMChannel, true, ['bulkDelete']); - -module.exports = GroupDMChannel; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 66cf9871..f99627a4 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -5,7 +5,7 @@ const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); -const { ChannelTypes, DefaultMessageNotifications, browser } = require('../util/Constants'); +const { ChannelTypes, DefaultMessageNotifications, PartialTypes, browser } = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); @@ -341,7 +341,9 @@ class Guild extends Base { * @readonly */ get owner() { - return this.members.get(this.ownerID) || null; + return this.members.get(this.ownerID) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? + this.members.add({ user: { id: this.ownerID } }, true) : + null); } /** diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index f78078a7..b466a3aa 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -4,6 +4,7 @@ const Collection = require('../util/Collection'); const Snowflake = require('../util/Snowflake'); const Webhook = require('./Webhook'); const Util = require('../util/Util'); +const PartialTypes = require('../util/Constants'); /** * The target type of an entry, e.g. `GUILD`. Here are the available types: @@ -234,7 +235,7 @@ class GuildAuditLogs { * Audit logs entry. */ class GuildAuditLogsEntry { - constructor(logs, guild, data) { + constructor(logs, guild, data) { // eslint-disable-line complexity const targetType = GuildAuditLogs.targetType(data.action_type); /** * The target type of this entry @@ -264,7 +265,9 @@ class GuildAuditLogsEntry { * The user that executed this entry * @type {User} */ - this.executor = guild.client.users.get(data.user_id); + this.executor = guild.client.options.partials.includes(PartialTypes.USER) ? + guild.client.users.add({ id: data.user_id }) : + guild.client.users.get(data.user_id); /** * An entry in the audit log representing a specific change. @@ -329,8 +332,12 @@ class GuildAuditLogsEntry { return o; }, {}); this.target.id = data.target_id; - } else if ([Targets.USER, Targets.GUILD].includes(targetType)) { - this.target = guild.client[`${targetType.toLowerCase()}s`].get(data.target_id); + } else if (targetType === Targets.USER) { + this.target = guild.client.options.partials.includes(PartialTypes.USER) ? + guild.client.users.add({ id: data.target_id }) : + guild.client.users.get(data.target_id); + } else if (targetType === Targets.GUILD) { + this.target = guild.client.guilds.get(data.target_id); } else if (targetType === Targets.WEBHOOK) { this.target = logs.webhooks.get(data.target_id) || new Webhook(guild.client, diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 73adb69a..e2dea432 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -28,7 +28,7 @@ class GuildMember extends Base { * The user that this guild member instance represents * @type {User} */ - this.user = {}; + if (data.user) this.user = client.users.add(data.user, true); /** * The timestamp the member joined the guild at @@ -79,6 +79,14 @@ class GuildMember extends Base { return clone; } + /** + * Whether this GuildMember is a partial + * @type {boolean} + */ + get partial() { + return !this.joinedTimestamp; + } + /** * A collection of roles that are applied to this member, mapped by the role ID * @type {GuildMemberRoleStore} @@ -355,6 +363,14 @@ class GuildMember extends Base { return this.guild.members.ban(this, options); } + /** + * Fetches this GuildMember. + * @returns {Promise} + */ + fetch() { + return this.guild.members.fetch(this.id, true); + } + /** * When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object. * @returns {string} diff --git a/src/structures/Message.js b/src/structures/Message.js index 310706e1..4b81cf94 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -24,7 +24,7 @@ class Message extends Base { /** * The channel that the message was sent in - * @type {TextChannel|DMChannel|GroupDMChannel} + * @type {TextChannel|DMChannel} */ this.channel = channel; @@ -60,7 +60,7 @@ class Message extends Base { * The author of the message * @type {User} */ - this.author = this.client.users.add(data.author, !data.webhook_id); + this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null; /** * Whether or not this message is pinned @@ -90,17 +90,19 @@ class Message extends Base { * A list of embeds in the message - e.g. YouTube Player * @type {MessageEmbed[]} */ - this.embeds = data.embeds.map(e => new Embed(e)); + this.embeds = (data.embeds || []).map(e => new Embed(e)); /** * A collection of attachments in the message - e.g. Pictures - mapped by their ID * @type {Collection} */ this.attachments = new Collection(); - for (const attachment of data.attachments) { - this.attachments.set(attachment.id, new MessageAttachment( - attachment.url, attachment.filename, attachment - )); + if (data.attachments) { + for (const attachment of data.attachments) { + this.attachments.set(attachment.id, new MessageAttachment( + attachment.url, attachment.filename, attachment + )); + } } /** @@ -167,6 +169,14 @@ class Message extends Base { } } + /** + * Whether or not this message is a partial + * @type {boolean} + */ + get partial() { + return typeof this.content !== 'string' || !this.author; + } + /** * Updates the message. * @param {Object} data Raw Discord message update data @@ -472,6 +482,14 @@ class Message extends Base { ); } + /** + * Fetch this message. + * @returns {Promise} + */ + fetch() { + return this.channel.messages.fetch(this.id, true); + } + /** * Fetches the webhook used to create this message. * @returns {Promise} diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 59120b9d..44156cc0 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -15,7 +15,7 @@ const { Events } = require('../util/Constants'); */ class MessageCollector extends Collector { /** - * @param {TextChannel|DMChannel|GroupDMChannel} channel The channel + * @param {TextChannel|DMChannel} channel The channel * @param {CollectorFilter} filter The filter to be applied to this collector * @param {MessageCollectorOptions} options The options to be applied to this collector * @emits MessageCollector#message diff --git a/src/structures/User.js b/src/structures/User.js index 22ea5c81..24bce793 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -53,6 +53,8 @@ class User extends Base { */ if (typeof data.avatar !== 'undefined') this.avatar = data.avatar; + if (typeof data.bot !== 'undefined') this.bot = Boolean(data.bot); + /** * The locale of the user's client (ISO 639-1) * @type {?string} @@ -73,6 +75,14 @@ class User extends Base { this.lastMessageChannelID = null; } + /** + * Whether this User is a partial + * @type {boolean} + */ + get partial() { + return typeof this.username !== 'string'; + } + /** * The timestamp the user was created at * @type {number} @@ -228,6 +238,14 @@ class User extends Base { return equal; } + /** + * Fetches this user. + * @returns {Promise} + */ + fetch() { + return this.client.users.fetch(this.id, true); + } + /** * When concatenated with a string, this automatically returns the user's mention instead of the User object. * @returns {string} diff --git a/src/util/Constants.js b/src/util/Constants.js index 508b5f97..1e13852c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -21,6 +21,9 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as * upon joining a guild (should be avoided whenever possible) * @property {boolean} [disableEveryone=false] Default value for {@link MessageOptions#disableEveryone} + * @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when + * they're missing all the data for a particular structure. See the "Partials" topic listed in the sidebar for some + * important usage information, as partials require you to put checks in place when handling data. * @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their * corresponding websocket events * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST @@ -44,6 +47,7 @@ exports.DefaultOptions = { messageSweepInterval: 0, fetchAllMembers: false, disableEveryone: false, + partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], retryLimit: 1, @@ -261,6 +265,23 @@ exports.Events = { RAW: 'raw', }; +/** + * The type of Structure allowed to be a partial: + * * USER + * * CHANNEL (only affects DMChannels) + * * GUILD_MEMBER + * * MESSAGE + * Partials require you to put checks in place when handling data, read the Partials topic listed in the + * sidebar for more information. + * @typedef {string} PartialType + */ +exports.PartialTypes = keyMirror([ + 'USER', + 'CHANNEL', + 'GUILD_MEMBER', + 'MESSAGE', +]); + /** * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: * * READY diff --git a/src/util/Structures.js b/src/util/Structures.js index a742e292..4ac56260 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -67,7 +67,6 @@ class Structures { const structures = { GuildEmoji: require('../structures/GuildEmoji'), DMChannel: require('../structures/DMChannel'), - GroupDMChannel: require('../structures/GroupDMChannel'), TextChannel: require('../structures/TextChannel'), VoiceChannel: require('../structures/VoiceChannel'), CategoryChannel: require('../structures/CategoryChannel'), diff --git a/src/util/Util.js b/src/util/Util.js index 1d7b7aee..bbf838e4 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -395,7 +395,7 @@ class Util { .replace(/@(everyone|here)/g, '@\u200b$1') .replace(/<@!?[0-9]+>/g, input => { const id = input.replace(/<|!|>|@/g, ''); - if (message.channel.type === 'dm' || message.channel.type === 'group') { + if (message.channel.type === 'dm') { const user = message.client.users.get(id); return user ? `@${user.username}` : input; } @@ -413,7 +413,7 @@ class Util { return channel ? `#${channel.name}` : input; }) .replace(/<@&[0-9]+>/g, input => { - if (message.channel.type === 'dm' || message.channel.type === 'group') return input; + if (message.channel.type === 'dm') return input; const role = message.guild.roles.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; }); diff --git a/test/voice.js b/test/voice.js index 70b5c30f..6d022a29 100644 --- a/test/voice.js +++ b/test/voice.js @@ -6,7 +6,7 @@ const ytdl = require('ytdl-core'); const prism = require('prism-media'); const fs = require('fs'); -const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' }); +const client = new Discord.Client({ fetchAllMembers: false, partials: true, apiRequestMethod: 'sequential' }); const auth = require('./auth.js'); @@ -34,6 +34,15 @@ client.on('presenceUpdate', (a, b) => { console.log(a ? a.status : null, b.status, b.user.username); }); +client.on('messageDelete', async (m) => { + if (m.channel.id != '80426989059575808') return; + console.log(m.channel.recipient); + console.log(m.channel.partial); + await m.channel.fetch(); + console.log('\n\n\n\n'); + console.log(m.channel); +}); + client.on('message', m => { if (!m.guild) return; if (m.author.id !== '66564597481480192') return; diff --git a/typings/index.d.ts b/typings/index.d.ts index 8c606400..4283755d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -129,8 +129,9 @@ declare module 'discord.js' { public readonly createdTimestamp: number; public deleted: boolean; public id: Snowflake; - public type: 'dm' | 'group' | 'text' | 'voice' | 'category' | 'unknown'; + public type: 'dm' | 'text' | 'voice' | 'category' | 'unknown'; public delete(reason?: string): Promise; + public fetch(): Promise; public toString(): string; } @@ -264,7 +265,6 @@ declare module 'discord.js' { export class ClientUser extends User { public mfaEnabled: boolean; public verified: boolean; - public createGroupDM(recipients: GroupDMRecipientOptions[]): Promise; public setActivity(options?: ActivityOptions): Promise; public setActivity(name: string, options?: ActivityOptions): Promise; public setAFK(afk: boolean): Promise; @@ -360,6 +360,7 @@ declare module 'discord.js' { constructor(client: Client, data?: object); public messages: MessageStore; public recipient: User; + public readonly partial: boolean; } export class Emoji extends Base { @@ -376,26 +377,6 @@ declare module 'discord.js' { public toString(): string; } - export class GroupDMChannel extends TextBasedChannel(Channel) { - constructor(client: Client, data?: object); - public applicationID: Snowflake; - public icon: string; - public managed: boolean; - public messages: MessageStore; - public name: string; - public nicks: Collection; - public readonly owner: User; - public ownerID: Snowflake; - public recipients: Collection; - public addUser(options: { user: UserResolvable, accessToken?: string, nick?: string }): Promise; - public edit (data: { icon?: string, name?: string }): Promise; - public equals(channel: GroupDMChannel): boolean; - public iconURL(options?: AvatarOptions): string; - public removeUser(user: UserResolvable): Promise; - public setIcon(icon: Base64Resolvable | BufferResolvable): Promise; - public setName(name: string): Promise; - } - export class Guild extends Base { constructor(client: Client, data: object); private _sortedRoles(): Collection; @@ -570,12 +551,14 @@ declare module 'discord.js' { public readonly kickable: boolean; public readonly manageable: boolean; public nickname: string; + public readonly partial: boolean; public readonly permissions: Readonly; public readonly presence: Presence; public roles: GuildMemberRoleStore; public user: User; public readonly voice: VoiceState; public ban(options?: BanOptions): Promise; + public fetch(): Promise; public createDM(): Promise; public deleteDM(): Promise; public edit(data: GuildMemberEditData, reason?: string): Promise; @@ -619,7 +602,7 @@ declare module 'discord.js' { export class Invite extends Base { constructor(client: Client, data: object); - public channel: GuildChannel | GroupDMChannel; + public channel: GuildChannel; public code: string; public readonly createdAt: Date; public createdTimestamp: number; @@ -640,7 +623,7 @@ declare module 'discord.js' { } export class Message extends Base { - constructor(client: Client, data: object, channel: TextChannel | DMChannel | GroupDMChannel); + constructor(client: Client, data: object, channel: TextChannel | DMChannel); private _edits: Message[]; private patch(data: object): void; @@ -648,7 +631,7 @@ declare module 'discord.js' { public application: ClientApplication; public attachments: Collection; public author: User; - public channel: TextChannel | DMChannel | GroupDMChannel; + public channel: TextChannel | DMChannel; public readonly cleanContent: string; public content: string; public readonly createdAt: Date; @@ -665,6 +648,7 @@ declare module 'discord.js' { public readonly member: GuildMember; public mentions: MessageMentions; public nonce: string; + public readonly partial: boolean; public readonly pinnable: boolean; public pinned: boolean; public reactions: ReactionStore; @@ -680,6 +664,7 @@ declare module 'discord.js' { public edit(options: MessageEditOptions | MessageEmbed | APIMessage): Promise; public equals(message: Message, rawData: object): boolean; public fetchWebhook(): Promise; + public fetch(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; @@ -706,7 +691,7 @@ declare module 'discord.js' { } export class MessageCollector extends Collector { - constructor(channel: TextChannel | DMChannel | GroupDMChannel, filter: CollectorFilter, options?: MessageCollectorOptions); + constructor(channel: TextChannel | DMChannel, filter: CollectorFilter, options?: MessageCollectorOptions); public channel: Channel; public options: MessageCollectorOptions; public received: number; @@ -1078,6 +1063,7 @@ declare module 'discord.js' { public readonly dmChannel: DMChannel; public id: Snowflake; public locale: string; + public readonly partial: boolean; public readonly presence: Presence; public readonly tag: string; public username: string; @@ -1086,6 +1072,7 @@ declare module 'discord.js' { public deleteDM(): Promise; public displayAvatarURL(options?: AvatarOptions): string; public equals(user: User): boolean; + public fetch(): Promise; public toString(): string; public typingDurationIn(channel: ChannelResolvable): number; public typingIn(channel: ChannelResolvable): boolean; @@ -1385,7 +1372,7 @@ declare module 'discord.js' { } export class MessageStore extends DataStore { - constructor(channel: TextChannel | DMChannel | GroupDMChannel, iterable?: Iterable); + constructor(channel: TextChannel | DMChannel, iterable?: Iterable); public fetch(message: Snowflake): Promise; public fetch(options?: ChannelLogsQueryOptions): Promise>; public fetchPinned(): Promise>; @@ -1607,6 +1594,7 @@ declare module 'discord.js' { messageSweepInterval?: number; fetchAllMembers?: boolean; disableEveryone?: boolean; + partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; restSweepInterval?: number; @@ -1676,7 +1664,6 @@ declare module 'discord.js' { type Extendable = { GuildEmoji: typeof GuildEmoji; DMChannel: typeof DMChannel; - GroupDMChannel: typeof GroupDMChannel; TextChannel: typeof TextChannel; VoiceChannel: typeof VoiceChannel; CategoryChannel: typeof CategoryChannel; @@ -1711,13 +1698,6 @@ declare module 'discord.js' { type: number; }; - type GroupDMRecipientOptions = { - user?: UserResolvable | Snowflake; - accessToken?: string; - nick?: string; - id?: Snowflake; - }; - type GuildAuditLogsAction = keyof GuildAuditLogsActions; type GuildAuditLogsActions = { @@ -1934,7 +1914,7 @@ declare module 'discord.js' { type MessageResolvable = Message | Snowflake; - type MessageTarget = TextChannel | DMChannel | GroupDMChannel | User | GuildMember | Webhook | WebhookClient; + type MessageTarget = TextChannel | DMChannel | User | GuildMember | Webhook | WebhookClient; type MessageType = 'DEFAULT' | 'RECIPIENT_ADD' @@ -2023,6 +2003,11 @@ declare module 'discord.js' { desktop?: ClientPresenceStatus }; + type PartialTypes = 'USER' + | 'CHANNEL' + | 'GUILD_MEMBER' + | 'MESSAGE'; + type PresenceStatus = ClientPresenceStatus | 'offline'; type PresenceStatusData = ClientPresenceStatus | 'invisible'; From 4289b18ab27110504a78c2f8b2ad7800912a623e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 14 Feb 2019 11:19:05 +0100 Subject: [PATCH 061/428] docs(GuildMember): add missing @name to document user property Fixes #3090 --- src/structures/GuildMember.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index e2dea432..91f54c8c 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -27,6 +27,7 @@ class GuildMember extends Base { /** * The user that this guild member instance represents * @type {User} + * @name GuildMember#user */ if (data.user) this.user = client.users.add(data.user, true); From 0564c5c777cfd2391261417a2047d4d7f92474ca Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 14 Feb 2019 16:57:26 +0000 Subject: [PATCH 062/428] voice: update prism --- src/client/voice/util/PlayInterface.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index ac66eb29..873cd81e 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -75,10 +75,10 @@ class PlayInterface { return this.player.playOpusStream(resource, options); } else if (type === 'ogg/opus') { if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); - return this.player.playOpusStream(resource.pipe(new prism.OggOpusDemuxer()), options); + return this.player.playOpusStream(resource.pipe(new prism.opus.OggDemuxer()), options); } else if (type === 'webm/opus') { if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); - return this.player.playOpusStream(resource.pipe(new prism.WebmOpusDemuxer()), options); + return this.player.playOpusStream(resource.pipe(new prism.opus.WebmDemuxer()), options); } } throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); From 4009986bc8bd1ccf0bdbd0b5135c1bde78839ed2 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 17 Feb 2019 21:48:47 +0000 Subject: [PATCH 063/428] voice: destroy opus --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 93c6df81..102c5eab 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -117,7 +117,7 @@ class StreamDispatcher extends Writable { if (this.player.dispatcher === this) this.player.dispatcher = null; const { streams } = this; if (streams.broadcast) streams.broadcast.dispatchers.delete(this); - if (streams.opus) streams.opus.unpipe(this); + if (streams.opus) streams.opus.destroy(); if (streams.ffmpeg) streams.ffmpeg.destroy(); super._destroy(err, cb); } From 6aa792f9ab8008c22397d1a46a862fa88ba5aef5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 19 Feb 2019 13:16:48 +0000 Subject: [PATCH 064/428] voice: cleanup internals whether end() or destroy() is called --- src/client/voice/dispatcher/StreamDispatcher.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 102c5eab..e05cd49e 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -67,6 +67,7 @@ class StreamDispatcher extends Writable { this.count = 0; this.on('finish', () => { + this._cleanup(); // Still emitting end for backwards compatibility, probably remove it in the future! this.emit('end'); this._setSpeaking(0); @@ -114,12 +115,16 @@ class StreamDispatcher extends Writable { } _destroy(err, cb) { + this._cleanup(); + super._destroy(err, cb); + } + + _cleanup() { if (this.player.dispatcher === this) this.player.dispatcher = null; const { streams } = this; if (streams.broadcast) streams.broadcast.dispatchers.delete(this); if (streams.opus) streams.opus.destroy(); if (streams.ffmpeg) streams.ffmpeg.destroy(); - super._destroy(err, cb); } /** From efbbfbcec6d9b090eb7053bbb39acea50c46c517 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 20 Feb 2019 19:15:33 +0000 Subject: [PATCH 065/428] fix #3102 --- src/client/voice/player/BasePlayer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index 763762ef..dc8f9602 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -66,8 +66,8 @@ class BasePlayer extends EventEmitter { stream.pipe(opus); return this.playOpusStream(opus, options, streams); } - const volume = streams.volume = new prism.VolumeTransformer16LE({ volume: options ? options.volume : 1 }); - stream.pipe(volume).pipe(opus); + streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 }); + stream.pipe(streams.volume).pipe(opus); return this.playOpusStream(opus, options, streams); } @@ -77,10 +77,10 @@ class BasePlayer extends EventEmitter { if (options.volume !== false && !streams.input) { streams.input = stream; const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 }); - const volume = streams.volume = new prism.VolumeTransformer16LE({ volume: options ? options.volume : 1 }); + streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 }); streams.opus = stream .pipe(decoder) - .pipe(volume) + .pipe(streams.volume) .pipe(new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 })); } const dispatcher = this.createDispatcher(options, streams); From 73be952406e54cb7626bc0f07085a32b7462126c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 21 Feb 2019 11:56:46 +0000 Subject: [PATCH 066/428] use official release of prism --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41385f9d..351e5cb4 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "form-data": "^2.3.3", "node-fetch": "^2.3.0", "pako": "^1.0.8", - "prism-media": "amishshah/prism-media", + "prism-media": "^1.0.0", "setimmediate": "^1.0.5", "tweetnacl": "^1.0.1", "ws": "^6.1.3" From 4d3f76656a7456fb4f196379346860114a70f1f9 Mon Sep 17 00:00:00 2001 From: Linn Dahlgren Date: Sat, 23 Feb 2019 10:05:04 +0100 Subject: [PATCH 067/428] docs(MessageEmbed): add missing type value (#3106) --- src/structures/MessageEmbed.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 0c925d27..0b630cc6 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -16,6 +16,7 @@ class MessageEmbed { * The type of this embed, either: * * `image` - an image embed * * `video` - a video embed + * * `gifv` - a gifv embed * * `link` - a link embed * * `rich` - a rich embed * @type {string} From bc0a761e2242c55db91192fd0a3e438507354e21 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sun, 24 Feb 2019 05:14:09 +0100 Subject: [PATCH 068/428] typings: Convert types to interfaces where applicable (#3068) --- typings/index.d.ts | 340 +++++++++++++++++++++++---------------------- 1 file changed, 173 insertions(+), 167 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4283755d..6ac87dbb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,11 +1,3 @@ -// Type definitions for discord.js 12.0.0 -// Project: https://github.com/hydrabolt/discord.js -// Definitions by: -// acdenisSK (https://github.com/acdenisSK) -// Zack Campbell (https://github.com/zajrik) -// iCrawl (https://github.com/iCrawl) -// License: MIT - declare module 'discord.js' { import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; @@ -72,7 +64,7 @@ declare module 'discord.js' { } export class Base { - constructor (client: Client); + constructor(client: Client); public readonly client: Client; public toJSON(...props: { [key: string]: boolean | string }[]): object; public valueOf(): string; @@ -837,7 +829,7 @@ declare module 'discord.js' { public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; public once(event: 'remove', listener: (reaction: MessageReaction, user: User) => void): this; public once(event: string, listener: Function): this; -} + } export class ReactionEmoji extends Emoji { constructor(reaction: MessageReaction, emoji: object); @@ -1340,7 +1332,7 @@ declare module 'discord.js' { } // Hacky workaround because changing the signature of an overriden method errors - class OverridableDataStore, R = any> extends DataStore { + class OverridableDataStore, R = any> extends DataStore { public add(data: any, cache: any): any; public set(key: any): any; } @@ -1419,7 +1411,7 @@ declare module 'discord.js' { const PartialTextBasedChannel: (Base?: Constructable) => Constructable; const TextBasedChannel: (Base?: Constructable) => Constructable; - type PartialTextBasedChannelFields = { + interface PartialTextBasedChannelFields { lastMessageID: Snowflake; lastMessageChannelID: Snowflake; readonly lastMessage: Message; @@ -1427,9 +1419,9 @@ declare module 'discord.js' { readonly lastPinAt: Date; send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - }; + } - type TextBasedChannelFields = { + interface TextBasedChannelFields extends PartialTextBasedChannelFields { typing: boolean; typingCount: number; awaitMessages(filter: CollectorFilter, options?: AwaitMessagesOptions): Promise>; @@ -1437,11 +1429,11 @@ declare module 'discord.js' { createMessageCollector(filter: CollectorFilter, options?: MessageCollectorOptions): MessageCollector; startTyping(count?: number): Promise; stopTyping(force?: boolean): void; - } & PartialTextBasedChannelFields; + } const WebhookMixin: (Base?: Constructable) => Constructable; - type WebhookFields = { + interface WebhookFields { readonly client: Client; id: Snowflake; token: string; @@ -1449,21 +1441,26 @@ declare module 'discord.js' { edit(options: WebhookEditData): Promise; send(content?: StringResolvable, options?: WebhookMessageOptions | MessageAdditions): Promise; send(options?: WebhookMessageOptions | MessageAdditions | APIMessage): Promise; - sendSlackMessage(body: object): Promise; - }; + sendSlackMessage(body: object): Promise; + } //#endregion //#region Typedefs - type ActivityFlagsString = 'INSTANCE' | 'JOIN' | 'SPECTATE' | 'JOIN_REQUEST' | 'SYNC' | 'PLAY'; + type ActivityFlagsString = 'INSTANCE' + | 'JOIN' + | 'SPECTATE' + | 'JOIN_REQUEST' + | 'SYNC' + | 'PLAY'; type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' | 'WATCHING'; - type APIErrror = { + interface APIErrror { UNKNOWN_ACCOUNT: number; UNKNOWN_APPLICATION: number; UNKNOWN_CHANNEL: number; @@ -1508,35 +1505,39 @@ declare module 'discord.js' { BULK_DELETE_MESSAGE_TOO_OLD: number; INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: number; REACTION_BLOCKED: number; - }; + } - type AddGuildMemberOptions = { + interface AddGuildMemberOptions { accessToken: String; nick?: string; roles?: Collection | RoleResolvable[]; mute?: boolean; deaf?: boolean; - }; + } - type AuditLogChange = { + interface AuditLogChange { key: string; old?: any; new?: any; - }; + } - type AvatarOptions = { + interface AvatarOptions { format?: ImageExt; size?: ImageSize; - }; + } - type AwaitMessagesOptions = MessageCollectorOptions & { errors?: string[] }; + interface AwaitMessagesOptions extends MessageCollectorOptions { + errors?: string[]; + } - type AwaitReactionsOptions = ReactionCollectorOptions & { errors?: string[] }; + interface AwaitReactionsOptions extends ReactionCollectorOptions { + errors?: string[]; + } - type BanOptions = { + interface BanOptions { days?: number; reason?: string; - }; + } type Base64Resolvable = Buffer | Base64String; @@ -1546,13 +1547,13 @@ declare module 'discord.js' { type BufferResolvable = Buffer | string; - type ChannelCreationOverwrites = { + interface ChannelCreationOverwrites { allow?: PermissionResolvable | number; deny?: PermissionResolvable | number; id: RoleResolvable | UserResolvable; - }; + } - type ChannelData = { + interface ChannelData { name?: string; position?: number; topic?: string; @@ -1563,29 +1564,29 @@ declare module 'discord.js' { rateLimitPerUser?: number; lockPermissions?: boolean; permissionOverwrites?: OverwriteResolvable[] | Collection; - }; + } - type ChannelLogsQueryOptions = { - limit?: number - before?: Snowflake - after?: Snowflake - around?: Snowflake - }; + interface ChannelLogsQueryOptions { + limit?: number; + before?: Snowflake; + after?: Snowflake; + around?: Snowflake; + } - type ChannelPosition = { + interface ChannelPosition { channel: ChannelResolvable; position: number; - }; + } type ChannelResolvable = Channel | Snowflake; - type ClientApplicationAsset = { + interface ClientApplicationAsset { name: string; id: Snowflake; type: 'BIG' | 'SMALL'; - }; + } - type ClientOptions = { + interface ClientOptions { shards?: number | number[]; shardCount?: number; totalShardCount?: number; @@ -1603,15 +1604,16 @@ declare module 'discord.js' { disabledEvents?: WSEventType[]; ws?: WebSocketOptions; http?: HTTPOptions; - }; + } type CollectorFilter = (...args: any[]) => boolean; - type CollectorOptions = { + + interface CollectorOptions { time?: number; dispose?: boolean; - }; + } - type ColorResolvable = ('DEFAULT' + type ColorResolvable = 'DEFAULT' | 'AQUA' | 'GREEN' | 'BLUE' @@ -1634,34 +1636,38 @@ declare module 'discord.js' { | 'DARK_GREY' | 'LIGHT_GREY' | 'DARK_NAVY' - | 'RANDOM') + | 'RANDOM' | [number, number, number] | number | string; - type DeconstructedSnowflake = { + interface DeconstructedSnowflake { timestamp: number; readonly date: Date; workerID: number; processID: number; increment: number; binary: string; - }; + } type DefaultMessageNotifications = 'ALL' | 'MENTIONS'; - type GuildEmojiEditData = { + interface GuildEmojiEditData { name?: string; roles?: Collection | RoleResolvable[]; - }; + } - type EmbedField = { name: string, value: string, inline?: boolean }; + interface EmbedField { + name: string; + value: string; + inline?: boolean; + } type EmojiIdentifierResolvable = string | EmojiResolvable; type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; - type Extendable = { + interface Extendable { GuildEmoji: typeof GuildEmoji; DMChannel: typeof DMChannel; TextChannel: typeof TextChannel; @@ -1676,76 +1682,76 @@ declare module 'discord.js' { VoiceState: typeof VoiceState; Role: typeof Role; User: typeof User; - }; + } - type FetchMemberOptions = { + interface FetchMemberOptions { user: UserResolvable; cache?: boolean; - }; + } - type FetchMembersOptions = { + interface FetchMembersOptions { query?: string; limit?: number; - }; + } - type FileOptions = { + interface FileOptions { attachment: BufferResolvable | Stream; name?: string; - }; + } - type GroupActivity = { + interface GroupActivity { partyID: string; type: number; - }; + } type GuildAuditLogsAction = keyof GuildAuditLogsActions; - type GuildAuditLogsActions = { - ALL?: null, - GUILD_UPDATE?: number, - CHANNEL_CREATE?: number, - CHANNEL_UPDATE?: number, - CHANNEL_DELETE?: number, - CHANNEL_OVERWRITE_CREATE?: number, - CHANNEL_OVERWRITE_UPDATE?: number, - CHANNEL_OVERWRITE_DELETE?: number, - MEMBER_KICK?: number, - MEMBER_PRUNE?: number, - MEMBER_BAN_ADD?: number, - MEMBER_BAN_REMOVE?: number, - MEMBER_UPDATE?: number, - MEMBER_ROLE_UPDATE?: number, - ROLE_CREATE?: number, - ROLE_UPDATE?: number, - ROLE_DELETE?: number, - INVITE_CREATE?: number, - INVITE_UPDATE?: number, - INVITE_DELETE?: number, - WEBHOOK_CREATE?: number, - WEBHOOK_UPDATE?: number, - WEBHOOK_DELETE?: number, - EMOJI_CREATE?: number, - EMOJI_UPDATE?: number, - EMOJI_DELETE?: number, - MESSAGE_DELETE?: number - }; + interface GuildAuditLogsActions { + ALL?: null; + GUILD_UPDATE?: number; + CHANNEL_CREATE?: number; + CHANNEL_UPDATE?: number; + CHANNEL_DELETE?: number; + CHANNEL_OVERWRITE_CREATE?: number; + CHANNEL_OVERWRITE_UPDATE?: number; + CHANNEL_OVERWRITE_DELETE?: number; + MEMBER_KICK?: number; + MEMBER_PRUNE?: number; + MEMBER_BAN_ADD?: number; + MEMBER_BAN_REMOVE?: number; + MEMBER_UPDATE?: number; + MEMBER_ROLE_UPDATE?: number; + ROLE_CREATE?: number; + ROLE_UPDATE?: number; + ROLE_DELETE?: number; + INVITE_CREATE?: number; + INVITE_UPDATE?: number; + INVITE_DELETE?: number; + WEBHOOK_CREATE?: number; + WEBHOOK_UPDATE?: number; + WEBHOOK_DELETE?: number; + EMOJI_CREATE?: number; + EMOJI_UPDATE?: number; + EMOJI_DELETE?: number; + MESSAGE_DELETE?: number; + } type GuildAuditLogsActionType = 'CREATE' | 'DELETE' | 'UPDATE' | 'ALL'; - type GuildAuditLogsFetchOptions = { + interface GuildAuditLogsFetchOptions { before?: Snowflake | GuildAuditLogsEntry; after?: Snowflake | GuildAuditLogsEntry; limit?: number; user?: UserResolvable; type?: string | number; - }; + } type GuildAuditLogsTarget = keyof GuildAuditLogsTargets; - type GuildAuditLogsTargets = { + interface GuildAuditLogsTargets { ALL?: string; GUILD?: string; CHANNEL?: string; @@ -1755,11 +1761,11 @@ declare module 'discord.js' { WEBHOOK?: string; EMOJI?: string; MESSAGE?: string; - }; + } type GuildChannelResolvable = Snowflake | GuildChannel; - type GuildCreateChannelOptions = { + interface GuildCreateChannelOptions { permissionOverwrites?: OverwriteResolvable[] | Collection; topic?: string; type?: 'text' | 'voice' | 'category'; @@ -1770,18 +1776,18 @@ declare module 'discord.js' { rateLimitPerUser?: number; position?: number; reason?: string; - }; + } - type GuildChannelCloneOptions = GuildCreateChannelOptions & { + interface GuildChannelCloneOptions extends GuildCreateChannelOptions { name?: string; - }; + } - type GuildEmojiCreateOptions = { + interface GuildEmojiCreateOptions { roles?: Collection | RoleResolvable[]; reason?: string; - }; + } - type GuildEditData = { + interface GuildEditData { name?: string; region?: string; verificationLevel?: number; @@ -1793,12 +1799,12 @@ declare module 'discord.js' { icon?: Base64Resolvable; owner?: GuildMemberResolvable; splash?: Base64Resolvable; - }; + } - type GuildEmbedData = { + interface GuildEmbedData { enabled: boolean; channel?: GuildChannelResolvable; - }; + } type GuildFeatures = 'INVITE_SPLASH' | 'MORE_EMOJI' @@ -1806,30 +1812,30 @@ declare module 'discord.js' { | 'VIP_REGIONS' | 'VANITY_URL'; - type GuildMemberEditData = { + interface GuildMemberEditData { nick?: string; roles?: Collection | RoleResolvable[]; mute?: boolean; deaf?: boolean; channel?: ChannelResolvable; - }; + } type GuildMemberResolvable = GuildMember | UserResolvable; type GuildResolvable = Guild | Snowflake; - type GuildPruneMembersOptions = { + interface GuildPruneMembersOptions { days?: number; dry?: boolean; reason?: string; - }; + } - type HTTPOptions = { + interface HTTPOptions { version?: number; host?: string; cdn?: string; invite?: string; - }; + } type ImageExt = 'webp' | 'png' @@ -1845,45 +1851,45 @@ declare module 'discord.js' { | 1024 | 2048; - type IntegrationData = { + interface IntegrationData { id: string; type: string; - }; + } - type IntegrationEditData = { + interface IntegrationEditData { expireBehavior?: number; expireGracePeriod?: number; - }; + } - type IntegrationAccount = { + interface IntegrationAccount { id: string; name: string; - }; + } - type InviteOptions = { + interface InviteOptions { temporary?: boolean; maxAge?: number; maxUses?: number; unique?: boolean; reason?: string; - }; + } type InviteResolvable = string; - type MessageCollectorOptions = CollectorOptions & { + interface MessageCollectorOptions extends CollectorOptions { max?: number; maxProcessed?: number; - }; + } type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; - type MessageEditOptions = { + interface MessageEditOptions { content?: string; embed?: MessageEmbedOptions | null; code?: string | boolean; - }; + } - type MessageEmbedOptions = { + interface MessageEmbedOptions { title?: string; description?: string; url?: string; @@ -1896,19 +1902,19 @@ declare module 'discord.js' { image?: { url?: string; proxy_url?: string; proxyURL?: string; height?: number; width?: number; }; video?: { url?: string; height?: number; width?: number; }; footer?: { text?: string; icon_url?: string; iconURL?: string; }; - }; + } - type MessageOptions = { + interface MessageOptions { tts?: boolean; nonce?: string; content?: string; - embed?: MessageEmbed | MessageEmbedOptions, + embed?: MessageEmbed | MessageEmbedOptions; disableEveryone?: boolean; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; reply?: UserResolvable; - }; + } type MessageReactionResolvable = MessageReaction | Snowflake; @@ -1925,22 +1931,22 @@ declare module 'discord.js' { | 'PINS_ADD' | 'GUILD_MEMBER_JOIN'; - type OverwriteData = { + interface OverwriteData { allow?: PermissionResolvable; deny?: PermissionResolvable; id: GuildMemberResolvable | RoleResolvable; type?: OverwriteType; - }; + } type OverwriteResolvable = PermissionOverwrites | OverwriteData; type OverwriteType = 'member' | 'role'; - type PermissionFlags = Record; + interface PermissionFlags extends Record { } - type PermissionObject = Record; + interface PermissionObject extends Record { } - type PermissionOverwriteOption = { [k in PermissionString]?: boolean | null }; + interface PermissionOverwriteOption extends Partial> { } type PermissionString = 'CREATE_INSTANT_INVITE' | 'KICK_MEMBERS' @@ -1976,13 +1982,13 @@ declare module 'discord.js' { type PermissionResolvable = BitFieldResolvable; - type PermissionOverwriteOptions = { + interface PermissionOverwriteOptions { allow: PermissionResolvable; deny: PermissionResolvable; id: UserResolvable | RoleResolvable; - }; + } - type PresenceData = { + interface PresenceData { status?: PresenceStatusData; afk?: boolean; activity?: { @@ -1991,17 +1997,17 @@ declare module 'discord.js' { url?: string; }; shardID?: number | number[]; - }; + } type PresenceResolvable = Presence | UserResolvable | Snowflake; type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; - type ClientPresenceStatusData = { - web?: ClientPresenceStatus, - mobile?: ClientPresenceStatus, - desktop?: ClientPresenceStatus - }; + interface ClientPresenceStatusData { + web?: ClientPresenceStatus; + mobile?: ClientPresenceStatus; + desktop?: ClientPresenceStatus; + } type PartialTypes = 'USER' | 'CHANNEL' @@ -2012,41 +2018,41 @@ declare module 'discord.js' { type PresenceStatusData = ClientPresenceStatus | 'invisible'; - type RateLimitData = { + interface RateLimitData { timeout: number; limit: number; timeDifference: number; method: string; path: string; route: string; - }; + } - type RawOverwriteData = { + interface RawOverwriteData { id: Snowflake; allow: number; deny: number; type: OverwriteType; - }; + } - type ReactionCollectorOptions = CollectorOptions & { + interface ReactionCollectorOptions extends CollectorOptions { max?: number; maxEmojis?: number; maxUsers?: number; - }; + } - type ResolvedOverwriteOptions = { + interface ResolvedOverwriteOptions { allow: Permissions; deny: Permissions; - }; + } - type RoleData = { + interface RoleData { name?: string; color?: ColorResolvable; hoist?: boolean; position?: number; permissions?: PermissionResolvable; mentionable?: boolean; - }; + } type RoleResolvable = Role | string; @@ -2054,25 +2060,25 @@ declare module 'discord.js' { type Snowflake = string; - type SplitOptions = { + interface SplitOptions { maxLength?: number; char?: string; prepend?: string; append?: string; - }; + } type Status = number; - type StreamOptions = { + interface StreamOptions { type?: StreamType; seek?: number; volume?: number; passes?: number; plp?: number; fec?: boolean; - bitrate?: number | 'auto' + bitrate?: number | 'auto'; highWaterMark?: number; - }; + } type SpeakingString = 'SPEAKING' | 'SOUNDSHARE'; @@ -2084,14 +2090,14 @@ declare module 'discord.js' { type VoiceStatus = number; - type WebhookEditData = { + interface WebhookEditData { name?: string; avatar?: BufferResolvable; channel?: ChannelResolvable; reason?: string; - }; + } - type WebhookMessageOptions = { + interface WebhookMessageOptions { username?: string; avatarURL?: string; tts?: boolean; @@ -2101,12 +2107,12 @@ declare module 'discord.js' { files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; - }; + } - type WebSocketOptions = { + interface WebSocketOptions { large_threshold?: number; compress?: boolean; - }; + } type WSEventType = 'READY' | 'RESUMED' From 7006f00ae4f2c9e23ec4f31bf2d1de9ae4c98b90 Mon Sep 17 00:00:00 2001 From: Linn Dahlgren Date: Sun, 24 Feb 2019 09:27:57 +0100 Subject: [PATCH 069/428] feat(MessageEmbed): add missing proxyURL property to video (#3109) * Added missing property to MessageEmbed.video * Updated typings for MessageEmbed.video --- src/structures/MessageEmbed.js | 8 +++++++- typings/index.d.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 0b630cc6..8276cad7 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -100,11 +100,17 @@ class MessageEmbed { * The video of this embed (if there is one) * @type {?Object} * @property {string} url URL of this video + * @property {string} proxyURL ProxyURL for this video * @property {number} height Height of this video * @property {number} width Width of this video * @readonly */ - this.video = data.video; + this.video = data.video ? { + url: data.video.url, + proxyURL: data.video.proxy_url, + height: data.video.height, + width: data.video.width, + } : null; /** * The author of this embed (if there is one) diff --git a/typings/index.d.ts b/typings/index.d.ts index 6ac87dbb..9a306de8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -713,7 +713,7 @@ declare module 'discord.js' { public title: string; public type: string; public url: string; - public readonly video: { url?: string; height?: number; width?: number }; + public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number }; public addBlankField(inline?: boolean): this; public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; From fe5563e15857efa79d25a8bd0f5c4878b0b64c1b Mon Sep 17 00:00:00 2001 From: bdistin Date: Sun, 24 Feb 2019 02:37:10 -0600 Subject: [PATCH 070/428] refactor(User): avoid invoking dmChannel getter more than once per method (#3108) also refactors to async methods --- src/structures/User.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/structures/User.js b/src/structures/User.js index 24bce793..f510ee13 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -204,22 +204,24 @@ class User extends Base { * Creates a DM channel between the client and the user. * @returns {Promise} */ - createDM() { - if (this.dmChannel) return Promise.resolve(this.dmChannel); - return this.client.api.users(this.client.user.id).channels.post({ data: { + async createDM() { + const { dmChannel } = this; + if (dmChannel) return dmChannel; + const data = await this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, - } }) - .then(data => this.client.actions.ChannelCreate.handle(data).channel); + } }); + return this.client.actions.ChannelCreate.handle(data).channel; } /** * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. * @returns {Promise} */ - deleteDM() { - if (!this.dmChannel) return Promise.reject(new Error('USER_NO_DMCHANNEL')); - return this.client.api.channels(this.dmChannel.id).delete() - .then(data => this.client.actions.ChannelDelete.handle(data).channel); + async deleteDM() { + const { dmChannel } = this; + if (!dmChannel) throw new Error('USER_NO_DMCHANNEL'); + const data = await this.client.api.channels(dmChannel.id).delete(); + return this.client.actions.ChannelDelete.handle(data).channel; } /** From 579283dfe9c35284eaa5ec2a8c1e081c48a78377 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 2 Mar 2019 21:58:40 +0100 Subject: [PATCH 071/428] fix(Role): proper undefined check for data.permission when editing --- src/structures/Role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index cbae5696..87cca568 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -172,7 +172,7 @@ class Role extends Base { * .catch(console.error); */ async edit(data, reason) { - if (data.permissions) data.permissions = Permissions.resolve(data.permissions); + if (typeof data.permissions !== 'undefined') data.permissions = Permissions.resolve(data.permissions); else data.permissions = this.permissions.bitfield; if (typeof data.position !== 'undefined') { await Util.setPosition(this, data.position, false, this.guild._sortedRoles(), From 1207c243d1d819b2a78230bb194e07f14d966ea9 Mon Sep 17 00:00:00 2001 From: Kamran Mackey Date: Mon, 4 Mar 2019 12:32:15 -0700 Subject: [PATCH 072/428] typings(Guild): add missing defaultRole property (#3126) * [typings] Fix missing defaultRole property under Guild. Signed-off-by: Kamran Mackey * [typings] Added missing readonly identifier. Signed-off-by: Kamran Mackey --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 9a306de8..e60dcfdf 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -386,6 +386,7 @@ declare module 'discord.js' { public readonly createdAt: Date; public readonly createdTimestamp: number; public defaultMessageNotifications: DefaultMessageNotifications | number; + public readonly defaultRole: Role; public deleted: boolean; public embedEnabled: boolean; public emojis: GuildEmojiStore; From 132788937a61490b5003117006f9e28164c14608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Tue, 5 Mar 2019 17:18:11 +0100 Subject: [PATCH 073/428] src: add missing events in constants (#3124) * src: Add missing events in constants * fix: Restore 'use strict'; --- src/util/Constants.js | 3 +++ typings/index.d.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index 1e13852c..b6c1b035 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -249,6 +249,7 @@ exports.Events = { USER_NOTE_UPDATE: 'userNoteUpdate', USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate', PRESENCE_UPDATE: 'presenceUpdate', + VOICE_SERVER_UPDATE: 'voiceServerUpdate', VOICE_STATE_UPDATE: 'voiceStateUpdate', VOICE_BROADCAST_SUBSCRIBE: 'subscribe', VOICE_BROADCAST_UNSUBSCRIBE: 'unsubscribe', @@ -316,6 +317,7 @@ exports.PartialTypes = keyMirror([ * * PRESENCE_UPDATE * * VOICE_STATE_UPDATE * * TYPING_START + * * VOICE_STATE_UPDATE * * VOICE_SERVER_UPDATE * * WEBHOOKS_UPDATE * @typedef {string} WSEventType @@ -352,6 +354,7 @@ exports.WSEvents = keyMirror([ 'PRESENCE_UPDATE', 'VOICE_STATE_UPDATE', 'TYPING_START', + 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE', 'WEBHOOKS_UPDATE', ]); diff --git a/typings/index.d.ts b/typings/index.d.ts index e60dcfdf..e1838dfc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2146,6 +2146,7 @@ declare module 'discord.js' { | 'PRESENCE_UPDATE' | 'VOICE_STATE_UPDATE' | 'TYPING_START' + | 'VOICE_STATE_UPDATE' | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE'; From 1673b6f8f5bc53a30e2f2ef1123057d4e50c37c8 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 8 Mar 2019 10:57:59 -0600 Subject: [PATCH 074/428] fix(APIMessage): edit shouldn't remove content (#3129) * edit shouldn't remove content If undefined is passed to the api, content isn't removed in such a case where you are only editing the embed. * fix a related doc string * update typings * requested changes --- src/structures/APIMessage.js | 25 +++++++++++++++---------- src/structures/Message.js | 2 +- typings/index.d.ts | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 41f98383..e6d67b68 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -65,13 +65,18 @@ class APIMessage { /** * Makes the content of this message. - * @returns {string|string[]} + * @returns {?(string|string[])} */ makeContent() { // eslint-disable-line complexity const GuildMember = require('./GuildMember'); - // eslint-disable-next-line eqeqeq - let content = Util.resolveString(this.options.content == null ? '' : this.options.content); + let content; + if (this.options.content === null) { + content = ''; + } else if (typeof this.options.content !== 'undefined') { + content = Util.resolveString(this.options.content); + } + const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; const splitOptions = isSplit ? { ...this.options.split } : undefined; @@ -88,24 +93,24 @@ class APIMessage { if (content || mentionPart) { if (isCode) { const codeName = typeof this.options.code === 'string' ? this.options.code : ''; - content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content, true)}\n\`\`\``; + content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content || '', true)}\n\`\`\``; if (isSplit) { splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; } } else if (mentionPart) { - content = `${mentionPart}${content}`; + content = `${mentionPart}${content || ''}`; } const disableEveryone = typeof this.options.disableEveryone === 'undefined' ? this.target.client.options.disableEveryone : this.options.disableEveryone; if (disableEveryone) { - content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + content = (content || '').replace(/@(everyone|here)/g, '@\u200b$1'); } if (isSplit) { - content = Util.splitMessage(content, splitOptions); + content = Util.splitMessage(content || '', splitOptions); } } @@ -275,7 +280,7 @@ class APIMessage { /** * Transforms the user-level arguments into a final options object. Passing a transformed options object alone into * this method will keep it the same, allowing for the reuse of the final options object. - * @param {StringResolvable} [content=''] Content to send + * @param {StringResolvable} [content] Content to send * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto transformed options * @param {boolean} [isWebhook=false] Whether or not to use WebhookMessageOptions as the result @@ -284,7 +289,7 @@ class APIMessage { static transformOptions(content, options, extra = {}, isWebhook = false) { if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; - content = ''; + content = undefined; } if (!options) { @@ -311,7 +316,7 @@ class APIMessage { /** * Creates an `APIMessage` from user-level arguments. * @param {MessageTarget} target Target to send to - * @param {StringResolvable} [content=''] Content to send + * @param {StringResolvable} [content] Content to send * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use * @param {MessageOptions|WebhookMessageOptions} [extra={}] - Extra options to add onto transformed options * @returns {MessageOptions|WebhookMessageOptions} diff --git a/src/structures/Message.js b/src/structures/Message.js index 4b81cf94..e302de26 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -365,7 +365,7 @@ class Message extends Base { /** * Edits the content of the message. - * @param {StringResolvable|APIMessage} [content=''] The new content for the message + * @param {StringResolvable|APIMessage} [content] The new content for the message * @param {MessageEditOptions|MessageEmbed} [options] The options to provide * @returns {Promise} * @example diff --git a/typings/index.d.ts b/typings/index.d.ts index e1838dfc..c1dfb86b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -56,7 +56,7 @@ declare module 'discord.js' { isWebhook?: boolean ): MessageOptions | WebhookMessageOptions; - public makeContent(): string | string[]; + public makeContent(): string | string[] | undefined; public resolve(): Promise; public resolveData(): this; public resolveFiles(): Promise; From df1889ab4902416de2bf6b3c144d55e35dbf116f Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Wed, 13 Mar 2019 05:14:32 +1100 Subject: [PATCH 075/428] cleanup(Guild): removed fetchAuditLogs' "after" option (#3142) * Removed code and documentation for Guild.fetchAuditLogs "after" option * Also removed the option from typings --- src/structures/Guild.js | 3 --- typings/index.d.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index f99627a4..9c44b1b8 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -566,7 +566,6 @@ class Guild extends Base { * Fetches audit logs for this guild. * @param {Object} [options={}] Options for fetching audit logs * @param {Snowflake|GuildAuditLogsEntry} [options.before] Limit to entries from before specified entry - * @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 {AuditLogAction|number} [options.type] Only show entries involving this action type @@ -579,12 +578,10 @@ class Guild extends Base { */ fetchAuditLogs(options = {}) { if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; - if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; return this.client.api.guilds(this.id)['audit-logs'].get({ query: { before: options.before, - after: options.after, limit: options.limit, user_id: this.client.users.resolveID(options.user), action_type: options.type, diff --git a/typings/index.d.ts b/typings/index.d.ts index c1dfb86b..3f9f222a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1744,7 +1744,6 @@ declare module 'discord.js' { interface GuildAuditLogsFetchOptions { before?: Snowflake | GuildAuditLogsEntry; - after?: Snowflake | GuildAuditLogsEntry; limit?: number; user?: UserResolvable; type?: string | number; From e62833b5e1d0ee9314c58792b639b3dbb2ea705b Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 19 Mar 2019 19:59:45 +0100 Subject: [PATCH 076/428] docs: mark getters as @ readonly --- src/client/voice/VoiceBroadcast.js | 1 + src/client/voice/VoiceConnection.js | 1 + src/client/voice/dispatcher/StreamDispatcher.js | 5 +++++ src/client/voice/networking/VoiceWebSocket.js | 1 + src/client/voice/util/VolumeInterface.js | 7 ++++--- src/stores/GuildEmojiRoleStore.js | 1 + src/stores/GuildMemberRoleStore.js | 1 + src/structures/ClientUser.js | 2 +- src/structures/DMChannel.js | 1 + src/structures/GuildMember.js | 1 + src/structures/Message.js | 1 + src/structures/Presence.js | 2 ++ src/structures/User.js | 1 + src/structures/VoiceChannel.js | 1 + src/structures/VoiceState.js | 5 +++++ 15 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 602f5920..27c8f7e2 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -34,6 +34,7 @@ class VoiceBroadcast extends EventEmitter { /** * The current master dispatcher, if any. This dispatcher controls all that is played by subscribed dispatchers. * @type {?BroadcastDispatcher} + * @readonly */ get dispatcher() { return this.player.dispatcher; diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index d0922ae8..727f2196 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -130,6 +130,7 @@ class VoiceConnection extends EventEmitter { /** * The client that instantiated this connection * @type {Client} + * @readonly */ get client() { return this.voiceManager.client; diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index e05cd49e..71e6e161 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -146,12 +146,14 @@ class StreamDispatcher extends Writable { /** * Whether or not playback is paused * @type {boolean} + * @readonly */ get paused() { return Boolean(this.pausedSince); } /** * Total time that this dispatcher has been paused * @type {number} + * @readonly */ get pausedTime() { return this._silentPausedTime + this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0); @@ -177,6 +179,7 @@ class StreamDispatcher extends Writable { /** * The time (in milliseconds) that the dispatcher has actually been playing audio for * @type {number} + * @readonly */ get streamTime() { return this.count * FRAME_LENGTH; @@ -185,6 +188,7 @@ class StreamDispatcher extends Writable { /** * The time (in milliseconds) that the dispatcher has been playing audio for, taking into account skips and pauses * @type {number} + * @readonly */ get totalStreamTime() { return Date.now() - this.startTime; @@ -322,6 +326,7 @@ class StreamDispatcher extends Writable { /** * Whether or not the Opus bitrate of this stream is editable * @type {boolean} + * @readonly */ get bitrateEditable() { return this.streams.opus && this.streams.opus.setBitrate; } diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 3720afa7..8f1e8340 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -32,6 +32,7 @@ class VoiceWebSocket extends EventEmitter { /** * The client of this voice WebSocket * @type {Client} + * @readonly */ get client() { return this.connection.voiceManager.client; diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index a631e6ca..ba162a94 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -15,6 +15,7 @@ class VolumeInterface extends EventEmitter { /** * Whether or not the volume of this stream is editable * @type {boolean} + * @readonly */ get volumeEditable() { return true; @@ -22,8 +23,8 @@ class VolumeInterface extends EventEmitter { /** * The current volume of the stream - * @readonly * @type {number} + * @readonly */ get volume() { return this._volume; @@ -31,8 +32,8 @@ class VolumeInterface extends EventEmitter { /** * The current volume of the stream in decibels - * @readonly * @type {number} + * @readonly */ get volumeDecibels() { return Math.log10(this.volume) * 20; @@ -40,8 +41,8 @@ class VolumeInterface extends EventEmitter { /** * The current volume of the stream from a logarithmic scale - * @readonly * @type {number} + * @readonly */ get volumeLogarithmic() { return Math.pow(this.volume, 1 / 1.660964); diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js index 8c097f56..2051161d 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/stores/GuildEmojiRoleStore.js @@ -20,6 +20,7 @@ class GuildEmojiRoleStore extends Collection { * The filtered collection of roles of the guild emoji * @type {Collection} * @private + * @readonly */ get _filtered() { return this.guild.roles.filter(role => this.emoji._roles.includes(role.id)); diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 9ab340d6..7b44a353 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -20,6 +20,7 @@ class GuildMemberRoleStore extends Collection { * The filtered collection of roles of the member * @type {Collection} * @private + * @readonly */ get _filtered() { const everyone = this.guild.defaultRole; diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index a86a6cb5..20bc42b9 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -30,8 +30,8 @@ class ClientUser extends Structures.get('User') { /** * ClientUser's presence - * @readonly * @type {Presence} + * @readonly */ get presence() { return this.client.presence; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index f1280e80..e58942c3 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -49,6 +49,7 @@ class DMChannel extends Channel { /** * Whether this DMChannel is a partial * @type {boolean} + * @readonly */ get partial() { return !this.recipient; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 91f54c8c..d479dd77 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -83,6 +83,7 @@ class GuildMember extends Base { /** * Whether this GuildMember is a partial * @type {boolean} + * @readonly */ get partial() { return !this.joinedTimestamp; diff --git a/src/structures/Message.js b/src/structures/Message.js index e302de26..afdb5e2c 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -172,6 +172,7 @@ class Message extends Base { /** * Whether or not this message is a partial * @type {boolean} + * @readonly */ get partial() { return typeof this.content !== 'string' || !this.author; diff --git a/src/structures/Presence.js b/src/structures/Presence.js index baacab13..5d82ac07 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -45,6 +45,7 @@ class Presence { /** * The user of this presence * @type {?User} + * @readonly */ get user() { return this.client.users.get(this.userID) || null; @@ -53,6 +54,7 @@ class Presence { /** * The member of this presence * @type {?GuildMember} + * @readonly */ get member() { return this.guild.members.get(this.userID) || null; diff --git a/src/structures/User.js b/src/structures/User.js index f510ee13..a0436f7e 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -78,6 +78,7 @@ class User extends Base { /** * Whether this User is a partial * @type {boolean} + * @readonly */ get partial() { return typeof this.username !== 'string'; diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 2d1fa956..8b49715f 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -30,6 +30,7 @@ class VoiceChannel extends GuildChannel { * The members in this voice channel * @type {Collection} * @name VoiceChannel#members + * @readonly */ get members() { const coll = new Collection(); diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 82f9eec7..9ff71e87 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -58,6 +58,7 @@ class VoiceState extends Base { /** * The member that this voice state belongs to * @type {?GuildMember} + * @readonly */ get member() { return this.guild.members.get(this.id) || null; @@ -66,6 +67,7 @@ class VoiceState extends Base { /** * The channel that the member is connected to * @type {?VoiceChannel} + * @readonly */ get channel() { return this.guild.channels.get(this.channelID) || null; @@ -74,6 +76,7 @@ class VoiceState extends Base { /** * Whether this member is either self-deafened or server-deafened * @type {?boolean} + * @readonly */ get deaf() { return this.serverDeaf || this.selfDeaf; @@ -82,6 +85,7 @@ class VoiceState extends Base { /** * Whether this member is either self-muted or server-muted * @type {?boolean} + * @readonly */ get mute() { return this.serverMute || this.selfMute; @@ -91,6 +95,7 @@ class VoiceState extends Base { * Whether this member is currently speaking. A boolean if the information is available (aka * the bot is connected to any voice channel in the guild), otherwise this is null * @type {?boolean} + * @readonly */ get speaking() { return this.channel && this.channel.connection ? From 2341d13615fb0a4649ba719c072661b65a4927a5 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 19 Mar 2019 19:32:11 +0000 Subject: [PATCH 077/428] fix: messageReactionRemove not emitting for partial messages (#3125) --- src/client/actions/Action.js | 13 +++++++++++++ src/client/actions/MessageReactionAdd.js | 9 +++++++++ src/client/actions/MessageReactionRemove.js | 3 +-- .../websocket/handlers/MESSAGE_REACTION_ADD.js | 11 +---------- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 09faae9e..0c798089 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -43,6 +43,19 @@ class GenericAction { }) : channel.messages.get(id)); } + + getReaction(data, message, user) { + const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); + const existing = message.reactions.get(emojiID); + if (!existing && this.client.options.partials.includes(PartialTypes.MESSAGE)) { + return message.reactions.add({ + emoji: data.emoji, + count: 0, + me: user.id === this.client.user.id, + }); + } + return existing; + } } module.exports = GenericAction; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index b7400cf7..185b980a 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -1,6 +1,7 @@ 'use strict'; const Action = require('./Action'); +const { Events } = require('../../util/Constants'); /* { user_id: 'id', @@ -31,6 +32,14 @@ class MessageReactionAdd extends Action { me: user.id === this.client.user.id, }); reaction._add(user); + /** + * Emitted whenever a reaction is added to a cached message. + * @event Client#messageReactionAdd + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user that applied the guild or reaction emoji + */ + this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); + return { message, reaction, user }; } } diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index e5d4f841..4e7995f8 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -26,8 +26,7 @@ class MessageReactionRemove extends Action { if (!message) return false; // Verify reaction - const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); - const reaction = message.reactions.get(emojiID); + const reaction = this.getReaction(data, message, user); if (!reaction) return false; reaction._remove(user); /** diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js index 6d0bbb05..e219b4a5 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -1,14 +1,5 @@ 'use strict'; -const { Events } = require('../../../util/Constants'); - module.exports = (client, packet) => { - const { user, reaction } = client.actions.MessageReactionAdd.handle(packet.d); - /** - * Emitted whenever a reaction is added to a cached message. - * @event Client#messageReactionAdd - * @param {MessageReaction} messageReaction The reaction object - * @param {User} user The user that applied the guild or reaction emoji - */ - if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); + client.actions.MessageReactionAdd.handle(packet.d); }; From 9b2bf03ff6041148a0c6c6351a2bd74c44b4166a Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 19 Mar 2019 19:35:58 +0000 Subject: [PATCH 078/428] feat(MessageStore): add cache parameter to fetchPinned() (#3154) * add cache param to MessageStore#fetchPinned() * typings for cache param * set cache to true by default --- src/stores/MessageStore.js | 5 +++-- typings/index.d.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index 52bf7fdd..db72114f 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -66,6 +66,7 @@ class MessageStore extends DataStore { * Fetches the pinned messages of this channel and returns a collection of them. * The returned Collection does not contain any reaction data of the messages. * Those need to be fetched separately. + * @param {boolean} [cache=true] Whether to cache the message(s) * @returns {Promise>} * @example * // Get pinned messages @@ -73,10 +74,10 @@ class MessageStore extends DataStore { * .then(messages => console.log(`Received ${messages.size} messages`)) * .catch(console.error); */ - fetchPinned() { + fetchPinned(cache = true) { return this.client.api.channels[this.channel.id].pins.get().then(data => { const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message)); + for (const message of data) messages.set(message.id, this.add(message, cache)); return messages; }); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 3f9f222a..e1a6bac8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1366,9 +1366,9 @@ declare module 'discord.js' { export class MessageStore extends DataStore { constructor(channel: TextChannel | DMChannel, iterable?: Iterable); - public fetch(message: Snowflake): Promise; - public fetch(options?: ChannelLogsQueryOptions): Promise>; - public fetchPinned(): Promise>; + public fetch(message: Snowflake, cache?: boolean): Promise; + public fetch(options?: ChannelLogsQueryOptions, cache?: boolean): Promise>; + public fetchPinned(cache?: boolean): Promise>; } export class PresenceStore extends DataStore { From 3df56540e2ca4adbd4ae018b2e296aeeb5bca0c3 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:22:55 +0000 Subject: [PATCH 079/428] voice: add documentation to VoiceBroadcast --- src/client/voice/VoiceBroadcast.js | 4 ++++ src/client/voice/dispatcher/BroadcastDispatcher.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 27c8f7e2..870798dd 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -27,6 +27,10 @@ class VoiceBroadcast extends EventEmitter { * @type {Client} */ this.client = client; + /** + * The dispatchers playing this broadcast + * @type {Set} + */ this.dispatchers = new DispatcherSet(this); this.player = new BroadcastAudioPlayer(this); } diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index e5c7650c..d7fd8dfa 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -29,6 +29,12 @@ class BroadcastDispatcher extends StreamDispatcher { super._destroy(err, cb); } + /** + * Set the bitrate of the current Opus encoder if using a compatible Opus stream. + * @param {number} value New bitrate, in kbps + * If set to 'auto', 48kbps will be used + * @returns {boolean} true if the bitrate has been successfully changed. + */ setBitrate(value) { if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false; const bitrate = value === 'auto' ? 48 : value; From c8225631c9f7906c0aa4966db701fe594074f27c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:36:53 +0000 Subject: [PATCH 080/428] voice: move broadcasts to client.voice --- src/client/Client.js | 17 --------- src/client/voice/ClientVoiceManager.js | 20 ++++++++++- src/client/voice/VoiceBroadcast.js | 35 +++++++++++++++++-- .../voice/dispatcher/StreamDispatcher.js | 2 +- src/client/voice/player/AudioPlayer.js | 2 +- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index b1dc1557..10272aee 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -144,12 +144,6 @@ class Client extends BaseClient { */ this.readyAt = null; - /** - * Active voice broadcasts that have been created - * @type {VoiceBroadcast[]} - */ - this.broadcasts = []; - if (this.options.messageSweepInterval > 0) { this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); } @@ -196,16 +190,6 @@ class Client extends BaseClient { return this.readyAt ? Date.now() - this.readyAt : null; } - /** - * Creates a voice broadcast. - * @returns {VoiceBroadcast} - */ - createVoiceBroadcast() { - const broadcast = new VoiceBroadcast(this); - this.broadcasts.push(broadcast); - return broadcast; - } - /** * Logs the client in, establishing a websocket connection to Discord. * @param {string} token Token of the account to log in with @@ -390,7 +374,6 @@ class Client extends BaseClient { toJSON() { return super.toJSON({ readyAt: false, - broadcasts: false, presences: false, }); } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 466b6d66..ba1e2a25 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -3,10 +3,11 @@ const Collection = require('../../util/Collection'); const { VoiceStatus } = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); +const VoiceBroadcast = require('./VoiceBroadcast'); const { Error } = require('../../errors'); /** - * Manages all the voice stuff for the client. + * Manages voice connections for the client * @private */ class ClientVoiceManager { @@ -22,6 +23,22 @@ class ClientVoiceManager { * @type {Collection} */ this.connections = new Collection(); + + /** + * Active voice broadcasts that have been created + * @type {VoiceBroadcast[]} + */ + this.broadcasts = []; + } + + /** + * Creates a voice broadcast. + * @returns {VoiceBroadcast} + */ + createVoiceBroadcast() { + const broadcast = new VoiceBroadcast(this); + this.broadcasts.push(broadcast); + return broadcast; } onVoiceServer({ guild_id, token, endpoint }) { @@ -46,6 +63,7 @@ class ClientVoiceManager { * Sets up a request to join a voice channel. * @param {VoiceChannel} channel The voice channel to join * @returns {Promise} + * @private */ joinChannel(channel) { return new Promise((resolve, reject) => { diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 870798dd..6cb2bcd1 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -2,7 +2,7 @@ const EventEmitter = require('events'); const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer'); -const DispatcherSet = require('./util/DispatcherSet'); +const { Events } = require('../../util/Constants'); const PlayInterface = require('./util/PlayInterface'); /** @@ -29,9 +29,9 @@ class VoiceBroadcast extends EventEmitter { this.client = client; /** * The dispatchers playing this broadcast - * @type {Set} + * @type {StreamDispatcher[]} */ - this.dispatchers = new DispatcherSet(this); + this.dispatchers = []; this.player = new BroadcastAudioPlayer(this); } @@ -60,6 +60,35 @@ class VoiceBroadcast extends EventEmitter { * @returns {BroadcastDispatcher} */ play() { return null; } + + add(dispatcher) { + const index = this.dispatchers.indexOf(dispatcher); + if (index === -1) { + /** + * Emitted whenever a stream dispatcher subscribes to the broadcast. + * @event VoiceBroadcast#subscribe + * @param {StreamDispatcher} dispatcher The subscribed dispatcher + */ + this.broadcast.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); + return true; + } else { + return false; + } + } + + delete(dispatcher) { + const index = this.dispatchers.indexOf(dispatcher); + if (index !== -1) { + /** + * Emitted whenever a stream dispatcher unsubscribes to the broadcast. + * @event VoiceBroadcast#unsubscribe + * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher + */ + this.broadcast.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); + return true; + } + return false; + } } PlayInterface.applyToClass(VoiceBroadcast); diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 71e6e161..340e40c5 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -122,7 +122,7 @@ class StreamDispatcher extends Writable { _cleanup() { if (this.player.dispatcher === this) this.player.dispatcher = null; const { streams } = this; - if (streams.broadcast) streams.broadcast.dispatchers.delete(this); + if (streams.broadcast) streams.broadcast.delete(this); if (streams.opus) streams.opus.destroy(); if (streams.ffmpeg) streams.ffmpeg.destroy(); } diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 3ce94d81..6f719a73 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -19,7 +19,7 @@ class AudioPlayer extends BasePlayer { playBroadcast(broadcast, options) { const dispatcher = this.createDispatcher(options, { broadcast }); - broadcast.dispatchers.add(dispatcher); + broadcast.add(dispatcher); return dispatcher; } } From 9a092b6e57908e763c67a07dcbab5826f6bceef7 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:37:55 +0000 Subject: [PATCH 081/428] voice: rename createVoiceBroadcast to createBroadcast --- src/client/voice/ClientVoiceManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index ba1e2a25..a8bf7992 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -35,7 +35,7 @@ class ClientVoiceManager { * Creates a voice broadcast. * @returns {VoiceBroadcast} */ - createVoiceBroadcast() { + createBroadcast() { const broadcast = new VoiceBroadcast(this); this.broadcasts.push(broadcast); return broadcast; From 6adb0a6609905318959a51f4e9d16b2c2a503987 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:52:18 +0000 Subject: [PATCH 082/428] voice: add ability to delete broadcasts --- src/client/voice/ClientVoiceManager.js | 1 - src/client/voice/VoiceBroadcast.js | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index a8bf7992..fa351364 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -8,7 +8,6 @@ const { Error } = require('../../errors'); /** * Manages voice connections for the client - * @private */ class ClientVoiceManager { constructor(client) { diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 6cb2bcd1..5446b46c 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -61,6 +61,16 @@ class VoiceBroadcast extends EventEmitter { */ play() { return null; } + + /** + * Ends the broadcast, unsubscribing all subscribed channels and deleting the broadcast + */ + end() { + for (const dispatcher of this.dispatchers) this.delete(dispatcher); + const index = this.client.voice.broadcasts.indexOf(this); + if (index !== -1) this.client.voice.broadcasts.splice(index, 1); + } + add(dispatcher) { const index = this.dispatchers.indexOf(dispatcher); if (index === -1) { @@ -79,6 +89,8 @@ class VoiceBroadcast extends EventEmitter { delete(dispatcher) { const index = this.dispatchers.indexOf(dispatcher); if (index !== -1) { + this.dispatchers.splice(index, 1); + dispatcher.destroy(); /** * Emitted whenever a stream dispatcher unsubscribes to the broadcast. * @event VoiceBroadcast#unsubscribe From b74a4356dd91ec3d3d54da09fdb4328ae4d2041c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:54:22 +0000 Subject: [PATCH 083/428] fix lint errors --- src/client/Client.js | 1 - src/client/voice/util/DispatcherSet.js | 42 -------------------------- 2 files changed, 43 deletions(-) delete mode 100644 src/client/voice/util/DispatcherSet.js diff --git a/src/client/Client.js b/src/client/Client.js index 10272aee..06b8fb48 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -11,7 +11,6 @@ const Webhook = require('../structures/Webhook'); const Invite = require('../structures/Invite'); const ClientApplication = require('../structures/ClientApplication'); const ShardClientUtil = require('../sharding/ShardClientUtil'); -const VoiceBroadcast = require('./voice/VoiceBroadcast'); const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); diff --git a/src/client/voice/util/DispatcherSet.js b/src/client/voice/util/DispatcherSet.js deleted file mode 100644 index 38a7c8b6..00000000 --- a/src/client/voice/util/DispatcherSet.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const { Events } = require('../../../util/Constants'); - -/** - * A "store" for handling broadcast dispatcher (un)subscription - * @private - */ -class DispatcherSet extends Set { - constructor(broadcast) { - super(); - /** - * The broadcast that this set belongs to - * @type {VoiceBroadcast} - */ - this.broadcast = broadcast; - } - - add(dispatcher) { - super.add(dispatcher); - /** - * Emitted whenever a stream dispatcher subscribes to the broadcast. - * @event VoiceBroadcast#subscribe - * @param {StreamDispatcher} dispatcher The subscribed dispatcher - */ - this.broadcast.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); - return this; - } - - delete(dispatcher) { - const ret = super.delete(dispatcher); - /** - * Emitted whenever a stream dispatcher unsubscribes to the broadcast. - * @event VoiceBroadcast#unsubscribe - * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher - */ - if (ret) this.broadcast.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); - return ret; - } -} - -module.exports = DispatcherSet; From 04fa56d0c07b66c77d0fdd6cb4419efb890043eb Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 24 Mar 2019 16:45:34 -0400 Subject: [PATCH 084/428] Improve structure extension errors --- src/util/Structures.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index 4ac56260..62646b40 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -45,17 +45,22 @@ class Structures { 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}` + `"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.'); + const received = `(received ${typeof extended})`; + throw new TypeError(`The extender function must return the extended structure class/prototype ${received}.`); } - if (Object.getPrototypeOf(extended) !== structures[structure]) { + + const prototype = Object.getPrototypeOf(extended); + if (prototype !== structures[structure]) { + const received = `${extended.name || 'unnamed'}${prototype.name ? ` extends ${prototype.name}` : ''}`; throw new Error( - 'The class/prototype returned from the extender function must extend the existing structure class/prototype.' + 'The class/prototype returned from the extender function must extend the existing structure class/prototype' + + ` (received function ${received}; expected extension of ${structures[structure].name}).` ); } From 4b6e8fcab5324392d9b91822006c05d2f4819a40 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Mon, 1 Apr 2019 18:36:03 +1100 Subject: [PATCH 085/428] typings(Collection): add missing thisArg to partition (#3167) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index e1a6bac8..f77ab51e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -289,7 +289,7 @@ declare module 'discord.js' { public lastKey(): K | undefined; public lastKey(count: number): K[]; public map(fn: (value: V, key: K, collection: Collection) => T, thisArg?: any): T[]; - public partition(fn: (value: V, key: K, collection: Collection) => boolean): [Collection, Collection]; + public partition(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): [Collection, Collection]; public random(): V | undefined; public random(count: number): V[]; public randomKey(): K | undefined; From 3f5161eb76b71ab8984e39ff39bf2937b82f5b43 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 1 Apr 2019 10:43:45 +0300 Subject: [PATCH 086/428] =?UTF-8?q?fix:=20Internal=20Sharding,=20this=20ti?= =?UTF-8?q?me=20fixed=E2=84=A2=20(#3140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * src: WIP Internal Sharding refactor * src: Refactor unavailable guild check Co-Authored-By: kyranet * src: More WIP Code F in the chat to the old manager * src: It should work but Discord says no. Seriously why is this not working! * fix: Inflator causing issues * src: Finishing touches and typings * misc: Proper debug message * fix: Making things hidden needs writable: true as well * fix: Sharding allowing multiple of the same shard, negative shards or strings * fix: Again... edge cases I love you guys .w. * misc: Touchups * misc: Better error? * docs: Typo * typings: Requested changes * src: Requested changes * src: Fix issues, validate provided shard options and more * src: Forgot to remove the listener * lint: eslint complaining * fix: Setting shardCount to auto crashing the process * misc: Requested changes * typings: Correct typings for shardCount client option * typings: Add invalidSession event to the shard and correct typings * src: Minor docs adjustements, and code consistency between setHelloTimeout and setHeartbeatTimeout * src: Don't block reconnect while creating shards Might fix silent disconnects *again* * src: Prevent reconnect from running if the Manager isn't READY That way, if a shard dies while we're still spawning, it won't cause issues * fix: Retry to reconnect if there's a network error going on. The manager *should* keep reconnecting unless the token is invalid * src: Enhance onClose handler for shards in the manager - If the close code is between 1000 and 2000 (inclusive), you cannot resume I tested this locally - If there's a session ID still present, immediately try to resume Faster resumes :papaBless: Otherwise, the invalid session event will trigger and it'll handle accordingly I swear if I see a SINGULAR Silent DC I'm yeeting * src: Fix error check * src: Make sure message exists on the error * src: Used the wrong property for the shardQueue * src: Make the hello timeout be made by the client god help * docs: Correct docs for WSEvents * misc: Remove old events from the Events constant * src: Throw the HTTP error if we don't get a 401 * typings: Can't forget about them * src: Implement some more fail safes just in case Seriously, better safe than sorry! Gotta failproof it completely --- package.json | 1 + src/client/Client.js | 80 ++-- src/client/websocket/WebSocketManager.js | 379 +++++++++++------ src/client/websocket/WebSocketShard.js | 495 +++++++++++++---------- src/errors/Messages.js | 2 +- src/rest/DiscordAPIError.js | 8 +- src/rest/RequestHandler.js | 2 +- src/util/Constants.js | 20 +- typings/index.d.ts | 97 ++++- 9 files changed, 671 insertions(+), 413 deletions(-) diff --git a/package.json b/package.json index 351e5cb4..fc5d8d1e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "devDependencies": { "@types/node": "^10.12.24", + "@types/ws": "^6.0.1", "discord.js-docgen": "discordjs/docgen", "eslint": "^5.13.0", "json-filter-loader": "^1.0.0", diff --git a/src/client/Client.js b/src/client/Client.js index 06b8fb48..73767f69 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -15,8 +15,7 @@ const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); -const { Events, WSCodes, browser, DefaultOptions } = require('../util/Constants'); -const { delayFor } = require('../util/Util'); +const { Events, browser, DefaultOptions } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); const Structures = require('../util/Structures'); const { Error, TypeError, RangeError } = require('../errors'); @@ -40,23 +39,33 @@ class Client extends BaseClient { } catch (_) { // Do nothing } + if (this.options.shards === DefaultOptions.shards) { if ('SHARDS' in data) { this.options.shards = JSON.parse(data.SHARDS); } } + if (this.options.totalShardCount === DefaultOptions.totalShardCount) { if ('TOTAL_SHARD_COUNT' in data) { this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); - } else if (Array.isArray(this.options.shards)) { + } else if (this.options.shards instanceof Array) { this.options.totalShardCount = this.options.shards.length; } else { this.options.totalShardCount = this.options.shardCount; } } - if (typeof this.options.shards === 'undefined' && this.options.shardCount) { - this.options.shards = []; - for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); + + if (typeof this.options.shards === 'undefined' && typeof this.options.shardCount === 'number') { + this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); + } + + if (typeof this.options.shards === 'number') this.options.shards = [this.options.shards]; + + if (typeof this.options.shards !== 'undefined') { + this.options.shards = [...new Set( + this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity) + )]; } this._validateOptions(); @@ -199,55 +208,21 @@ class Client extends BaseClient { async login(token = this.token) { if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); - this.emit(Events.DEBUG, `Authenticating using token ${token}`); - let endpoint = this.api.gateway; - if (this.options.shardCount === 'auto') endpoint = endpoint.bot; - const res = await endpoint.get(); + this.emit(Events.DEBUG, `Provided token: ${token}`); + if (this.options.presence) { this.options.ws.presence = await this.presence._parse(this.options.presence); } - if (res.session_start_limit && res.session_start_limit.remaining === 0) { - const { session_start_limit: { reset_after } } = res; - this.emit(Events.DEBUG, `Exceeded identify threshold, setting a timeout for ${reset_after} ms`); - await delayFor(reset_after); + + this.emit(Events.DEBUG, 'Preparing to connect to the gateway...'); + + try { + await this.ws.connect(); + return this.token; + } catch (error) { + this.destroy(); + throw error; } - const gateway = `${res.url}/`; - if (this.options.shardCount === 'auto') { - this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); - this.options.shardCount = res.shards; - this.options.totalShardCount = res.shards; - if (typeof this.options.shards === 'undefined' || !this.options.shards.length) { - this.options.shards = []; - for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); - } - } - this.emit(Events.DEBUG, `Using gateway ${gateway}`); - this.ws.connect(gateway); - await new Promise((resolve, reject) => { - const onready = () => { - clearTimeout(timeout); - this.removeListener(Events.DISCONNECT, ondisconnect); - resolve(); - }; - const ondisconnect = event => { - clearTimeout(timeout); - this.removeListener(Events.READY, onready); - this.destroy(); - if (WSCodes[event.code]) { - reject(new Error(WSCodes[event.code])); - } - }; - const timeout = setTimeout(() => { - this.removeListener(Events.READY, onready); - this.removeListener(Events.DISCONNECT, ondisconnect); - this.destroy(); - reject(new Error('WS_CONNECTION_TIMEOUT')); - }, this.options.shardCount * 25e3); - if (timeout.unref !== undefined) timeout.unref(); - this.once(Events.READY, onready); - this.once(Events.DISCONNECT, ondisconnect); - }); - return token; } /** @@ -397,9 +372,10 @@ class Client extends BaseClient { if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); } - if (options.shards && typeof options.shards !== 'number' && !Array.isArray(options.shards)) { + if (options.shards && !(options.shards instanceof Array)) { throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); } + if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS'); if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1'); if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index bc34b84b..c483e803 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,9 +1,10 @@ 'use strict'; +const { Error: DJSError } = require('../../errors'); const Collection = require('../../util/Collection'); const Util = require('../../util/Util'); const WebSocketShard = require('./WebSocketShard'); -const { Events, Status, WSEvents } = require('../../util/Constants'); +const { Events, ShardEvents, Status, WSCodes, WSEvents } = require('../../util/Constants'); const PacketHandlers = require('./handlers'); const BeforeReadyWhitelist = [ @@ -16,6 +17,8 @@ const BeforeReadyWhitelist = [ WSEvents.GUILD_MEMBER_REMOVE, ]; +const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011]; + /** * The WebSocket manager for this client. */ @@ -25,6 +28,7 @@ class WebSocketManager { * The client that instantiated this WebSocketManager * @type {Client} * @readonly + * @name WebSocketManager#client */ Object.defineProperty(this, 'client', { value: client }); @@ -34,6 +38,13 @@ class WebSocketManager { */ this.gateway = undefined; + /** + * The amount of shards this manager handles + * @private + * @type {number|string} + */ + this.totalShards = this.client.options.shardCount; + /** * A collection of all shards this manager handles * @type {Collection} @@ -41,18 +52,20 @@ class WebSocketManager { this.shards = new Collection(); /** - * An array of shards to be spawned or reconnected - * @type {Array} + * An array of shards to be connected or that need to reconnect + * @type {Set} * @private + * @name WebSocketManager#shardQueue */ - this.shardQueue = []; + Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true }); /** * An array of queued events before this WebSocketManager became ready * @type {object[]} * @private + * @name WebSocketManager#packetQueue */ - this.packetQueue = []; + Object.defineProperty(this, 'packetQueue', { value: [] }); /** * The current status of this WebSocketManager @@ -61,28 +74,28 @@ class WebSocketManager { this.status = Status.IDLE; /** - * If this manager is expected to close + * If this manager was destroyed. It will prevent shards from reconnecting * @type {boolean} * @private */ - this.expectingClose = false; + this.destroyed = false; + + /** + * If this manager is currently reconnecting one or multiple shards + * @type {boolean} + * @private + */ + this.reconnecting = false; /** * The current session limit of the client - * @type {?Object} * @private + * @type {?Object} * @prop {number} total Total number of identifies available * @prop {number} remaining Number of identifies remaining * @prop {number} reset_after Number of milliseconds after which the limit resets */ - this.sessionStartLimit = null; - - /** - * If the manager is currently reconnecting shards - * @type {boolean} - * @private - */ - this.isReconnectingShards = false; + this.sessionStartLimit = undefined; } /** @@ -96,121 +109,198 @@ class WebSocketManager { } /** - * Emits a debug event. - * @param {string} message Debug message + * Emits a debug message. + * @param {string} message The debug message + * @param {?WebSocketShard} [shard] The shard that emitted this message, if any * @private */ - debug(message) { - this.client.emit(Events.DEBUG, message); + debug(message, shard) { + this.client.emit(Events.DEBUG, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`); } /** - * Checks if a new identify payload can be sent. + * Connects this manager to the gateway. * @private - * @returns {Promise} */ - async _checkSessionLimit() { - this.sessionStartLimit = await this.client.api.gateway.bot.get().then(r => r.session_start_limit); - const { remaining, reset_after } = this.sessionStartLimit; - if (remaining !== 0) return true; - return reset_after; - } + async connect() { + const invalidToken = new DJSError(WSCodes[4004]); + const { + url: gatewayURL, + shards: recommendedShards, + session_start_limit: sessionStartLimit, + } = await this.client.api.gateway.bot.get().catch(error => { + throw error.httpStatus === 401 ? invalidToken : error; + }); - /** - * Handles the session identify rate limit for creating a shard. - * @private - */ - async _handleSessionLimit() { - const canSpawn = await this._checkSessionLimit(); - if (typeof canSpawn === 'number') { - this.debug(`Exceeded identify threshold, setting a timeout for ${canSpawn}ms`); - await Util.delayFor(canSpawn); + this.sessionStartLimit = sessionStartLimit; + + const { total, remaining, reset_after } = sessionStartLimit; + + this.debug(`Fetched Gateway Information + URL: ${gatewayURL} + Recommended Shards: ${recommendedShards}`); + + this.debug(`Session Limit Information + Total: ${total} + Remaining: ${remaining}`); + + this.gateway = `${gatewayURL}/`; + + if (this.totalShards === 'auto') { + this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); + this.totalShards = this.client.options.shardCount = this.client.options.totalShardCount = recommendedShards; + if (typeof this.client.options.shards === 'undefined' || !this.client.options.shards.length) { + this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); + } } - this.create(); - } - /** - * Creates a connection to a gateway. - * @param {string} [gateway=this.gateway] The gateway to connect to - * @private - */ - connect(gateway = this.gateway) { - this.gateway = gateway; - - if (typeof this.client.options.shards === 'number') { - this.debug(`Spawning shard with ID ${this.client.options.shards}`); - this.shardQueue.push(this.client.options.shards); - } else if (Array.isArray(this.client.options.shards)) { - this.debug(`Spawning ${this.client.options.shards.length} shards`); - this.shardQueue.push(...this.client.options.shards); + if (this.client.options.shards instanceof Array) { + const { shards } = this.client.options; + this.totalShards = shards.length; + this.debug(`Spawning shards: ${shards.join(', ')}`); + this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); } else { - this.debug(`Spawning ${this.client.options.shardCount} shards`); - this.shardQueue.push(...Array.from({ length: this.client.options.shardCount }, (_, index) => index)); + this.debug(`Spawning ${this.totalShards} shards`); + this.shardQueue = new Set(Array.from({ length: this.totalShards }, (_, id) => new WebSocketShard(this, id))); } - this.create(); + + await this._handleSessionLimit(remaining, reset_after); + + return this.createShards(); } /** - * Creates or reconnects a shard. + * Handles the creation of a shard. + * @returns {Promise} * @private */ - create() { - // Nothing to create - if (!this.shardQueue.length) return; + async createShards() { + // If we don't have any shards to handle, return + if (!this.shardQueue.size) return false; - let item = this.shardQueue.shift(); - if (typeof item === 'string' && !isNaN(item)) item = Number(item); + const [shard] = this.shardQueue; - if (item instanceof WebSocketShard) { - const timeout = setTimeout(() => { - this.debug(`[Shard ${item.id}] Failed to connect in 15s... Destroying and trying again`); - item.destroy(); - if (!this.shardQueue.includes(item)) this.shardQueue.push(item); - this.reconnect(true); - }, 15000); - item.once(Events.READY, this._shardReady.bind(this, timeout)); - item.once(Events.RESUMED, this._shardReady.bind(this, timeout)); - item.connect(); - return; + this.shardQueue.delete(shard); + + if (!shard.eventsAttached) { + shard.on(ShardEvents.READY, () => { + /** + * Emitted when a shard turns ready. + * @event Client#shardReady + * @param {number} id The shard ID that turned ready + */ + this.client.emit(Events.SHARD_READY, shard.id); + + if (!this.shardQueue.size) this.reconnecting = false; + }); + + shard.on(ShardEvents.RESUMED, () => { + /** + * Emitted when a shard resumes successfully. + * @event Client#shardResumed + * @param {number} id The shard ID that resumed + */ + this.client.emit(Events.SHARD_RESUMED, shard.id); + }); + + shard.on(ShardEvents.CLOSE, event => { + if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { + /** + * Emitted when a shard's WebSocket disconnects and will no longer reconnect. + * @event Client#shardDisconnected + * @param {CloseEvent} event The WebSocket close event + * @param {number} id The shard ID that disconnected + */ + this.client.emit(Events.SHARD_DISCONNECTED, event, shard.id); + this.debug(WSCodes[event.code], shard); + return; + } + + if (event.code >= 1000 && event.code <= 2000) { + // Any event code in this range cannot be resumed. + shard.sessionID = undefined; + } + + /** + * Emitted when a shard is attempting to reconnect or re-identify. + * @event Client#shardReconnecting + * @param {number} id The shard ID that is attempting to reconnect + */ + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + + if (shard.sessionID) { + this.debug(`Session ID is present, attempting an immediate reconnect...`, shard); + shard.connect().catch(() => null); + return; + } + + shard.destroy(); + + this.shardQueue.add(shard); + this.reconnect(); + }); + + shard.on(ShardEvents.INVALID_SESSION, () => { + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + + this.shardQueue.add(shard); + this.reconnect(); + }); + + shard.on(ShardEvents.DESTROYED, () => { + this.debug('Shard was destroyed but no WebSocket connection existed... Reconnecting...', shard); + + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + + this.shardQueue.add(shard); + this.reconnect(); + }); + + shard.eventsAttached = true; } - const shard = new WebSocketShard(this, item); - this.shards.set(item, shard); - shard.once(Events.READY, this._shardReady.bind(this)); + this.shards.set(shard.id, shard); + + try { + await shard.connect(); + } catch (error) { + if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) { + throw new DJSError(WSCodes[error.code]); + } else { + this.debug('Failed to connect to the gateway, requeueing...', shard); + this.shardQueue.add(shard); + } + } + // If we have more shards, add a 5s delay + if (this.shardQueue.size) { + this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`); + await Util.delayFor(5000); + await this._handleSessionLimit(); + return this.createShards(); + } + + return true; } /** - * Shared handler for shards turning ready or resuming. - * @param {Timeout} [timeout=null] Optional timeout to clear if shard didn't turn ready in time + * Handles reconnects for this manager. * @private + * @returns {Promise} */ - _shardReady(timeout = null) { - if (timeout) clearTimeout(timeout); - if (this.shardQueue.length) { - this.client.setTimeout(this._handleSessionLimit.bind(this), 5000); - } else { - this.isReconnectingShards = false; - } - } - - /** - * Handles the reconnect of a shard. - * @param {WebSocketShard|boolean} shard The shard to reconnect, or a boolean to indicate an immediate reconnect - * @private - */ - async reconnect(shard) { - // If the item is a shard, add it to the queue - if (shard instanceof WebSocketShard) this.shardQueue.push(shard); - if (typeof shard === 'boolean') { - // If a boolean is passed, force a reconnect right now - } else if (this.isReconnectingShards) { - // If we're already reconnecting shards, and no boolean was provided, return - return; - } - this.isReconnectingShards = true; + async reconnect() { + if (this.reconnecting || this.status !== Status.READY) return false; + this.reconnecting = true; try { await this._handleSessionLimit(); + await this.createShards(); } catch (error) { + this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); + if (error.httpStatus !== 401) { + this.debug(`Possible network error occured. Retrying in 5s...`); + await Util.delayFor(5000); + this.reconnecting = false; + return this.reconnect(); + } // If we get an error at this point, it means we cannot reconnect anymore if (this.client.listenerCount(Events.INVALIDATED)) { /** @@ -225,6 +315,52 @@ class WebSocketManager { } else { this.client.destroy(); } + } finally { + this.reconnecting = false; + } + return true; + } + + /** + * Broadcasts a packet to every shard this manager handles. + * @param {Object} packet The packet to send + * @private + */ + broadcast(packet) { + for (const shard of this.shards.values()) shard.send(packet); + } + + /** + * Destroys this manager and all its shards. + * @private + */ + destroy() { + if (this.destroyed) return; + this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); + this.destroyed = true; + this.shardQueue.clear(); + for (const shard of this.shards.values()) shard.destroy(); + } + + /** + * Handles the timeout required if we cannot identify anymore. + * @param {number} [remaining] The amount of remaining identify sessions that can be done today + * @param {number} [resetAfter] The amount of time in which the identify counter resets + * @private + */ + async _handleSessionLimit(remaining, resetAfter) { + if (typeof remaining === 'undefined' && typeof resetAfter === 'undefined') { + const { session_start_limit } = await this.client.api.gateway.bot.get(); + this.sessionStartLimit = session_start_limit; + remaining = session_start_limit.remaining; + resetAfter = session_start_limit.reset_after; + this.debug(`Session Limit Information + Total: ${session_start_limit.total} + Remaining: ${remaining}`); + } + if (!remaining) { + this.debug(`Exceeded identify threshold. Will attempt a connection in ${resetAfter}ms`); + await Util.delayFor(resetAfter); } } @@ -263,15 +399,13 @@ class WebSocketManager { * @private */ checkReady() { - if (this.shards.size !== this.client.options.shardCount || - this.shards.some(s => s.status !== Status.READY)) { + if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) { return false; } - let unavailableGuilds = 0; - for (const guild of this.client.guilds.values()) { - if (!guild.available) unavailableGuilds++; - } + const unavailableGuilds = this.client.guilds.reduce((acc, guild) => guild.available ? acc : acc + 1, 0); + + // TODO: Rethink implementation for this if (unavailableGuilds === 0) { this.status = Status.NEARLY; if (!this.client.options.fetchAllMembers) return this.triggerReady(); @@ -280,16 +414,18 @@ class WebSocketManager { Promise.all(promises) .then(() => this.triggerReady()) .catch(e => { - this.debug(`Failed to fetch all members before ready! ${e}`); + this.debug(`Failed to fetch all members before ready! ${e}\n${e.stack}`); this.triggerReady(); }); + } else { + this.debug(`There are ${unavailableGuilds} unavailable guilds. Waiting for their GUILD_CREATE packets`); } + return true; } /** * Causes the client to be marked as ready and emits the ready event. - * @returns {void} * @private */ triggerReady() { @@ -303,31 +439,10 @@ class WebSocketManager { * Emitted when the client becomes ready to start working. * @event Client#ready */ - this.client.emit(Events.READY); + this.client.emit(Events.CLIENT_READY); this.handlePacket(); } - - /** - * Broadcasts a message to every shard in this WebSocketManager. - * @param {*} packet The packet to send - * @private - */ - broadcast(packet) { - for (const shard of this.shards.values()) shard.send(packet); - } - - /** - * Destroys all shards. - * @private - */ - destroy() { - if (this.expectingClose) return; - this.expectingClose = true; - this.isReconnectingShards = false; - this.shardQueue.length = 0; - for (const shard of this.shards.values()) shard.destroy(); - } } module.exports = WebSocketManager; diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 96ab06a4..19cb11e4 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -2,8 +2,7 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); -const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); -const Util = require('../../util/Util'); +const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); let zlib; try { @@ -21,13 +20,13 @@ class WebSocketShard extends EventEmitter { super(); /** - * The WebSocket Manager of this connection + * The WebSocketManager of the shard * @type {WebSocketManager} */ this.manager = manager; /** - * The ID of the this shard + * The ID of the shard * @type {number} */ this.id = id; @@ -91,20 +90,22 @@ class WebSocketShard extends EventEmitter { * @type {Object} * @private */ - this.ratelimit = { - queue: [], - total: 120, - remaining: 120, - time: 60e3, - timer: null, - }; + Object.defineProperty(this, 'ratelimit', { + value: { + queue: [], + total: 120, + remaining: 120, + time: 60e3, + timer: null, + }, + }); /** * The WebSocket connection for the current shard * @type {?WebSocket} * @private */ - this.connection = null; + Object.defineProperty(this, 'connection', { value: null, writable: true }); /** * @external Inflate @@ -116,9 +117,21 @@ class WebSocketShard extends EventEmitter { * @type {?Inflate} * @private */ - this.inflate = null; + Object.defineProperty(this, 'inflate', { value: null, writable: true }); - if (this.manager.gateway) this.connect(); + /** + * The HELLO timeout + * @type {?NodeJS.Timer} + * @private + */ + Object.defineProperty(this, 'helloTimeout', { value: null, writable: true }); + + /** + * If the manager attached its event handlers on the shard + * @type {boolean} + * @private + */ + Object.defineProperty(this, 'eventsAttached', { value: false, writable: true }); } /** @@ -133,82 +146,86 @@ class WebSocketShard extends EventEmitter { /** * Emits a debug event. - * @param {string} message Debug message + * @param {string} message The debug message * @private */ debug(message) { - this.manager.debug(`[Shard ${this.id}] ${message}`); + this.manager.debug(message, this); } /** - * Sends a heartbeat to the WebSocket. - * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect - * @private - */ - sendHeartbeat() { - if (!this.lastHeartbeatAcked) { - this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting."); - this.connection.close(4000); - return; - } - this.debug('Sending a heartbeat'); - this.lastHeartbeatAcked = false; - this.lastPingTimestamp = Date.now(); - this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }); - } - - /** - * Sets the heartbeat timer for this shard. - * @param {number} time If -1, clears the interval, any other number sets an interval - * @private - */ - setHeartbeatTimer(time) { - if (time === -1) { - if (this.heartbeatInterval) { - this.debug('Clearing heartbeat interval'); - this.manager.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } - return; - } - this.debug(`Setting a heartbeat interval for ${time}ms`); - this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); - } - - /** - * Acknowledges a heartbeat. - * @private - */ - ackHeartbeat() { - this.lastHeartbeatAcked = true; - const latency = Date.now() - this.lastPingTimestamp; - this.debug(`Heartbeat acknowledged, latency of ${latency}ms`); - this.pings.unshift(latency); - if (this.pings.length > 3) this.pings.length = 3; - } - - /** - * Connects this shard to the gateway. + * Connects the shard to the gateway. * @private + * @returns {Promise} A promise that will resolve if the shard turns ready successfully, + * or reject if we couldn't connect */ connect() { - const { expectingClose, gateway } = this.manager; - if (expectingClose) return; - this.inflate = new zlib.Inflate({ - chunkSize: 65535, - flush: zlib.Z_SYNC_FLUSH, - to: WebSocket.encoding === 'json' ? 'string' : '', + const { gateway, client } = this.manager; + + if (this.status === Status.READY && this.connection && this.connection.readyState === WebSocket.OPEN) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const onReady = () => { + this.off(ShardEvents.CLOSE, onClose); + this.off(ShardEvents.RESUMED, onResumed); + this.off(ShardEvents.INVALID_SESSION, onInvalid); + resolve(); + }; + + const onResumed = () => { + this.off(ShardEvents.CLOSE, onClose); + this.off(ShardEvents.READY, onReady); + this.off(ShardEvents.INVALID_SESSION, onInvalid); + resolve(); + }; + + const onClose = event => { + this.off(ShardEvents.READY, onReady); + this.off(ShardEvents.RESUMED, onResumed); + this.off(ShardEvents.INVALID_SESSION, onInvalid); + reject(event); + }; + + const onInvalid = () => { + this.off(ShardEvents.READY, onReady); + this.off(ShardEvents.RESUMED, onResumed); + this.off(ShardEvents.CLOSE, onClose); + // eslint-disable-next-line prefer-promise-reject-errors + reject(); + }; + + this.once(ShardEvents.READY, onReady); + this.once(ShardEvents.RESUMED, onResumed); + this.once(ShardEvents.CLOSE, onClose); + this.once(ShardEvents.INVALID_SESSION, onInvalid); + + if (this.connection && this.connection.readyState === WebSocket.OPEN) { + this.identifyNew(); + return; + } + + this.inflate = new zlib.Inflate({ + chunkSize: 65535, + flush: zlib.Z_SYNC_FLUSH, + to: WebSocket.encoding === 'json' ? 'string' : '', + }); + + this.debug(`Trying to connect to ${gateway}, version ${client.options.ws.version}`); + + this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; + this.setHelloTimeout(); + + const ws = this.connection = WebSocket.create(gateway, { + v: client.options.ws.version, + compress: 'zlib-stream', + }); + ws.onopen = this.onOpen.bind(this); + ws.onmessage = this.onMessage.bind(this); + ws.onerror = this.onError.bind(this); + ws.onclose = this.onClose.bind(this); }); - this.debug(`Connecting to ${gateway}`); - const ws = this.connection = WebSocket.create(gateway, { - v: this.manager.client.options.ws.version, - compress: 'zlib-stream', - }); - ws.onopen = this.onOpen.bind(this); - ws.onmessage = this.onMessage.bind(this); - ws.onerror = this.onError.bind(this); - ws.onclose = this.onClose.bind(this); - this.status = Status.CONNECTING; } /** @@ -216,7 +233,8 @@ class WebSocketShard extends EventEmitter { * @private */ onOpen() { - this.debug('Connected to the gateway'); + this.debug('Opened a connection to the gateway successfully.'); + this.status = Status.NEARLY; } /** @@ -240,53 +258,106 @@ class WebSocketShard extends EventEmitter { packet = WebSocket.unpack(this.inflate.result); this.manager.client.emit(Events.RAW, packet, this.id); } catch (err) { - this.manager.client.emit(Events.ERROR, err); + this.manager.client.emit(Events.SHARD_ERROR, err, this.id); return; } this.onPacket(packet); } + /** + * Called whenever an error occurs with the WebSocket. + * @param {ErrorEvent} error The error that occurred + * @private + */ + onError({ error }) { + if (error && error.message === 'uWs client connection error') { + this.debug('Received a uWs error. Closing the connection and reconnecting...'); + this.connection.close(4000); + return; + } + + /** + * Emitted whenever a shard's WebSocket encounters a connection error. + * @event Client#shardError + * @param {Error} error The encountered error + * @param {number} shardID The shard that encountered this error + */ + this.manager.client.emit(Events.SHARD_ERROR, error, this.id); + } + + /** + * @external CloseEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} + */ + + /** + * @external ErrorEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent} + */ + + /** + * Called whenever a connection to the gateway is closed. + * @param {CloseEvent} event Close event that was received + * @private + */ + onClose(event) { + this.closeSequence = this.sequence; + this.sequence = -1; + this.debug(`WebSocket was closed. + Event Code: ${event.code} + Clean: ${event.wasClean} + Reason: ${event.reason || 'No reason received'}`); + + this.status = Status.DISCONNECTED; + + /** + * Emitted when a shard's WebSocket closes. + * @private + * @event WebSocketShard#close + * @param {CloseEvent} event The received event + */ + this.emit(ShardEvents.CLOSE, event); + } + /** * Called whenever a packet is received. - * @param {Object} packet Packet received + * @param {Object} packet The received packet * @private */ onPacket(packet) { if (!packet) { - this.debug('Received null or broken packet'); + this.debug(`Received broken packet: ${packet}.`); return; } switch (packet.t) { case WSEvents.READY: /** - * Emitted when a shard becomes ready. + * Emitted when the shard becomes ready * @event WebSocketShard#ready */ - this.emit(Events.READY); - /** - * Emitted when a shard becomes ready. - * @event Client#shardReady - * @param {number} shardID The ID of the shard - */ - this.manager.client.emit(Events.SHARD_READY, this.id); + this.emit(ShardEvents.READY); this.sessionID = packet.d.session_id; this.trace = packet.d._trace; this.status = Status.READY; - this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}`); + this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}.`); this.lastHeartbeatAcked = true; this.sendHeartbeat(); break; case WSEvents.RESUMED: { - this.emit(Events.RESUMED); + /** + * Emitted when the shard resumes successfully + * @event WebSocketShard#resumed + */ + this.emit(ShardEvents.RESUMED); + this.trace = packet.d._trace; this.status = Status.READY; const replayed = packet.s - this.closeSequence; - this.debug(`RESUMED ${this.trace.join(' -> ')} | Replayed ${replayed} events.`); + this.debug(`RESUMED ${this.trace.join(' -> ')} | Session ${this.sessionID} | Replayed ${replayed} events.`); this.lastHeartbeatAcked = true; this.sendHeartbeat(); - break; } } @@ -294,6 +365,7 @@ class WebSocketShard extends EventEmitter { switch (packet.op) { case OPCodes.HELLO: + this.setHelloTimeout(-1); this.setHeartbeatTimer(packet.d.heartbeat_interval); this.identify(); break; @@ -301,21 +373,20 @@ class WebSocketShard extends EventEmitter { this.connection.close(1001); break; case OPCodes.INVALID_SESSION: - this.debug(`Session was invalidated. Resumable: ${packet.d}.`); - // If the session isn't resumable - if (!packet.d) { - // Reset the sequence, since it isn't valid anymore - this.sequence = -1; - // If we had a session ID before - if (this.sessionID) { - this.sessionID = null; - this.connection.close(1000); - return; - } - this.connection.close(1000); + this.debug(`Session invalidated. Resumable: ${packet.d}.`); + // If we can resume the session, do so immediately + if (packet.d) { + this.identifyResume(); return; } - this.identifyResume(); + // Reset the sequence + this.sequence = -1; + // Reset the session ID as it's invalid + this.sessionID = null; + // Set the status to reconnecting + this.status = Status.RECONNECTING; + // Finally, emit the INVALID_SESSION event + this.emit(ShardEvents.INVALID_SESSION); break; case OPCodes.HEARTBEAT_ACK: this.ackHeartbeat(); @@ -329,10 +400,78 @@ class WebSocketShard extends EventEmitter { } /** - * Identifies the client on a connection. - * @returns {void} + * Sets the HELLO packet timeout. + * @param {number} [time] If set to -1, it will clear the hello timeout timeout * @private */ + setHelloTimeout(time) { + if (time === -1) { + if (this.helloTimeout) { + this.debug('Clearing the HELLO timeout.'); + this.manager.client.clearTimeout(this.helloTimeout); + this.helloTimeout = null; + } + return; + } + this.debug('Setting a HELLO timeout for 20s.'); + this.helloTimeout = this.manager.client.setTimeout(() => { + this.debug('Did not receive HELLO in time. Destroying and connecting again.'); + this.destroy(4009); + }, 20000); + } + + /** + * Sets the heartbeat timer for this shard. + * @param {number} time If -1, clears the interval, any other number sets an interval + * @private + */ + setHeartbeatTimer(time) { + if (time === -1) { + if (this.heartbeatInterval) { + this.debug('Clearing the heartbeat interval.'); + this.manager.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + return; + } + this.debug(`Setting a heartbeat interval for ${time}ms.`); + this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); + } + + /** + * Sends a heartbeat to the WebSocket. + * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect + * @private + */ + sendHeartbeat() { + if (!this.lastHeartbeatAcked) { + this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting."); + this.destroy(4009); + return; + } + this.debug('Sending a heartbeat.'); + this.lastHeartbeatAcked = false; + this.lastPingTimestamp = Date.now(); + this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }, true); + } + + /** + * Acknowledges a heartbeat. + * @private + */ + ackHeartbeat() { + this.lastHeartbeatAcked = true; + const latency = Date.now() - this.lastPingTimestamp; + this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`); + this.pings.unshift(latency); + if (this.pings.length > 3) this.pings.length = 3; + } + + /** + * Identifies the client on the connection. + * @private + * @returns {void} + */ identify() { return this.sessionID ? this.identifyResume() : this.identifyNew(); } @@ -342,31 +481,32 @@ class WebSocketShard extends EventEmitter { * @private */ identifyNew() { - if (!this.manager.client.token) { - this.debug('No token available to identify a new session with'); + const { client } = this.manager; + if (!client.token) { + this.debug('No token available to identify a new session.'); return; } - // Clone the generic payload and assign the token + + // Clone the identify payload and assign the token and shard info const d = { - ...this.manager.client.options.ws, - token: this.manager.client.token, - shard: [this.id, Number(this.manager.client.options.totalShardCount)], + ...client.options.ws, + token: client.token, + shard: [this.id, Number(client.options.totalShardCount)], }; - // Send the payload - this.debug('Identifying as a new session'); - this.send({ op: OPCodes.IDENTIFY, d }); + this.debug(`Identifying as a new session. Shard ${this.id}/${client.options.totalShardCount}`); + this.send({ op: OPCodes.IDENTIFY, d }, true); } /** * Resumes a session on the gateway. - * @returns {void} * @private */ identifyResume() { if (!this.sessionID) { - this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead'); - return this.identifyNew(); + this.debug('Warning: attempted to resume but no session ID was present; identifying as a new session.'); + this.identifyNew(); + return; } this.debug(`Attempting to resume session ${this.sessionID} at sequence ${this.closeSequence}`); @@ -377,85 +517,19 @@ class WebSocketShard extends EventEmitter { seq: this.closeSequence, }; - return this.send({ op: OPCodes.RESUME, d }); + this.send({ op: OPCodes.RESUME, d }, true); } /** - * Called whenever an error occurs with the WebSocket. - * @param {Error} error The error that occurred - * @private + * Adds a packet to the queue to be sent to the gateway. + * If you use this method, make sure you understand that you need to provide + * a full [Payload](https://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands). + * Do not use this method if you don't know what you're doing. + * @param {Object} data The full packet to send + * @param {?boolean} [important=false] If this packet should be added first in queue */ - onError(error) { - if (error && error.message === 'uWs client connection error') { - this.connection.close(4000); - return; - } - - /** - * Emitted whenever the client's WebSocket encounters a connection error. - * @event Client#error - * @param {Error} error The encountered error - * @param {number} shardID The shard that encountered this error - */ - this.manager.client.emit(Events.ERROR, error, this.id); - } - - /** - * @external CloseEvent - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} - */ - - /** - * Called whenever a connection to the gateway is closed. - * @param {CloseEvent} event Close event that was received - * @private - */ - onClose(event) { - this.closeSequence = this.sequence; - this.debug(`WebSocket was closed. - Event Code: ${event.code} - Reason: ${event.reason}`); - - if (event.code === 1000 ? this.manager.expectingClose : WSCodes[event.code]) { - /** - * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. - * @event Client#disconnect - * @param {CloseEvent} event The WebSocket close event - * @param {number} shardID The shard that disconnected - */ - this.manager.client.emit(Events.DISCONNECT, event, this.id); - this.debug(WSCodes[event.code]); - return; - } - - this.destroy(); - - this.status = Status.RECONNECTING; - - /** - * Emitted whenever a shard tries to reconnect to the WebSocket. - * @event Client#reconnecting - * @param {number} shardID The shard ID that is reconnecting - */ - this.manager.client.emit(Events.RECONNECTING, this.id); - - this.debug(`${this.sessionID ? `Reconnecting in 3500ms` : 'Queueing a reconnect'} to the gateway...`); - - if (this.sessionID) { - Util.delayFor(3500).then(() => this.connect()); - } else { - this.manager.reconnect(this); - } - } - - /** - * Adds data to the queue to be sent. - * @param {Object} data Packet to send - * @private - * @returns {void} - */ - send(data) { - this.ratelimit.queue.push(data); + send(data, important = false) { + this.ratelimit.queue[important ? 'unshift' : 'push'](data); this.processQueue(); } @@ -472,7 +546,7 @@ class WebSocketShard extends EventEmitter { } this.connection.send(WebSocket.pack(data), err => { - if (err) this.manager.client.emit(Events.ERROR, err); + if (err) this.manager.client.emit(Events.SHARD_ERROR, err, this.id); }); } @@ -499,21 +573,36 @@ class WebSocketShard extends EventEmitter { } /** - * Destroys this shard and closes its connection. + * Destroys this shard and closes its WebSocket connection. + * @param {?number} [closeCode=1000] The close code to use * @private */ - destroy() { + destroy(closeCode = 1000) { this.setHeartbeatTimer(-1); - if (this.connection) this.connection.close(1000); + this.setHelloTimeout(-1); + // Close the WebSocket connection, if any + if (this.connection) { + this.connection.close(closeCode); + } else { + /** + * Emitted when a shard is destroyed, but no WebSocket connection was present. + * @private + * @event WebSocketShard#destroyed + */ + this.emit(ShardEvents.DESTROYED); + } this.connection = null; + // Set the shard status this.status = Status.DISCONNECTED; + // Reset the sequence + this.sequence = -1; + // Reset the ratelimit data this.ratelimit.remaining = this.ratelimit.total; this.ratelimit.queue.length = 0; if (this.ratelimit.timer) { this.manager.client.clearTimeout(this.ratelimit.timer); this.ratelimit.timer = null; } - this.sequence = -1; } } diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 5945c167..1cfe4da9 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -4,12 +4,12 @@ const { register } = require('./DJSError'); const Messages = { CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`, + CLIENT_INVALID_PROVIDED_SHARDS: 'None of the provided shards were valid.', TOKEN_INVALID: 'An invalid token was provided.', TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.', - WS_CONNECTION_TIMEOUT: 'The connection to the gateway timed out.', WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, diff --git a/src/rest/DiscordAPIError.js b/src/rest/DiscordAPIError.js index 559194f2..3ccd2b33 100644 --- a/src/rest/DiscordAPIError.js +++ b/src/rest/DiscordAPIError.js @@ -5,7 +5,7 @@ * @extends Error */ class DiscordAPIError extends Error { - constructor(path, error, method) { + constructor(path, error, method, status) { super(); const flattened = this.constructor.flattenErrors(error.errors || error).join('\n'); this.name = 'DiscordAPIError'; @@ -28,6 +28,12 @@ class DiscordAPIError extends Error { * @type {number} */ this.code = error.code; + + /** + * The HTTP status code + * @type {number} + */ + this.httpStatus = status; } /** diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 44fca4fb..45065a66 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -167,7 +167,7 @@ class RequestHandler { try { const data = await parseResponse(res); if (res.status >= 400 && res.status < 500) { - return reject(new DiscordAPIError(request.path, data, request.method)); + return reject(new DiscordAPIError(request.path, data, request.method, res.status)); } return null; } catch (err) { diff --git a/src/util/Constants.js b/src/util/Constants.js index b6c1b035..dfd370f7 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -212,8 +212,7 @@ exports.VoiceOPCodes = { exports.Events = { RATE_LIMIT: 'rateLimit', - READY: 'ready', - RESUMED: 'resumed', + CLIENT_READY: 'ready', GUILD_CREATE: 'guildCreate', GUILD_DELETE: 'guildDelete', GUILD_UPDATE: 'guildUpdate', @@ -246,8 +245,6 @@ exports.Events = { MESSAGE_REACTION_REMOVE: 'messageReactionRemove', MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', USER_UPDATE: 'userUpdate', - USER_NOTE_UPDATE: 'userNoteUpdate', - USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate', PRESENCE_UPDATE: 'presenceUpdate', VOICE_SERVER_UPDATE: 'voiceServerUpdate', VOICE_STATE_UPDATE: 'voiceStateUpdate', @@ -256,16 +253,26 @@ exports.Events = { TYPING_START: 'typingStart', TYPING_STOP: 'typingStop', WEBHOOKS_UPDATE: 'webhookUpdate', - DISCONNECT: 'disconnect', - RECONNECTING: 'reconnecting', ERROR: 'error', WARN: 'warn', DEBUG: 'debug', + SHARD_DISCONNECTED: 'shardDisconnected', + SHARD_ERROR: 'shardError', + SHARD_RECONNECTING: 'shardReconnecting', SHARD_READY: 'shardReady', + SHARD_RESUMED: 'shardResumed', INVALIDATED: 'invalidated', RAW: 'raw', }; +exports.ShardEvents = { + CLOSE: 'close', + DESTROYED: 'destroyed', + INVALID_SESSION: 'invalidSession', + READY: 'ready', + RESUMED: 'resumed', +}; + /** * The type of Structure allowed to be a partial: * * USER @@ -312,7 +319,6 @@ exports.PartialTypes = keyMirror([ * * MESSAGE_REACTION_REMOVE * * MESSAGE_REACTION_REMOVE_ALL * * USER_UPDATE - * * USER_NOTE_UPDATE * * USER_SETTINGS_UPDATE * * PRESENCE_UPDATE * * VOICE_STATE_UPDATE diff --git a/typings/index.d.ts b/typings/index.d.ts index f77ab51e..3c3e118f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2,6 +2,7 @@ declare module 'discord.js' { import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; + import * as WebSocket from 'ws'; export const version: string; @@ -181,15 +182,18 @@ declare module 'discord.js' { public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; - public on(event: 'reconnecting', listener: (shardID: number) => void): this; - public on(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; - public on(event: 'shardReady', listener: (shardID: number) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; + public on(event: 'invalidated', listener: () => void): this; + public on(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this; + public on(event: 'shardError', listener: (error: Error, id: number) => void): this; + public on(event: 'shardReconnecting', listener: (id: number) => void): this; + public on(event: 'shardReady', listener: (id: number) => void): this; + public on(event: 'shardResumed', listener: (id: number) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -215,15 +219,18 @@ declare module 'discord.js' { public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; - public once(event: 'reconnecting', listener: (shardID: number) => void): this; - public once(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; - public once(event: 'shardReady', listener: (shardID: number) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; + public once(event: 'invalidated', listener: () => void): this; + public once(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this; + public once(event: 'shardError', listener: (error: Error, id: number) => void): this; + public once(event: 'shardReconnecting', listener: (id: number) => void): this; + public once(event: 'shardReady', listener: (id: number) => void): this; + public once(event: 'shardResumed', listener: (id: number) => void): this; public once(event: string, listener: Function): this; } @@ -340,12 +347,13 @@ declare module 'discord.js' { } export class DiscordAPIError extends Error { - constructor(path: string, error: object, method: string); + constructor(path: string, error: object, method: string, httpStatus: number); private static flattenErrors(obj: object, key: string): string[]; public code: number; public method: string; public path: string; + public httpStatus: number; } export class DMChannel extends TextBasedChannel(Channel) { @@ -1270,27 +1278,80 @@ declare module 'discord.js' { export class WebSocketManager { constructor(client: Client); + private totalShards: number | string; + private shardQueue: Set; + private packetQueue: object[]; + private destroyed: boolean; + private reconnecting: boolean; + private sessionStartLimit?: { total: number; remaining: number; reset_after: number; }; + public readonly client: Client; - public gateway: string | undefined; - public readonly ping: number; + public gateway?: string; public shards: Collection; public status: Status; + public readonly ping: number; - public broadcast(packet: object): void; + private debug(message: string, shard?: WebSocketShard): void; + private connect(): Promise; + private createShards(): Promise; + private reconnect(): Promise; + private broadcast(packet: object): void; + private destroy(): void; + private _handleSessionLimit(remaining?: number, resetAfter?: number): Promise; + private handlePacket(packet?: object, shard?: WebSocketShard): Promise; + private checkReady(): boolean; + private triggerReady(): void; } export class WebSocketShard extends EventEmitter { constructor(manager: WebSocketManager, id: number); - public id: number; - public readonly ping: number; - public pings: number[]; - public status: Status; + private sequence: number; + private closeSequence: number; + private sessionID?: string; + private lastPingTimestamp: number; + private lastHeartbeatAcked: boolean; + private trace: string[]; + private ratelimit: { queue: object[]; total: number; remaining: number; time: 60e3; timer: NodeJS.Timeout | null; }; + private connection: WebSocket | null; + private helloTimeout: NodeJS.Timeout | null; + private eventsAttached: boolean; + public manager: WebSocketManager; + public id: number; + public status: Status; + public pings: [number, number, number]; + public readonly ping: number; - public send(packet: object): void; + private debug(message: string): void; + private connect(): Promise; + private onOpen(): void; + private onMessage(event: MessageEvent): void; + private onError(error: ErrorEvent): void; + private onClose(event: CloseEvent): void; + private onPacket(packet: object): void; + private setHelloTimeout(time?: number): void; + private setHeartbeatTimer(time: number): void; + private sendHeartbeat(): void; + private ackHeartbeat(): void; + private identify(): void; + private identifyNew(): void; + private identifyResume(): void; + private _send(data: object): void; + private processQueue(): void; + private destroy(closeCode: number): void; + public send(data: object): void; public on(event: 'ready', listener: () => void): this; + public on(event: 'resumed', listener: () => void): this; + public on(event: 'close', listener: (event: CloseEvent) => void): this; + public on(event: 'invalidSession', listener: () => void): this; + public on(event: string, listener: Function): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'resumed', listener: () => void): this; + public once(event: 'close', listener: (event: CloseEvent) => void): this; + public once(event: 'invalidSession', listener: () => void): this; + public once(event: string, listener: Function): this; } //#endregion @@ -1589,7 +1650,7 @@ declare module 'discord.js' { interface ClientOptions { shards?: number | number[]; - shardCount?: number; + shardCount?: number | 'auto'; totalShardCount?: number; messageCacheMaxSize?: number; messageCacheLifetime?: number; @@ -2149,5 +2210,9 @@ declare module 'discord.js' { | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE'; + type MessageEvent = { data: WebSocket.Data; type: string; target: WebSocket; }; + type CloseEvent = { wasClean: boolean; code: number; reason: string; target: WebSocket; }; + type ErrorEvent = { error: any, message: string, type: string, target: WebSocket; }; + //#endregion } From 32a432f4a55094ea7c0f6260529f8f911ea8a2de Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 1 Apr 2019 13:46:27 +0200 Subject: [PATCH 087/428] cleanup(ShardClientUtil): rename id to ids --- src/sharding/ShardClientUtil.js | 6 +++--- typings/index.d.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 026c97ef..acbe2250 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -46,11 +46,11 @@ class ShardClientUtil { } /** - * Shard ID or array of shard IDs of this client - * @type {number|number[]} + * Array of shard IDs of this client + * @type {number[]} * @readonly */ - get id() { + get ids() { return this.client.options.shards; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 3c3e118f..d0383f69 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -937,7 +937,7 @@ declare module 'discord.js' { public client: Client; public readonly count: number; - public readonly id: number | number[]; + public readonly ids: number[]; public mode: ShardingManagerMode; public parentPort: any; public broadcastEval(script: string): Promise; From 089f65fd2acb284fa741f4faf302ec34660026b6 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 3 Apr 2019 22:45:51 +0200 Subject: [PATCH 088/428] fix(RequestHandler): pass HTTPError pass path instead of route as path --- src/rest/RequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 45065a66..4e10b5fe 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -103,7 +103,7 @@ class RequestHandler { // NodeFetch error expected for all "operational" errors, such as 500 status code this.busy = false; return reject( - new HTTPError(error.message, error.constructor.name, error.status, request.method, request.route) + new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path) ); } From bb92289e45b4d9d576a4277d0bfa082814d2bc20 Mon Sep 17 00:00:00 2001 From: bdistin Date: Wed, 3 Apr 2019 16:02:19 -0500 Subject: [PATCH 089/428] fix: remove GuildChannel fallback, and remove GuildChannel as extendable (#3165) * remake pr * typings --- src/stores/ChannelStore.js | 2 +- src/structures/Channel.js | 6 +----- src/util/Structures.js | 1 - typings/index.d.ts | 1 - 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 88c5a0bb..aad7927a 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -60,7 +60,7 @@ class ChannelStore extends DataStore { const channel = Channel.create(this.client, data, guild); if (!channel) { - this.client.emit(Events.DEBUG, `Failed to find guild for channel ${data.id} ${data.type}`); + this.client.emit(Events.DEBUG, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`); return null; } diff --git a/src/structures/Channel.js b/src/structures/Channel.js index b4341194..de5f462e 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -116,12 +116,8 @@ class Channel extends Base { channel = new CategoryChannel(guild, data); break; } - default: { - const GuildChannel = Structures.get('GuildChannel'); - channel = new GuildChannel(guild, data); - } } - guild.channels.set(channel.id, channel); + if (channel) guild.channels.set(channel.id, channel); } } return channel; diff --git a/src/util/Structures.js b/src/util/Structures.js index 62646b40..5cdfc831 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -75,7 +75,6 @@ const structures = { 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'), diff --git a/typings/index.d.ts b/typings/index.d.ts index d0383f69..f1547c87 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1735,7 +1735,6 @@ declare module 'discord.js' { TextChannel: typeof TextChannel; VoiceChannel: typeof VoiceChannel; CategoryChannel: typeof CategoryChannel; - GuildChannel: typeof GuildChannel; GuildMember: typeof GuildMember; Guild: typeof Guild; Message: typeof Message; From c078682722541713c9ac30f6777d6dafe560fcb5 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Fri, 5 Apr 2019 10:09:58 +0100 Subject: [PATCH 090/428] feat(Webhook): add url getter (#3178) * add Webhook#url * set typing as readonly * suggested change * another one --- src/structures/Webhook.js | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 472de07e..1ad7cba9 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -213,6 +213,15 @@ class Webhook { return this.client.api.webhooks(this.id, this.token).delete({ reason }); } + /** + * The url of this webhook + * @type {string} + * @readonly + */ + get url() { + return this.client.options.http.api + this.client.api.webhooks(this.id, this.token); + } + static applyToClass(structure) { for (const prop of [ 'send', diff --git a/typings/index.d.ts b/typings/index.d.ts index f1547c87..53857b33 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1270,6 +1270,7 @@ declare module 'discord.js' { public guildID: Snowflake; public name: string; public owner: User | object; + public readonly url: string; } export class WebhookClient extends WebhookMixin(BaseClient) { From 5e9bd786d13f2da11544e45e4ac9622269259789 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 5 Apr 2019 04:32:19 -0500 Subject: [PATCH 091/428] refactor(APIRequest): utilize URLSearchParams (#3180) * utilize URLSearchParams * options.query can be undefined/null * oops * remembered what I intended --- src/client/Client.js | 12 ++++++++---- src/rest/APIRequest.js | 10 +++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 73767f69..b9a20659 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -338,11 +338,15 @@ class Client extends BaseClient { * .then(link => console.log(`Generated bot invite link: ${link}`)) * .catch(console.error); */ - generateInvite(permissions) { + async generateInvite(permissions) { permissions = Permissions.resolve(permissions); - return this.fetchApplication().then(application => - `https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot` - ); + const application = await this.fetchApplication(); + const query = new URLSearchParams({ + client_id: application.id, + permissions: permissions, + scope: 'bot', + }); + return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`; } toJSON() { diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 7b0a73e4..2001fb05 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -1,6 +1,5 @@ 'use strict'; -const querystring = require('querystring'); const FormData = require('form-data'); const https = require('https'); const { browser, UserAgent } = require('../util/Constants'); @@ -16,8 +15,13 @@ class APIRequest { this.route = options.route; this.options = options; - const queryString = (querystring.stringify(options.query).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); - this.path = `${path}${queryString ? `?${queryString}` : ''}`; + let queryString = ''; + if (options.query) { + // Filter out undefined query options + const query = Object.entries(options.query).filter(([, value]) => typeof value !== 'undefined'); + queryString = new URLSearchParams(query).toString(); + } + this.path = `${path}${queryString && `?${queryString}`}`; } make() { From bfab203934395ebd8884b3fb6360d2d44a0778dd Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 Apr 2019 14:46:25 +0200 Subject: [PATCH 092/428] fix(ShardingManager): do not spawn the last shard early An off-by-one error resulted in the last shard getting the delay of the second last one. Closes #3181 --- src/sharding/ShardingManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index aca3ebed..80cf71d3 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -194,7 +194,7 @@ class ShardingManager extends EventEmitter { const promises = []; const shard = this.createShard(shardID); promises.push(shard.spawn(waitForReady)); - if (delay > 0 && this.shards.size !== this.shardList.length - 1) promises.push(Util.delayFor(delay)); + if (delay > 0 && this.shards.size !== this.shardList.length) promises.push(Util.delayFor(delay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } From 00eb7e325a4bbc41faa7411a26c3c82cf60d83c1 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 Apr 2019 16:32:50 +0200 Subject: [PATCH 093/428] fix(ApiRequest): filter out null query values Fixes #3183 --- src/rest/APIRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 2001fb05..672cccd4 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -18,7 +18,7 @@ class APIRequest { let queryString = ''; if (options.query) { // Filter out undefined query options - const query = Object.entries(options.query).filter(([, value]) => typeof value !== 'undefined'); + const query = Object.entries(options.query).filter(([, value]) => value !== null && typeof value !== 'undefined'); queryString = new URLSearchParams(query).toString(); } this.path = `${path}${queryString && `?${queryString}`}`; From 982f48ce6adff982884fc43ea9eb5d041d219bf0 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 7 Apr 2019 10:57:11 +0300 Subject: [PATCH 094/428] src: Fix TypeError --- src/client/voice/VoiceBroadcast.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 5446b46c..406e14ef 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -10,7 +10,7 @@ const PlayInterface = require('./util/PlayInterface'); * * Example usage: * ```js - * const broadcast = client.createVoiceBroadcast(); + * const broadcast = client.voice.createBroadcast(); * broadcast.play('./music.mp3'); * // Play "music.mp3" in all voice connections that the client is in * for (const connection of client.voiceConnections.values()) { @@ -79,7 +79,7 @@ class VoiceBroadcast extends EventEmitter { * @event VoiceBroadcast#subscribe * @param {StreamDispatcher} dispatcher The subscribed dispatcher */ - this.broadcast.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); + this.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); return true; } else { return false; @@ -96,7 +96,7 @@ class VoiceBroadcast extends EventEmitter { * @event VoiceBroadcast#unsubscribe * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher */ - this.broadcast.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); + this.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); return true; } return false; From 152d2e88bd94371e20df4702d08c139f6861ea02 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 8 Apr 2019 13:06:23 +0100 Subject: [PATCH 095/428] refactor(WebSocket): utilize URLSearchParams (#3185) * replace querystring with URLSearchParams * looks a bit nicer using urlSearchParams.set(...) --- src/WebSocket.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WebSocket.js b/src/WebSocket.js index 2a303cc4..c8f73972 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -1,7 +1,6 @@ 'use strict'; const { browser } = require('./util/Constants'); -const querystring = require('querystring'); try { var erlpack = require('erlpack'); if (!erlpack.pack) erlpack = null; @@ -30,8 +29,9 @@ exports.unpack = data => { exports.create = (gateway, query = {}, ...args) => { const [g, q] = gateway.split('?'); query.encoding = exports.encoding; - if (q) query = Object.assign(querystring.parse(q), query); - const ws = new exports.WebSocket(`${g}?${querystring.stringify(query)}`, ...args); + query = new URLSearchParams(query); + if (q) new URLSearchParams(q).forEach((v, k) => query.set(k, v)); + const ws = new exports.WebSocket(`${g}?${query}`, ...args); if (browser) ws.binaryType = 'arraybuffer'; return ws; }; From 70d4b4455b4b7687c7e00aae8f6db5f453f2c7d2 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 8 Apr 2019 15:20:53 +0300 Subject: [PATCH 096/428] refactor(ClientVoiceManager): make public, remove Client#voiceConnections (#3186) * docs: make voice public * typings: Update typings to match the docs * typings: ClientVoiceManager is nullable in Client Co-Authored-By: vladfrangu * typings: Mark client as readonly Co-Authored-By: vladfrangu * src: Make the client readonly * src: Remove Client#voiceConnections getter in favor of ClientVoiceManager#connections --- src/client/Client.js | 11 ----------- src/client/voice/ClientVoiceManager.js | 4 +++- typings/index.d.ts | 20 ++++++++++++++------ 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index b9a20659..bdbc442c 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -86,7 +86,6 @@ class Client extends BaseClient { /** * The voice manager of the client (`null` in browsers) * @type {?ClientVoiceManager} - * @private */ this.voice = !browser ? new ClientVoiceManager(this) : null; @@ -157,16 +156,6 @@ class Client extends BaseClient { } } - /** - * All active voice connections that have been established, mapped by guild ID - * @type {Collection} - * @readonly - */ - get voiceConnections() { - if (browser) return new Collection(); - return this.voice.connections; - } - /** * All custom emojis that the client has access to, mapped by their IDs * @type {GuildEmojiStore} diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index fa351364..6986c34a 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -14,8 +14,10 @@ class ClientVoiceManager { /** * The client that instantiated this voice manager * @type {Client} + * @readonly + * @name ClientVoiceManager#client */ - this.client = client; + Object.defineProperty(this, 'client', { value: client }); /** * A collection mapping connection IDs to the Connection objects diff --git a/typings/index.d.ts b/typings/index.d.ts index 53857b33..4af8827c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -131,11 +131,9 @@ declare module 'discord.js' { export class Client extends BaseClient { constructor(options?: ClientOptions); private actions: object; - private voice: object; private _eval(script: string): any; private _validateOptions(options?: ClientOptions): void; - public broadcasts: VoiceBroadcast[]; public channels: ChannelStore; public readonly emojis: GuildEmojiStore; public guilds: GuildStore; @@ -146,9 +144,8 @@ declare module 'discord.js' { public readonly uptime: number; public user: ClientUser | null; public users: UserStore; - public readonly voiceConnections: Collection; + public voice: ClientVoiceManager | null; public ws: WebSocketManager; - public createVoiceBroadcast(): VoiceBroadcast; public destroy(): void; public fetchApplication(): Promise; public fetchInvite(invite: InviteResolvable): Promise; @@ -234,6 +231,15 @@ declare module 'discord.js' { public once(event: string, listener: Function): this; } + export class ClientVoiceManager { + constructor(client: Client); + public readonly client: Client; + public connections: Collection; + public broadcasts: VoiceBroadcast[]; + + public createBroadcast(): VoiceBroadcast; + } + export class ClientApplication extends Base { constructor(client: Client, data: object); public botPublic?: boolean; @@ -1114,6 +1120,7 @@ declare module 'discord.js' { class VoiceBroadcast extends EventEmitter { constructor(client: Client); public client: Client; + public dispatchers: StreamDispatcher[]; public readonly dispatcher: BroadcastDispatcher; public play(input: string | Readable, options?: StreamOptions): BroadcastDispatcher; @@ -1148,10 +1155,11 @@ declare module 'discord.js' { } class VoiceConnection extends EventEmitter { - constructor(voiceManager: object, channel: VoiceChannel); + constructor(voiceManager: ClientVoiceManager, channel: VoiceChannel); private authentication: object; private sockets: object; private ssrcMap: Map; + private _speaking: Map>; private _disconnect(): void; private authenticate(): void; private authenticateFailed(reason: string): void; @@ -1175,7 +1183,7 @@ declare module 'discord.js' { public receiver: VoiceReceiver; public speaking: Readonly; public status: VoiceStatus; - public voiceManager: object; + public voiceManager: ClientVoiceManager; public disconnect(): void; public play(input: VoiceBroadcast | Readable | string, options?: StreamOptions): StreamDispatcher; From 89e27e507144dd3c32d644e0f298207eb538ff65 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 8 Apr 2019 19:29:19 +0300 Subject: [PATCH 097/428] src: Make broadcasts work again (#3190) --- src/client/voice/VoiceBroadcast.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 406e14ef..e8751c05 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -74,6 +74,7 @@ class VoiceBroadcast extends EventEmitter { add(dispatcher) { const index = this.dispatchers.indexOf(dispatcher); if (index === -1) { + this.dispatchers.push(dispatcher); /** * Emitted whenever a stream dispatcher subscribes to the broadcast. * @event VoiceBroadcast#subscribe From 6be5051f9296fe9a10549f2f9a11c18776b1b617 Mon Sep 17 00:00:00 2001 From: Reseq64 <47262222+Reseq64@users.noreply.github.com> Date: Wed, 10 Apr 2019 17:07:50 +0200 Subject: [PATCH 098/428] typo(RequestHandler): fix spelling of 'requests' (#3196) Removed the additional "s" to "requessts" --- src/rest/RequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 4e10b5fe..8f650b42 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -163,7 +163,7 @@ class RequestHandler { return this.run(); } } else { - // Handle possible malformed requessts + // Handle possible malformed requests try { const data = await parseResponse(res); if (res.status >= 400 && res.status < 500) { From 266ac1c659d9b40a8a7a944e26daf3cee2381b1c Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Wed, 10 Apr 2019 14:40:22 -0500 Subject: [PATCH 099/428] docs(Role): fix setPosition's reason type (#3198) --- src/structures/Role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index 87cca568..45b4ed31 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -298,7 +298,7 @@ class Role extends Base { * @param {number} position The position of the role * @param {Object} [options] Options for setting position * @param {boolean} [options.relative=false] Change the position relative to its current value - * @param {boolean} [options.reason] Reason for changing the position + * @param {string} [options.reason] Reason for changing the position * @returns {Promise} * @example * // Set the position of the role From 62cba2e14850645254cb345d36ac5fb19c76a22a Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Thu, 11 Apr 2019 09:10:31 +0100 Subject: [PATCH 100/428] docs(GuildChannel): fix setPosition's reason type (#3199) --- src/structures/GuildChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 9904e4c1..694fdf02 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -391,7 +391,7 @@ class GuildChannel extends Channel { * @param {number} position The new position for the guild channel * @param {Object} [options] Options for setting position * @param {boolean} [options.relative=false] Change the position relative to its current value - * @param {boolean} [options.reason] Reason for changing the position + * @param {string} [options.reason] Reason for changing the position * @returns {Promise} * @example * // Set a new channel position From 8da141637cc43fdfa592225c121063159b716bac Mon Sep 17 00:00:00 2001 From: thomasxd24 Date: Thu, 11 Apr 2019 22:34:13 +0200 Subject: [PATCH 101/428] fix end method in VoiceBroadcast (#3194) * fix end method in VoiceBroadcast this.client is a ClientVoiceManager and thus its this.client.broadcasts instead of this.client.voice.broascasts * revert the voicebroadcast and pass this.client at clientvoice * passed this.client --- src/client/voice/ClientVoiceManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 6986c34a..2ca2b465 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -37,7 +37,7 @@ class ClientVoiceManager { * @returns {VoiceBroadcast} */ createBroadcast() { - const broadcast = new VoiceBroadcast(this); + const broadcast = new VoiceBroadcast(this.client); this.broadcasts.push(broadcast); return broadcast; } From 97c196ca6ab1593101e20371914ca41dc7d2dae3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 13 Apr 2019 19:32:43 +0200 Subject: [PATCH 102/428] docs(GuildEmoji): add @ name to requiresColons and managed properties --- src/structures/GuildEmoji.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 24fd7c02..ff705e59 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -29,12 +29,14 @@ class GuildEmoji extends Emoji { /** * Whether or not this emoji requires colons surrounding it * @type {boolean} + * @name GuildEmoji#requiresColons */ if (typeof data.require_colons !== 'undefined') this.requiresColons = data.require_colons; /** * Whether this emoji is managed by an external service * @type {boolean} + * @name GuildEmoji#managed */ if (typeof data.managed !== 'undefined') this.managed = data.managed; From 5d10585af8f41cc5fbf49172a9ce143a4ad34f62 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sun, 14 Apr 2019 13:43:01 +0100 Subject: [PATCH 103/428] docs(Presence): add missing descriptions to clientStatus (#3127) * add description on jsdocs for User.clientStatus * Update src/structures/Presence.js Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * Update src/structures/Presence.js Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * Update src/structures/Presence.js Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * update typings --- src/structures/Presence.js | 6 +++--- typings/index.d.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 5d82ac07..a2d76bc0 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -77,9 +77,9 @@ class Presence { /** * The devices this presence is on * @type {?object} - * @property {PresenceStatus} web - * @property {PresenceStatus} mobile - * @property {PresenceStatus} desktop + * @property {?PresenceStatus} web The current presence in the web application + * @property {?PresenceStatus} mobile The current presence in the mobile application + * @property {?PresenceStatus} desktop The current presence in the desktop application */ this.clientStatus = data.client_status || null; diff --git a/typings/index.d.ts b/typings/index.d.ts index 4af8827c..172d0dc8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -813,7 +813,7 @@ declare module 'discord.js' { public activity: Activity; public flags: Readonly; public status: PresenceStatus; - public clientStatus: ClientPresenceStatusData; + public clientStatus: ClientPresenceStatusData | null; public readonly user: User; public readonly member?: GuildMember; public equals(presence: Presence): boolean; @@ -2073,9 +2073,9 @@ declare module 'discord.js' { type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; interface ClientPresenceStatusData { - web?: ClientPresenceStatus; - mobile?: ClientPresenceStatus; - desktop?: ClientPresenceStatus; + web?: PresenceStatus; + mobile?: PresenceStatus; + desktop?: PresenceStatus; } type PartialTypes = 'USER' From ca43919642a80e1b332af88fac5a2d34f67a8b3d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 14 Apr 2019 14:50:55 +0200 Subject: [PATCH 104/428] docs: document constructors of extendible structures (#3160) * docs: document constructors of extendible structures * docs(ClientPresence): document default value for data parameter Co-Authored-By: SpaceEEC * docs(Presence): document default value for data parameter Co-Authored-By: SpaceEEC * docs(DMChannel): capitalize DM in the constructor doc --- src/structures/ClientPresence.js | 4 ++++ src/structures/DMChannel.js | 4 ++++ src/structures/Guild.js | 4 ++++ src/structures/GuildChannel.js | 4 ++++ src/structures/GuildEmoji.js | 5 +++++ src/structures/GuildMember.js | 5 +++++ src/structures/Message.js | 5 +++++ src/structures/MessageReaction.js | 5 +++++ src/structures/Presence.js | 4 ++++ src/structures/Role.js | 5 +++++ src/structures/TextChannel.js | 4 ++++ src/structures/User.js | 4 ++++ src/structures/VoiceState.js | 4 ++++ typings/index.d.ts | 2 +- 14 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index a92dfb30..90db53bd 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -6,6 +6,10 @@ const { ActivityTypes, OPCodes } = require('../util/Constants'); const { TypeError } = require('../errors'); class ClientPresence extends Presence { + /** + * @param {Client} client The instantiating client + * @param {Object} [data={}] The data for the client presence + */ constructor(client, data = {}) { super(client, Object.assign(data, { status: 'online', user: { id: null } })); } diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index e58942c3..e172f226 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -10,6 +10,10 @@ const MessageStore = require('../stores/MessageStore'); * @implements {TextBasedChannel} */ class DMChannel extends Channel { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the DM channel + */ constructor(client, data) { super(client, data); // Override the channel type so partials have a known type diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9c44b1b8..9aeb3097 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -26,6 +26,10 @@ const { Error, TypeError } = require('../errors'); * @extends {Base} */ class Guild extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild + */ constructor(client, data) { super(client); diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 694fdf02..4ba8eb99 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -14,6 +14,10 @@ const { Error, TypeError } = require('../errors'); * @extends {Channel} */ class GuildChannel extends Channel { + /** + * @param {Guild} guild The guild the guild channel is part of + * @param {Object} data The data for the guild channel + */ constructor(guild, data) { super(guild.client, data); diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index ff705e59..30c67263 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -10,6 +10,11 @@ const Emoji = require('./Emoji'); * @extends {Emoji} */ class GuildEmoji extends Emoji { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild emoji + * @param {Guild} guild The guild the guild emoji is part of + */ constructor(client, data, guild) { super(client, data); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index d479dd77..e0abea44 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -15,6 +15,11 @@ const { Error } = require('../errors'); * @extends {Base} */ class GuildMember extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild member + * @param {Guild} guild The guild the member is part of + */ constructor(client, data, guild) { super(client); diff --git a/src/structures/Message.js b/src/structures/Message.js index afdb5e2c..b57f4299 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -19,6 +19,11 @@ const APIMessage = require('./APIMessage'); * @extends {Base} */ class Message extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the message + * @param {TextChannel|DMChannel} channel The channel the message was sent in + */ constructor(client, data, channel) { super(client); diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 69138fda..fe10e428 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -9,6 +9,11 @@ const ReactionUserStore = require('../stores/ReactionUserStore'); * Represents a reaction to a message. */ class MessageReaction { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the message reaction + * @param {Message} message The message the reaction refers to + */ constructor(client, data, message) { /** * The message that this reaction refers to diff --git a/src/structures/Presence.js b/src/structures/Presence.js index a2d76bc0..16b4f002 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -25,6 +25,10 @@ const { ActivityTypes } = require('../util/Constants'); * Represents a user's presence. */ class Presence { + /** + * @param {Client} client The instantiating client + * @param {Object} [data={}] The data for the presence + */ constructor(client, data = {}) { Object.defineProperty(this, 'client', { value: client }); /** diff --git a/src/structures/Role.js b/src/structures/Role.js index 45b4ed31..ce9f3637 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -11,6 +11,11 @@ const { Error, TypeError } = require('../errors'); * @extends {Base} */ class Role extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the role + * @param {Guild} guild The guild the role is part of + */ constructor(client, data, guild) { super(client); diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index a643815e..e50bee05 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -13,6 +13,10 @@ const MessageStore = require('../stores/MessageStore'); * @implements {TextBasedChannel} */ class TextChannel extends GuildChannel { + /** + * @param {Guild} guild The guild the text channel is part of + * @param {Object} data The data for the text channel + */ constructor(guild, data) { super(guild, data); /** diff --git a/src/structures/User.js b/src/structures/User.js index a0436f7e..c2276c4f 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -12,6 +12,10 @@ const { Error } = require('../errors'); * @extends {Base} */ class User extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the user + */ constructor(client, data) { super(client); diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 9ff71e87..d74fc6aa 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -6,6 +6,10 @@ const Base = require('./Base'); * Represents the voice state for a Guild Member. */ class VoiceState extends Base { + /** + * @param {Guild} guild The guild the voice state is part of + * @param {Object} data The data for the voice state + */ constructor(guild, data) { super(guild.client); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 172d0dc8..3ad8789d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -809,7 +809,7 @@ declare module 'discord.js' { } export class Presence { - constructor(client: Client, data?: object); + constructor(client: Client, data: object); public activity: Activity; public flags: Readonly; public status: PresenceStatus; From 520810d48411013307ca310f0eff988a210266ca Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sun, 14 Apr 2019 13:58:33 +0100 Subject: [PATCH 105/428] feat(Util): add YELLOW to ColorResolvable (#3182) --- src/util/Constants.js | 1 + src/util/Util.js | 1 + typings/index.d.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index dfd370f7..f314d3d6 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -422,6 +422,7 @@ exports.Colors = { AQUA: 0x1ABC9C, GREEN: 0x2ECC71, BLUE: 0x3498DB, + YELLOW: 0xFFFF00, PURPLE: 0x9B59B6, LUMINOUS_VIVID_PINK: 0xE91E63, GOLD: 0xF1C40F, diff --git a/src/util/Util.js b/src/util/Util.js index bbf838e4..700c519b 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -251,6 +251,7 @@ class Util { * - `AQUA` * - `GREEN` * - `BLUE` + * - `YELLOW` * - `PURPLE` * - `LUMINOUS_VIVID_PINK` * - `GOLD` diff --git a/typings/index.d.ts b/typings/index.d.ts index 3ad8789d..673104f7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1685,9 +1685,11 @@ declare module 'discord.js' { } type ColorResolvable = 'DEFAULT' + | 'WHITE' | 'AQUA' | 'GREEN' | 'BLUE' + | 'YELLOW' | 'PURPLE' | 'LUMINOUS_VIVID_PINK' | 'GOLD' From cbb9b14950dd8866557406a014b7b08e4c2acff4 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sun, 14 Apr 2019 08:27:59 -0500 Subject: [PATCH 106/428] test: add Webhook(Client) testing file (#2855) * test: add WebhookClient testing file * webhooks cant edit messages, silly * add more webhook types as requested * fix typo * eslint matters * fix(webhooktests): add 'use strict', remove embed: tests --- test/webhooktest.js | 149 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 test/webhooktest.js diff --git a/test/webhooktest.js b/test/webhooktest.js new file mode 100644 index 00000000..2fe11b9d --- /dev/null +++ b/test/webhooktest.js @@ -0,0 +1,149 @@ +'use strict'; + +const Discord = require('../src'); + +const { owner, token, webhookChannel, webhookToken } = require('./auth.js'); + +const client = new Discord.Client(); + +const fetch = require('node-fetch'); +const fs = require('fs'); +const path = require('path'); +const util = require('util'); + +const fill = c => Array(4).fill(c.repeat(1000)); +const buffer = l => fetch(l).then(res => res.buffer()); +const read = util.promisify(fs.readFile); +const readStream = fs.createReadStream; +const wait = util.promisify(setTimeout); + +const linkA = 'https://lolisafe.moe/iiDMtAXA.png'; +const linkB = 'https://lolisafe.moe/9hSpedPh.png'; +const fileA = path.join(__dirname, 'blobReach.png'); + +const embed = () => new Discord.MessageEmbed(); +const attach = (attachment, name) => new Discord.MessageAttachment(attachment, name); + +const tests = [ + (m, hook) => hook.send('x'), + (m, hook) => hook.send(['x', 'y']), + + (m, hook) => hook.send('x', { code: true }), + (m, hook) => hook.send('1', { code: 'js' }), + (m, hook) => hook.send('x', { code: '' }), + + (m, hook) => hook.send(fill('x'), { split: true }), + (m, hook) => hook.send(fill('1'), { code: 'js', split: true }), + (m, hook) => hook.send(fill('x'), { reply: m.author, code: 'js', split: true }), + (m, hook) => hook.send(fill('xyz '), { split: { char: ' ' } }), + + (m, hook) => hook.send({ embeds: [{ description: 'a' }] }), + (m, hook) => hook.send({ files: [{ attachment: linkA }] }), + (m, hook) => hook.send({ + embeds: [{ description: 'a' }], + files: [{ attachment: linkA, name: 'xyz.png' }], + }), + + (m, hook) => hook.send('x', embed().setDescription('a')), + (m, hook) => hook.send(embed().setDescription('a')), + (m, hook) => hook.send({ embeds: [embed().setDescription('a')] }), + (m, hook) => hook.send([embed().setDescription('a'), embed().setDescription('b')]), + + (m, hook) => hook.send('x', attach(linkA)), + (m, hook) => hook.send(attach(linkA)), + (m, hook) => hook.send({ files: [linkA] }), + (m, hook) => hook.send({ files: [attach(linkA)] }), + async (m, hook) => hook.send(attach(await buffer(linkA))), + async (m, hook) => hook.send({ files: [await buffer(linkA)] }), + async (m, hook) => hook.send({ files: [{ attachment: await buffer(linkA) }] }), + (m, hook) => hook.send([attach(linkA), attach(linkB)]), + + (m, hook) => hook.send(embed().setDescription('a')), + + (m, hook) => hook.send({ embeds: [{ description: 'a' }] }), + (m, hook) => hook.send(embed().setDescription('a')), + + (m, hook) => hook.send(['x', 'y'], [embed().setDescription('a'), attach(linkB)]), + (m, hook) => hook.send(['x', 'y'], [attach(linkA), attach(linkB)]), + + (m, hook) => hook.send([embed().setDescription('a'), attach(linkB)]), + (m, hook) => hook.send({ + embeds: [embed().setImage('attachment://two.png')], + files: [attach(linkB, 'two.png')], + }), + (m, hook) => hook.send({ + embeds: [ + embed() + .setImage('attachment://two.png') + .attachFiles([attach(linkB, 'two.png')]), + ], + }), + async (m, hook) => hook.send(['x', 'y', 'z'], { + code: 'js', + embeds: [ + embed() + .setImage('attachment://two.png') + .attachFiles([attach(linkB, 'two.png')]), + ], + files: [{ attachment: await buffer(linkA) }], + }), + + (m, hook) => hook.send('x', attach(fileA)), + (m, hook) => hook.send({ files: [fileA] }), + (m, hook) => hook.send(attach(fileA)), + async (m, hook) => hook.send({ files: [await read(fileA)] }), + async (m, hook) => hook.send(fill('x'), { + reply: m.author, + code: 'js', + split: true, + embeds: [embed().setImage('attachment://zero.png')], + files: [attach(await buffer(linkA), 'zero.png')], + }), + + (m, hook) => hook.send('x', attach(readStream(fileA))), + (m, hook) => hook.send({ files: [readStream(fileA)] }), + (m, hook) => hook.send({ files: [{ attachment: readStream(fileA) }] }), + async (m, hook) => hook.send(fill('xyz '), { + reply: m.author, + code: 'js', + split: { char: ' ', prepend: 'hello! ', append: '!!!' }, + embeds: [embed().setImage('attachment://zero.png')], + files: [linkB, attach(await buffer(linkA), 'zero.png'), readStream(fileA)], + }), + + (m, hook) => hook.send('Done!'), +]; + + +client.on('message', async message => { + if (message.author.id !== owner) return; + const match = message.content.match(/^do (.+)$/); + const hooks = [ + { type: 'WebhookClient', hook: new Discord.WebhookClient(webhookChannel, webhookToken) }, + { type: 'TextChannel#fetchWebhooks', hook: await message.channel.fetchWebhooks().then(x => x.first()) }, + { type: 'Guild#fetchWebhooks', hook: await message.guild.fetchWebhooks().then(x => x.first()) }, + ]; + if (match && match[1] === 'it') { + /* eslint-disable no-await-in-loop */ + for (const { type, hook } of hooks) { + for (const [i, test] of tests.entries()) { + await message.channel.send(`**#${i}-Hook: ${type}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message, hook).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + await wait(1000); + } + } + /* eslint-enable no-await-in-loop */ + } else if (match) { + const n = parseInt(match[1]) || 0; + const test = tests.slice(n)[0]; + const i = tests.indexOf(test); + /* eslint-disable no-await-in-loop */ + for (const { type, hook } of hooks) { + await message.channel.send(`**#${i}-Hook: ${type}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message, hook).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + } + /* eslint-enable no-await-in-loop */ + } +}); + +client.login(token); From 2af101efbb0fe5c014727dc130a6307066395fe7 Mon Sep 17 00:00:00 2001 From: danielnewell <46284599+danielnewell@users.noreply.github.com> Date: Sun, 14 Apr 2019 09:47:52 -0400 Subject: [PATCH 107/428] docs(faq): add a link to the guide, restructure a bit (#3082) * Update faq.md added link to guide * docs(faq): link documentation as per suggestion --- docs/general/faq.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/general/faq.md b/docs/general/faq.md index 950f4d9c..212a16d8 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -1,7 +1,6 @@ # Frequently Asked Questions -These are just questions that get asked frequently, that usually have a common resolution. -If you have issues not listed here, please ask in the [official Discord server](https://discord.gg/bRCvFy9). -Always make sure to read the documentation. +These questions are some of the most frequently asked. + ## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽ Update to Node.js 8.0.0 or newer. @@ -21,3 +20,7 @@ Update to Node.js 8.0.0 or newer. - **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats! - **Windows:** Run `npm install --global --production windows-build-tools` in an admin command prompt or PowerShell. Then, running `npm install node-opus` in your bot's directory should successfully build it. Woo! + +Other questions can be found at the [official Discord.js guide](https://discordjs.guide/popular-topics/common-questions.html) +If you have issues not listed here or on the guide, feel free to ask in the [official Discord.js server](https://discord.gg/bRCvFy9). +Always make sure to read the [documentation](https://discord.js.org/#/docs/main/stable/general/welcome). From d9a053df67b780b85e09657345bfa7c06c9cf41b Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 15 Apr 2019 13:41:37 +0100 Subject: [PATCH 108/428] docs(Presence): add ClientPresenceStatus typedef (#3208) --- src/structures/Presence.js | 17 ++++++++++++----- typings/index.d.ts | 6 +++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 16b4f002..55937cea 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -13,7 +13,6 @@ const { ActivityTypes } = require('../util/Constants'); /** * The status of this presence: - * * * **`online`** - user is online * * **`idle`** - user is AFK * * **`offline`** - user is offline or invisible @@ -21,6 +20,14 @@ const { ActivityTypes } = require('../util/Constants'); * @typedef {string} PresenceStatus */ +/** + * The status of this presence: + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} ClientPresenceStatus + */ + /** * Represents a user's presence. */ @@ -80,10 +87,10 @@ class Presence { /** * The devices this presence is on - * @type {?object} - * @property {?PresenceStatus} web The current presence in the web application - * @property {?PresenceStatus} mobile The current presence in the mobile application - * @property {?PresenceStatus} desktop The current presence in the desktop application + * @type {?Object} + * @property {?ClientPresenceStatus} web The current presence in the web application + * @property {?ClientPresenceStatus} mobile The current presence in the mobile application + * @property {?ClientPresenceStatus} desktop The current presence in the desktop application */ this.clientStatus = data.client_status || null; diff --git a/typings/index.d.ts b/typings/index.d.ts index 673104f7..fd680f05 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2075,9 +2075,9 @@ declare module 'discord.js' { type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; interface ClientPresenceStatusData { - web?: PresenceStatus; - mobile?: PresenceStatus; - desktop?: PresenceStatus; + web?: ClientPresenceStatus; + mobile?: ClientPresenceStatus; + desktop?: ClientPresenceStatus; } type PartialTypes = 'USER' From eb537b6f4896d84cc660e95be8685223e8f9c785 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 15 Apr 2019 21:17:27 +0300 Subject: [PATCH 109/428] docs(WebSocketShard): mark non-nullable parameters as non-nullable (#3209) * docs: Imagine having an optional nullable param * docs: Another one --- src/client/websocket/WebSocketShard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 19cb11e4..c927c156 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -526,7 +526,7 @@ class WebSocketShard extends EventEmitter { * a full [Payload](https://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands). * Do not use this method if you don't know what you're doing. * @param {Object} data The full packet to send - * @param {?boolean} [important=false] If this packet should be added first in queue + * @param {boolean} [important=false] If this packet should be added first in queue */ send(data, important = false) { this.ratelimit.queue[important ? 'unshift' : 'push'](data); @@ -574,7 +574,7 @@ class WebSocketShard extends EventEmitter { /** * Destroys this shard and closes its WebSocket connection. - * @param {?number} [closeCode=1000] The close code to use + * @param {number} [closeCode=1000] The close code to use * @private */ destroy(closeCode = 1000) { From 52bc5b0170e290291d9760c13a1969874f936e6d Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Mon, 15 Apr 2019 19:46:59 +0100 Subject: [PATCH 110/428] feat(MessageEmbed): resolve color in embed constructor (#2912) * Resolve color in embed constructor * Use ColorResolvable type for color parameter * docs(MessageEmbed): color property is still a number --- src/structures/MessageEmbed.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 8276cad7..706b20e9 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -45,7 +45,7 @@ class MessageEmbed { * The color of this embed * @type {?number} */ - this.color = data.color; + this.color = Util.resolveColor(data.color); /** * The timestamp of this embed diff --git a/typings/index.d.ts b/typings/index.d.ts index fd680f05..fe15fa89 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1965,7 +1965,7 @@ declare module 'discord.js' { description?: string; url?: string; timestamp?: Date | number; - color?: number | string; + color?: ColorResolvable; fields?: { name: string; value: string; inline?: boolean; }[]; files?: (MessageAttachment | string | FileOptions)[]; author?: { name?: string; url?: string; icon_url?: string; iconURL?: string; }; From 0b1176d9a1078d056835dc4e573fe639d4c5ed79 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 15 Apr 2019 20:01:39 +0100 Subject: [PATCH 111/428] chore(typings): declaring explicit nullable returns (#3134) * strict nullable & WebSocketManager private typings * missing semicolon * kyra suggestion #1 Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * kyra suggestion #2&3 * space's requested changes * space's requested change * strict nullable & WebSocketManager private typings missing semicolon kyra suggestion #1 Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> kyra suggestion #2&3 space's requested changes space's requested change * resolve conflicts * deflate --- typings/index.d.ts | 207 +++++++++++++++++++++++---------------------- 1 file changed, 105 insertions(+), 102 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index fe15fa89..0a2a1248 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -10,21 +10,21 @@ declare module 'discord.js' { export class Activity { constructor(presence: Presence, data?: object); - public applicationID: Snowflake; - public assets: RichPresenceAssets; - public details: string; + public applicationID: Snowflake | null; + public assets: RichPresenceAssets | null; + public details: string | null; public name: string; public party: { - id: string; + id: string | null; size: [number, number]; - }; - public state: string; + } | null; + public state: string | null; public timestamps: { - start: Date; - end: Date; - }; + start: Date | null; + end: Date | null; + } | null; public type: ActivityType; - public url: string; + public url: string | null; public equals(activity: Activity): boolean; } @@ -35,10 +35,10 @@ declare module 'discord.js' { export class APIMessage { constructor(target: MessageTarget, options: MessageOptions | WebhookMessageOptions); - public data?: object; + public data: object | null; public readonly isUser: boolean; public readonly isWebhook: boolean; - public files?: object[]; + public files: object[] | null; public options: MessageOptions | WebhookMessageOptions; public target: MessageTarget; @@ -138,10 +138,10 @@ declare module 'discord.js' { public readonly emojis: GuildEmojiStore; public guilds: GuildStore; public readyAt: Date | null; - public readonly readyTimestamp: number; - public shard: ShardClientUtil; - public token: string; - public readonly uptime: number; + public readonly readyTimestamp: number | null; + public shard: ShardClientUtil | null; + public token: string | null; + public readonly uptime: number | null; public user: ClientUser | null; public users: UserStore; public voice: ClientVoiceManager | null; @@ -169,16 +169,16 @@ declare module 'discord.js' { public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildMemberUpdate' | 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; - public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; + public on(event: 'resume', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; @@ -206,16 +206,16 @@ declare module 'discord.js' { public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildMemberUpdate' | 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; - public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; + public once(event: 'resume', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; @@ -237,21 +237,23 @@ declare module 'discord.js' { public connections: Collection; public broadcasts: VoiceBroadcast[]; + private joinChannel(channel: VoiceChannel): Promise; + public createBroadcast(): VoiceBroadcast; } export class ClientApplication extends Base { constructor(client: Client, data: object); - public botPublic?: boolean; - public botRequireCodeGrant?: boolean; - public cover?: string; + public botPublic: boolean | null; + public botRequireCodeGrant: boolean | null; + public cover: string | null; public readonly createdAt: Date; public readonly createdTimestamp: number; public description: string; public icon: string; public id: Snowflake; public name: string; - public owner?: User; + public owner: User | null; public rpcOrigins: string[]; public coverImage(options?: AvatarOptions): string; public fetchAssets(): Promise; @@ -290,8 +292,8 @@ declare module 'discord.js' { public equals(collection: Collection): boolean; public every(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; public filter(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): Collection; - public find(fn: (value: V, key: K, collection: Collection) => boolean): V; - public findKey(fn: (value: V, key: K, collection: Collection) => boolean): K; + public find(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): V; + public findKey(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): K; public first(): V | undefined; public first(count: number): V[]; public firstKey(): K | undefined; @@ -317,7 +319,7 @@ declare module 'discord.js' { export abstract class Collector extends EventEmitter { constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); - private _timeout: NodeJS.Timer; + private _timeout: NodeJS.Timer | null; public readonly client: Client; public collected: Collection; @@ -391,7 +393,7 @@ declare module 'discord.js' { protected setup(data: any): void; - public readonly afkChannel: VoiceChannel; + public readonly afkChannel: VoiceChannel | null; public afkChannelID: Snowflake; public afkTimeout: number; public applicationID: Snowflake; @@ -399,6 +401,7 @@ declare module 'discord.js' { public channels: GuildChannelStore; public readonly createdAt: Date; public readonly createdTimestamp: number; + public readonly defaultRole: Role | null; public defaultMessageNotifications: DefaultMessageNotifications | number; public readonly defaultRole: Role; public deleted: boolean; @@ -411,13 +414,13 @@ declare module 'discord.js' { public readonly joinedAt: Date; public joinedTimestamp: number; public large: boolean; - public readonly me: GuildMember; + public readonly me: GuildMember | null; public memberCount: number; public members: GuildMemberStore; public mfaLevel: number; public name: string; public readonly nameAcronym: string; - public readonly owner: GuildMember; + public readonly owner: GuildMember | null; public ownerID: Snowflake; public presences: PresenceStore; public region: string; @@ -425,11 +428,11 @@ declare module 'discord.js' { public readonly shard: WebSocketShard; public shardID: number; public splash: string; - public readonly systemChannel: TextChannel; + public readonly systemChannel: TextChannel | null; public systemChannelID: Snowflake; public verificationLevel: number; public readonly verified: boolean; - public readonly voiceConnection: VoiceConnection; + public readonly voiceConnection: VoiceConnection | null; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; @@ -483,13 +486,13 @@ declare module 'discord.js' { constructor(logs: GuildAuditLogs, guild: Guild, data: object); public action: GuildAuditLogsAction; public actionType: GuildAuditLogsActionType; - public changes: AuditLogChange[]; + public changes: AuditLogChange[] | null; public readonly createdAt: Date; public readonly createdTimestamp: number; public executor: User; - public extra: object | Role | GuildMember; + public extra: object | Role | GuildMember | null; public id: Snowflake; - public reason: string; + public reason: string | null; public target: Guild | User | Role | GuildEmoji | Invite | Webhook; public targetType: GuildAuditLogsTarget; public toJSON(): object; @@ -505,10 +508,10 @@ declare module 'discord.js' { public guild: Guild; public readonly manageable: boolean; public name: string; - public readonly parent: CategoryChannel; + public readonly parent: CategoryChannel | null; public parentID: Snowflake; public permissionOverwrites: Collection; - public readonly permissionsLocked: boolean; + public readonly permissionsLocked: boolean | null; public readonly position: number; public rawPosition: number; public readonly viewable: boolean; @@ -553,8 +556,8 @@ declare module 'discord.js' { public readonly displayName: string; public guild: Guild; public readonly id: Snowflake; - public readonly joinedAt: Date; - public joinedTimestamp: number; + public readonly joinedAt: Date | null; + public joinedTimestamp: number | null; public readonly kickable: boolean; public readonly manageable: boolean; public nickname: string; @@ -612,18 +615,18 @@ declare module 'discord.js' { public channel: GuildChannel; public code: string; public readonly createdAt: Date; - public createdTimestamp: number; - public readonly expiresAt: Date; - public readonly expiresTimestamp: number; - public guild: Guild; - public inviter: User; - public maxAge: number; - public maxUses: number; + public createdTimestamp: number | null; + public readonly expiresAt: Date | null; + public readonly expiresTimestamp: number | null; + public guild: Guild | null; + public inviter: User | null; + public maxAge: number | null; + public maxUses: number | null; public memberCount: number; public presenceCount: number; - public temporary: boolean; + public temporary: boolean | null; public readonly url: string; - public uses: number; + public uses: number | null; public delete(reason?: string): Promise; public toJSON(): object; public toString(): string; @@ -634,10 +637,10 @@ declare module 'discord.js' { private _edits: Message[]; private patch(data: object): void; - public activity: GroupActivity; - public application: ClientApplication; + public activity: GroupActivity | null; + public application: ClientApplication | null; public attachments: Collection; - public author: User; + public author: User | null; public channel: TextChannel | DMChannel; public readonly cleanContent: string; public content: string; @@ -646,13 +649,13 @@ declare module 'discord.js' { public readonly deletable: boolean; public deleted: boolean; public readonly editable: boolean; - public readonly editedAt: Date; - public editedTimestamp: number; + public readonly editedAt: Date | null; + public editedTimestamp: number | null; public readonly edits: Message[]; public embeds: MessageEmbed[]; - public readonly guild: Guild; + public readonly guild: Guild | null; public id: Snowflake; - public readonly member: GuildMember; + public readonly member: GuildMember | null; public mentions: MessageMentions; public nonce: string; public readonly partial: boolean; @@ -663,7 +666,7 @@ declare module 'discord.js' { public tts: boolean; public type: MessageType; public readonly url: string; - public webhookID: Snowflake; + public webhookID: Snowflake | null; public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; public delete(options?: { timeout?: number, reason?: string }): Promise; @@ -685,13 +688,13 @@ declare module 'discord.js' { constructor(attachment: BufferResolvable | Stream, name?: string, data?: object); public attachment: BufferResolvable | Stream; - public height: number; + public height: number | null; public id: Snowflake; public name?: string; public proxyURL: string; public size: number; public url: string; - public width: number; + public width: number | null; public setFile(attachment: BufferResolvable | Stream, name?: string): this; public setName(name: string): this; public toJSON(): object; @@ -712,23 +715,23 @@ declare module 'discord.js' { constructor(data?: MessageEmbed | MessageEmbedOptions); private _apiTransform(): MessageEmbedOptions; - public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string }; + public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string } | null; public color: number; - public readonly createdAt: Date; + public readonly createdAt: Date | null; public description: string; public fields: EmbedField[]; public files: (MessageAttachment | string | FileOptions)[]; - public footer: { text?: string; iconURL?: string; proxyIconURL?: string }; - public readonly hexColor: string; - public image: { url: string; proxyURL?: string; height?: number; width?: number; }; + public footer: { text?: string; iconURL?: string; proxyIconURL?: string } | null; + public readonly hexColor: string | null; + public image: { url: string; proxyURL?: string; height?: number; width?: number; } | null; public readonly length: number; public provider: { name: string; url: string; }; - public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; }; - public timestamp: number; + public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; } | null; + public timestamp: number | null; public title: string; public type: string; public url: string; - public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number }; + public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number } | null; public addBlankField(inline?: boolean): this; public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; @@ -749,9 +752,9 @@ declare module 'discord.js' { export class MessageMentions { constructor(message: Message, users: object[] | Collection, roles: Snowflake[] | Collection, everyone: boolean); - private _channels: Collection; + private _channels: Collection | null; private readonly _content: Message; - private _members: Collection; + private _members: Collection | null; public readonly channels: Collection; public readonly client: Client; @@ -762,7 +765,7 @@ declare module 'discord.js' { ignoreRoles?: boolean; ignoreEveryone?: boolean; }): boolean; - public readonly members: Collection; + public readonly members: Collection | null; public roles: Collection; public users: Collection; public toJSON(): object; @@ -809,13 +812,13 @@ declare module 'discord.js' { } export class Presence { - constructor(client: Client, data: object); - public activity: Activity; + constructor(client: Client, data?: object); + public activity: Activity | null; public flags: Readonly; public status: PresenceStatus; public clientStatus: ClientPresenceStatusData | null; - public readonly user: User; - public readonly member?: GuildMember; + public readonly user: User | null; + public readonly member: GuildMember | null; public equals(presence: Presence): boolean; } @@ -831,7 +834,7 @@ declare module 'discord.js' { public collect(reaction: MessageReaction): Snowflake | string; public dispose(reaction: MessageReaction, user: User): Snowflake | string; public empty(): void; - public endReason(): string; + public endReason(): string | null; public on(event: 'collect', listener: (reaction: MessageReaction, user: User) => void): this; public on(event: 'dispose', listener: (reaction: MessageReaction, user: User) => void): this; @@ -854,12 +857,12 @@ declare module 'discord.js' { export class RichPresenceAssets { constructor(activity: Activity, assets: object); - public largeImage: Snowflake; - public largeText: string; - public smallImage: Snowflake; - public smallText: string; - public largeImageURL(options: AvatarOptions): string; - public smallImageURL(options: AvatarOptions): string; + public largeImage: Snowflake | null; + public largeText: string | null; + public smallImage: Snowflake | null; + public smallText: string | null; + public largeImageURL(options: AvatarOptions): string | null; + public smallImageURL(options: AvatarOptions): string | null; } export class Role extends Base { @@ -910,9 +913,9 @@ declare module 'discord.js' { public env: object; public id: number; public manager: ShardingManager; - public process: ChildProcess; + public process: ChildProcess | null; public ready: boolean; - public worker: any; + public worker: any | null; public eval(script: string): Promise; public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; @@ -945,7 +948,7 @@ declare module 'discord.js' { public readonly count: number; public readonly ids: number[]; public mode: ShardingManagerMode; - public parentPort: any; + public parentPort: any | null; public broadcastEval(script: string): Promise; public broadcastEval(fn: (client: Client) => T): Promise; public fetchClientValues(prop: string): Promise; @@ -969,7 +972,7 @@ declare module 'discord.js' { public respawn: boolean; public shardArgs: string[]; public shards: Collection; - public token: string; + public token: string | null; public totalShards: number | 'auto'; public broadcast(message: any): Promise; public broadcastEval(script: string): Promise; @@ -994,9 +997,9 @@ declare module 'discord.js' { constructor(player: object, options?: StreamOptions, streams?: object); public player: object; public pausedSince: number; - public broadcast: VoiceBroadcast; + public broadcast: VoiceBroadcast | null; public readonly paused: boolean; - public readonly pausedTime: boolean; + public readonly pausedTime: boolean | null; public readonly streamTime: number; public readonly totalStreamTime: number; public readonly bitrateEditable: boolean; @@ -1061,7 +1064,7 @@ declare module 'discord.js' { export class User extends PartialTextBasedChannel(Base) { constructor(client: Client, data: object); - public avatar: string; + public avatar: string | null; public bot: boolean; public readonly createdAt: Date; public readonly createdTimestamp: number; @@ -1074,7 +1077,7 @@ declare module 'discord.js' { public readonly presence: Presence; public readonly tag: string; public username: string; - public avatarURL(options?: AvatarOptions): string; + public avatarURL(options?: AvatarOptions): string | null; public createDM(): Promise; public deleteDM(): Promise; public displayAvatarURL(options?: AvatarOptions): string; @@ -1102,7 +1105,7 @@ declare module 'discord.js' { public static makePlainError(err: Error): { name: string, message: string, stack: string }; public static mergeDefault(def: object, given: object): object; public static moveElementInArray(array: any[], element: any, newIndex: number, offset?: boolean): number; - public static parseEmoji(text: string): { animated: boolean; name: string; id: string; }; + public static parseEmoji(text: string): { animated: boolean; name: string; id: string | null; } | null; public static resolveColor(color: ColorResolvable): number; public static resolveString(data: StringResolvable): string; public static setPosition( @@ -1238,19 +1241,19 @@ declare module 'discord.js' { export class VoiceState extends Base { constructor(guild: Guild, data: object); - public readonly channel?: VoiceChannel; + public readonly channel: VoiceChannel | null; public channelID?: Snowflake; public readonly deaf?: boolean; public guild: Guild; public id: Snowflake; - public readonly member: GuildMember; + public readonly member: GuildMember | null; public readonly mute?: boolean; public selfDeaf?: boolean; public selfMute?: boolean; public serverDeaf?: boolean; public serverMute?: boolean; public sessionID?: string; - public readonly speaking?: boolean; + public readonly speaking: boolean | null; public setDeaf(mute: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; @@ -1277,7 +1280,7 @@ declare module 'discord.js' { public channelID: Snowflake; public guildID: Snowflake; public name: string; - public owner: User | object; + public owner: User | object | null; public readonly url: string; } @@ -1394,7 +1397,7 @@ declare module 'discord.js' { export class GuildEmojiStore extends DataStore { constructor(guild: Guild, iterable?: Iterable); public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; - public resolveIdentifier(emoji: EmojiIdentifierResolvable): string; + public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; } export class GuildChannelStore extends DataStore { @@ -1410,8 +1413,8 @@ declare module 'discord.js' { export class GuildMemberRoleStore extends OverridableDataStore { constructor(member: GuildMember); - public readonly hoist: Role; - public readonly color: Role; + public readonly hoist: Role | null; + public readonly color: Role | null; public readonly highest: Role; public add(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; @@ -1431,7 +1434,7 @@ declare module 'discord.js' { export class GuildStore extends DataStore { constructor(client: Client, iterable?: Iterable); - public create(name: string, options?: { region?: string, icon?: BufferResolvable | Base64Resolvable }): Promise; + public create(name: string, options?: { region?: string, icon: BufferResolvable | Base64Resolvable | null }): Promise; } export class MessageStore extends DataStore { @@ -1483,10 +1486,10 @@ declare module 'discord.js' { const TextBasedChannel: (Base?: Constructable) => Constructable; interface PartialTextBasedChannelFields { - lastMessageID: Snowflake; - lastMessageChannelID: Snowflake; - readonly lastMessage: Message; - lastPinTimestamp: number; + lastMessageID: Snowflake | null; + lastMessageChannelID: Snowflake | null; + readonly lastMessage: Message | null; + lastPinTimestamp: number | null; readonly lastPinAt: Date; send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; @@ -1874,7 +1877,7 @@ declare module 'discord.js' { interface GuildEmbedData { enabled: boolean; - channel?: GuildChannelResolvable; + channel: GuildChannelResolvable | null; } type GuildFeatures = 'INVITE_SPLASH' From 39fd8fd6454aaa61e098ef8fac1a33aeede97f9a Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 16 Apr 2019 21:14:01 +0100 Subject: [PATCH 112/428] fix(typings): remove duplicated Guild#defaultRole (#3211) --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0a2a1248..4c32f0b1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -403,7 +403,6 @@ declare module 'discord.js' { public readonly createdTimestamp: number; public readonly defaultRole: Role | null; public defaultMessageNotifications: DefaultMessageNotifications | number; - public readonly defaultRole: Role; public deleted: boolean; public embedEnabled: boolean; public emojis: GuildEmojiStore; From b5320299f79c5ccfba8dac95699d2d7202682b56 Mon Sep 17 00:00:00 2001 From: "Deivu (Saya)" <36309350+Deivu@users.noreply.github.com> Date: Wed, 17 Apr 2019 21:32:57 +0800 Subject: [PATCH 113/428] Only reset sessionID when close code is 1000 or 4006 (#3213) * Event code 1001 should not get its sessionID reset * Reset sessionID when close code is 1000 or 4006 --- src/client/websocket/WebSocketManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index c483e803..eb4a8282 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -216,7 +216,7 @@ class WebSocketManager { return; } - if (event.code >= 1000 && event.code <= 2000) { + if (event.code === 1000 || event.code === 4006) { // Any event code in this range cannot be resumed. shard.sessionID = undefined; } From bccbb550b08364df38d7ae8e03d162d8565a76cb Mon Sep 17 00:00:00 2001 From: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Date: Fri, 19 Apr 2019 02:47:39 -0400 Subject: [PATCH 114/428] docs(Collector): specify the unit for `CollectionOptions#time` (#3219) --- src/structures/interfaces/Collector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 1dbd6142..8e6d8463 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -15,7 +15,7 @@ const EventEmitter = require('events'); /** * Options to be applied to the collector. * @typedef {Object} CollectorOptions - * @property {number} [time] How long to run the collector for + * @property {number} [time] How long to run the collector for in milliseconds * @property {boolean} [dispose=false] Whether to dispose data when it's deleted */ From cde955c7660d753fc1b51c5f94b4f51c6b0d8533 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 19 Apr 2019 08:49:17 +0200 Subject: [PATCH 115/428] fix(PresenceUpdateAction): emit presences again (#3214) * fix(PresenceUpdateAction): emit presences again * update typings --- src/client/actions/PresenceUpdate.js | 40 +++++++++++++--------------- typings/index.d.ts | 6 +++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 649bf6ce..538789e2 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -5,40 +5,38 @@ const { Events } = require('../../util/Constants'); class PresenceUpdateAction extends Action { handle(data) { - let cached = this.client.users.get(data.user.id); - if (!cached && data.user.username) cached = this.client.users.add(data.user); - if (!cached) return; + let user = this.client.users.get(data.user.id); + if (!user && data.user.username) user = this.client.users.add(data.user); + if (!user) return; if (data.user && data.user.username) { - if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); + if (!user.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); } const guild = this.client.guilds.get(data.guild_id); if (!guild) return; - let member = guild.members.get(cached.id); + let oldPresence = guild.presences.get(user.id); + if (oldPresence) oldPresence = oldPresence._clone(); + let member = guild.members.get(user.id); if (!member && data.status !== 'offline') { - member = guild.members.add({ user: cached, roles: data.roles, deaf: false, mute: false }); + member = guild.members.add({ + user, + roles: data.roles, + deaf: false, + mute: false, + }); this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member); } - - if (member) { - if (this.client.listenerCount(Events.PRESENCE_UPDATE) === 0) { - guild.presences.add(data); - return; - } - const old = member._clone(); - if (member.presence) old.frozenPresence = member.presence._clone(); - guild.presences.add(data); + guild.presences.add(Object.assign(data, { guild })); + if (member && this.client.listenerCount(Events.PRESENCE_UPDATE)) { /** - * Emitted whenever a guild member's presence changes, or they change one of their details. + * Emitted whenever a guild member's presence (e.g. status, activity) is changed. * @event Client#presenceUpdate - * @param {GuildMember} oldMember The member before the presence update - * @param {GuildMember} newMember The member after the presence update + * @param {?Presence} oldPresence The presence before the update, if one at all + * @param {Presence} newPresence The presence after the update */ - this.client.emit(Events.PRESENCE_UPDATE, old, member); - } else { - guild.presences.add(data); + this.client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence); } } } diff --git a/typings/index.d.ts b/typings/index.d.ts index 4c32f0b1..d1894df8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -169,13 +169,14 @@ declare module 'discord.js' { public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public on(event: 'guildMemberUpdate' | 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; public on(event: 'resume', listener: (replayed: number, shardID: number) => void): this; @@ -206,13 +207,14 @@ declare module 'discord.js' { public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public once(event: 'guildMemberUpdate' | 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; public once(event: 'resume', listener: (replayed: number, shardID: number) => void): this; From 1514df0f87cb7fc6cc210b00db977414c39665df Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 21 Apr 2019 10:32:16 +0300 Subject: [PATCH 116/428] fix: emit resume event, silent disconnects, error event param (#3192) * src: Fix shardResumed event not being emitted * docs: Document Client#error again * src: Fix onError due to incorrect typings * src: handle onError properly for both uws and ws * src: Try to fix silent disconnects when using uWs * fix(WebSocketShard): uws emits plain objects, not errors Emitting line of code: https://github.com/discordjs/uws/blob/39aa429f94d9668608f69848b3a84db3a3e92914/src/uws.js#L80-L83 Listener attaching is here: https://github.com/discordjs/uws/blob/master/src/uws.js#L128 For reference, found a clue here: https://github.com/discordjs/discord.js/issues/1528 --- src/client/websocket/WebSocketManager.js | 9 --------- src/client/websocket/WebSocketShard.js | 18 +++++++++++++----- src/client/websocket/handlers/RESUMED.js | 10 +++++----- src/sharding/ShardClientUtil.js | 5 +++++ typings/index.d.ts | 2 +- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index eb4a8282..71a24757 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -194,15 +194,6 @@ class WebSocketManager { if (!this.shardQueue.size) this.reconnecting = false; }); - shard.on(ShardEvents.RESUMED, () => { - /** - * Emitted when a shard resumes successfully. - * @event Client#shardResumed - * @param {number} id The shard ID that resumed - */ - this.client.emit(Events.SHARD_RESUMED, shard.id); - }); - shard.on(ShardEvents.CLOSE, event => { if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { /** diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index c927c156..a317bc04 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -239,7 +239,7 @@ class WebSocketShard extends EventEmitter { /** * Called whenever a message is received. - * @param {Event} event Event received + * @param {MessageEvent} event Event received * @private */ onMessage({ data }) { @@ -266,11 +266,14 @@ class WebSocketShard extends EventEmitter { /** * Called whenever an error occurs with the WebSocket. - * @param {ErrorEvent} error The error that occurred + * @param {ErrorEvent|Object} event The error that occurred * @private */ - onError({ error }) { - if (error && error.message === 'uWs client connection error') { + onError(event) { + const error = event && event.error ? event.error : error; + if (!error) return; + + if (error.message === 'uWs client connection error') { this.debug('Received a uWs error. Closing the connection and reconnecting...'); this.connection.close(4000); return; @@ -295,6 +298,11 @@ class WebSocketShard extends EventEmitter { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent} */ + /** + * @external MessageEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent} + */ + /** * Called whenever a connection to the gateway is closed. * @param {CloseEvent} event Close event that was received @@ -581,7 +589,7 @@ class WebSocketShard extends EventEmitter { this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); // Close the WebSocket connection, if any - if (this.connection) { + if (this.connection && this.connection.readyState !== WebSocket.CLOSED) { this.connection.close(closeCode); } else { /** diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js index bd00ec88..e345cb74 100644 --- a/src/client/websocket/handlers/RESUMED.js +++ b/src/client/websocket/handlers/RESUMED.js @@ -5,10 +5,10 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, packet, shard) => { const replayed = shard.sequence - shard.closeSequence; /** - * Emitted when the client gateway resumes. - * @event Client#resume - * @param {number} replayed The number of events that were replayed - * @param {number} shardID The ID of the shard that resumed + * Emitted when a shard resumes successfully. + * @event Client#shardResumed + * @param {number} id The shard ID that resumed + * @param {number} replayedEvents The amount of replayed events */ - client.emit(Events.RESUMED, replayed, shard.id); + client.emit(Events.SHARD_RESUMED, shard.id, replayed); }; diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index acbe2250..03c80ead 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -181,6 +181,11 @@ class ShardClientUtil { _respond(type, message) { this.send(message).catch(err => { err.message = `Error when sending ${type} response to master process: ${err.message}`; + /** + * Emitted when the client encounters an error. + * @event Client#error + * @param {Error} error The error encountered + */ this.client.emit(Events.ERROR, err); }); } diff --git a/typings/index.d.ts b/typings/index.d.ts index d1894df8..f0b064f4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1339,7 +1339,7 @@ declare module 'discord.js' { private connect(): Promise; private onOpen(): void; private onMessage(event: MessageEvent): void; - private onError(error: ErrorEvent): void; + private onError(error: ErrorEvent | object): void; private onClose(event: CloseEvent): void; private onPacket(packet: object): void; private setHelloTimeout(time?: number): void; From 52b4f09e58f796dd895539784b5f8117fd3df168 Mon Sep 17 00:00:00 2001 From: Dragoteryx <33252782+Dragoteryx@users.noreply.github.com> Date: Sun, 21 Apr 2019 09:34:12 +0200 Subject: [PATCH 117/428] fix(Structures): allow multiple extensions by checking prototype chain (#3217) --- src/util/Structures.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index 5cdfc831..2dc26770 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -55,8 +55,8 @@ class Structures { throw new TypeError(`The extender function must return the extended structure class/prototype ${received}.`); } - const prototype = Object.getPrototypeOf(extended); - if (prototype !== structures[structure]) { + if (!(extended.prototype instanceof structures[structure])) { + const prototype = Object.getPrototypeOf(extended); const received = `${extended.name || 'unnamed'}${prototype.name ? ` extends ${prototype.name}` : ''}`; throw new Error( 'The class/prototype returned from the extender function must extend the existing structure class/prototype' + From abd9d368161cbf5c4dd5df79aeeca67ba4d25900 Mon Sep 17 00:00:00 2001 From: Purpzie <25022704+Purpzie@users.noreply.github.com> Date: Sun, 21 Apr 2019 02:38:09 -0500 Subject: [PATCH 118/428] feat(Util): resolve text parameter of splitMessage to a string (#3212) --- src/util/Util.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index 700c519b..82856949 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -51,11 +51,12 @@ class Util { /** * Splits a string into multiple chunks at a designated character that do not exceed a specific length. - * @param {string} text Content to split + * @param {StringResolvable} text Content to split * @param {SplitOptions} [options] Options controlling the behavior of the split * @returns {string|string[]} */ static splitMessage(text, { maxLength = 2000, char = '\n', prepend = '', append = '' } = {}) { + text = this.resolveString(text); if (text.length <= maxLength) return text; const splitText = text.split(char); if (splitText.some(chunk => chunk.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN'); From 01c708bc759fa85eb5259cb689b95e865ede7e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Sun, 21 Apr 2019 13:34:09 +0200 Subject: [PATCH 119/428] feat(Sharding): change `waitForReady` to `spawnTimeout` (#3080) This means that you'll not only be able to choose between having a timeout or not, but also to set the amount of milliseconds as you wish. --- src/sharding/Shard.js | 20 +++++++++++--------- src/sharding/ShardClientUtil.js | 7 ++++--- src/sharding/ShardingManager.js | 16 +++++++++------- typings/index.d.ts | 10 +++++----- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index adc5caeb..4fab578d 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -102,10 +102,11 @@ class Shard extends EventEmitter { /** * Forks a child process or creates a worker thread for the shard. * You should not need to call this manually. - * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait until the {@link Client} has become ready + * before resolving. (-1 or Infinity for no wait) * @returns {Promise} */ - async spawn(waitForReady = true) { + async spawn(spawnTimeout = 30000) { if (this.process) throw new Error('SHARDING_PROCESS_EXISTS', this.id); if (this.worker) throw new Error('SHARDING_WORKER_EXISTS', this.id); @@ -128,12 +129,12 @@ class Shard extends EventEmitter { */ this.emit('spawn', this.process || this.worker); - if (!waitForReady) return this.process || this.worker; + if (spawnTimeout === -1 || spawnTimeout === Infinity) return this.process || this.worker; 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); + setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), spawnTimeout); }); return this.process || this.worker; } @@ -156,13 +157,14 @@ class Shard extends EventEmitter { /** * Kills and restarts the shard's process/worker. * @param {number} [delay=500] How long to wait between killing the process/worker and restarting it (in milliseconds) - * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait until the {@link Client} has become ready + * before resolving. (-1 or Infinity for no wait) * @returns {Promise} */ - async respawn(delay = 500, waitForReady = true) { + async respawn(delay = 500, spawnTimeout) { this.kill(); if (delay > 0) await Util.delayFor(delay); - return this.spawn(waitForReady); + return this.spawn(spawnTimeout); } /** @@ -308,8 +310,8 @@ class Shard extends EventEmitter { // Shard is requesting a respawn of all shards if (message._sRespawnAll) { - const { shardDelay, respawnDelay, waitForReady } = message._sRespawnAll; - this.manager.respawnAll(shardDelay, respawnDelay, waitForReady).catch(() => { + const { shardDelay, respawnDelay, spawnTimeout } = message._sRespawnAll; + this.manager.respawnAll(shardDelay, respawnDelay, spawnTimeout).catch(() => { // Do nothing }); return; diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 03c80ead..4d0b4a43 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -143,12 +143,13 @@ class ShardClientUtil { * @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/worker and restarting it * (in milliseconds) - * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait for a shard to become ready before + * continuing to another. (-1 or Infinity for no wait) * @returns {Promise} Resolves upon the message being sent * @see {@link ShardingManager#respawnAll} */ - respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { - return this.send({ _sRespawnAll: { shardDelay, respawnDelay, waitForReady } }); + respawnAll(shardDelay = 5000, respawnDelay = 500, spawnTimeout = 30000) { + return this.send({ _sRespawnAll: { shardDelay, respawnDelay, spawnTimeout } }); } /** diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 80cf71d3..15b74ffd 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -156,12 +156,13 @@ class ShardingManager extends EventEmitter { /** * Spawns multiple shards. - * @param {number} [amount=this.totalShards] Number of shards to spawn + * @param {number|string} [amount=this.totalShards] Number of shards to spawn * @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 + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait until the {@link Client} has become ready + * before resolving. (-1 or Infinity for no wait) * @returns {Promise>} */ - async spawn(amount = this.totalShards, delay = 5500, waitForReady = true) { + async spawn(amount = this.totalShards, delay = 5500, spawnTimeout) { // Obtain/verify the number of shards to spawn if (amount === 'auto') { amount = await Util.fetchRecommendedShards(this.token); @@ -193,7 +194,7 @@ class ShardingManager extends EventEmitter { for (const shardID of this.shardList) { const promises = []; const shard = this.createShard(shardID); - promises.push(shard.spawn(waitForReady)); + promises.push(shard.spawn(spawnTimeout)); if (delay > 0 && this.shards.size !== this.shardList.length) promises.push(Util.delayFor(delay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } @@ -245,13 +246,14 @@ class ShardingManager extends EventEmitter { * @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 + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait for a shard to become ready before + * continuing to another. (-1 or Infinity for no wait) * @returns {Promise>} */ - async respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { + async respawnAll(shardDelay = 5000, respawnDelay = 500, spawnTimeout) { let s = 0; for (const shard of this.shards.values()) { - const promises = [shard.respawn(respawnDelay, waitForReady)]; + const promises = [shard.respawn(respawnDelay, spawnTimeout)]; if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } diff --git a/typings/index.d.ts b/typings/index.d.ts index f0b064f4..535295d1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -921,9 +921,9 @@ declare module 'discord.js' { public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; public kill(): void; - public respawn(delay?: number, waitForReady?: boolean): Promise; + public respawn(delay?: number, spawnTimeout?: number): Promise; public send(message: any): Promise; - public spawn(waitForReady?: boolean): Promise; + public spawn(spawnTimeout?: number): Promise; public on(event: 'death', listener: (child: ChildProcess) => void): this; public on(event: 'disconnect' | 'ready' | 'reconnecting', listener: () => void): this; @@ -953,7 +953,7 @@ declare module 'discord.js' { public broadcastEval(script: string): Promise; public broadcastEval(fn: (client: Client) => T): Promise; public fetchClientValues(prop: string): Promise; - public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: boolean): Promise; + public respawnAll(shardDelay?: number, respawnDelay?: number, spawnTimeout?: number): Promise; public send(message: any): Promise; public static singleton(client: Client, mode: ShardingManagerMode): ShardClientUtil; @@ -979,8 +979,8 @@ declare module 'discord.js' { public broadcastEval(script: string): Promise; public createShard(id: number): Shard; public fetchClientValues(prop: string): Promise; - public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: boolean): Promise>; - public spawn(amount?: number | 'auto', delay?: number, waitForReady?: boolean): Promise>; + public respawnAll(shardDelay?: number, respawnDelay?: number, spawnTimeout?: number): Promise>; + public spawn(amount?: number | 'auto', delay?: number, spawnTimeout?: number): Promise>; public on(event: 'shardCreate', listener: (shard: Shard) => void): this; From d1778772cdb0935e3eac4479969625c2a010f11c Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Sun, 21 Apr 2019 06:47:09 -0500 Subject: [PATCH 120/428] docs: update node version requirement, npm install links, add update guide (#3220) * Update welcome.md Update node version requirement, npm install links, docs links, made a note that the guide is for stable and added a new link to the WIP update guide. * docs(welcome.md): put notice for wip update guild on its own line * docs(welcome.md): indent own line * docs(README.md): apply the same changes here --- README.md | 13 +++++++------ docs/general/welcome.md | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index faaa2bfd..69a779fc 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,12 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 8.0.0 or newer is required.** +**Node.js 10.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. -Without voice support: `npm install discord.js` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus` -With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript` +Without voice support: `npm install discordjs/discord.js` +With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discordjs/discord.js node-opus` +With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript` ### Audio engines The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. @@ -68,8 +68,9 @@ client.login('token'); ## Links * [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) -* [Documentation](https://discord.js.org/#/docs) -* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) +* [Documentation](https://discord.js.org/#/docs/main/master/general/welcome) +* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable + See also the WIP [Update Guide](https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md) also including updated and removed items in the library. * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/discordjs/discord.js) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index eb94f475..2bc0525c 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -33,12 +33,12 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 8.0.0 or newer is required.** +**Node.js 10.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. -Without voice support: `npm install discord.js` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus` -With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript` +Without voice support: `npm install discordjs/discord.js` +With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discordjs/discord.js node-opus` +With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript` ### Audio engines The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. @@ -74,8 +74,9 @@ client.login('token'); ## Links * [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) -* [Documentation](https://discord.js.org/#/docs) -* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) +* [Documentation](https://discord.js.org/#/docs/main/master/general/welcome) +* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable + See also the WIP [Update Guide](https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md) also including updated and removed items in the library. * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/discordjs/discord.js) From f7f4607b5ffa3fb3a31dac76262f0de7d27def0d Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 22 Apr 2019 08:09:06 +0100 Subject: [PATCH 121/428] docs(faq): bump to node 10.0.0 (#3224) --- docs/general/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/general/faq.md b/docs/general/faq.md index 212a16d8..c4ce363d 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -3,7 +3,7 @@ These questions are some of the most frequently asked. ## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽ -Update to Node.js 8.0.0 or newer. +Update to Node.js 10.0.0 or newer. ## How do I get voice working? - Install FFMPEG. From c4b79571ba5bd468c6c2b27b7c6f43d421730dd5 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 22 Apr 2019 08:24:32 +0100 Subject: [PATCH 122/428] feat(Invite): add deletable getter (#3203) * add Invite#deletable * fix ci * reee * since guild is nullable * accommodate for external invites * nit(Invite): use guild instead of channel.guild --- src/structures/Invite.js | 13 +++++++++++++ typings/index.d.ts | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index fec3f2d7..43d8c652 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -1,6 +1,7 @@ 'use strict'; const { Endpoints } = require('../util/Constants'); +const Permissions = require('../util/Permissions'); const Base = require('./Base'); /** @@ -91,6 +92,18 @@ class Invite extends Base { return this.createdTimestamp ? new Date(this.createdTimestamp) : null; } + /** + * Whether the invite is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + const guild = this.guild; + if (!guild || !this.client.guilds.has(guild.id)) return false; + return this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) || + guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD); + } + /** * The timestamp the invite will expire at * @type {?number} diff --git a/typings/index.d.ts b/typings/index.d.ts index 535295d1..f6488599 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -615,7 +615,8 @@ declare module 'discord.js' { constructor(client: Client, data: object); public channel: GuildChannel; public code: string; - public readonly createdAt: Date; + public readonly deletable: boolean; + public readonly createdAt: Date | null; public createdTimestamp: number | null; public readonly expiresAt: Date | null; public readonly expiresTimestamp: number | null; From 23e6414420dccc9d3a87e13d691aaa390eceaa34 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 23 Apr 2019 11:32:03 +0100 Subject: [PATCH 123/428] fix: old objects not being cached (#3225) please post issues if you find any, will probably cause some --- src/stores/DataStore.js | 2 +- test/voice.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index ade8c70f..be1e93d7 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -18,7 +18,7 @@ class DataStore extends Collection { add(data, cache = true, { id, extras = [] } = {}) { const existing = this.get(id || data.id); - if (existing && existing.partial && cache && existing._patch) existing._patch(data); + if (existing && existing._patch && cache) existing._patch(data); if (existing) return existing; const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; diff --git a/test/voice.js b/test/voice.js index 6d022a29..e8613a58 100644 --- a/test/voice.js +++ b/test/voice.js @@ -6,7 +6,7 @@ const ytdl = require('ytdl-core'); const prism = require('prism-media'); const fs = require('fs'); -const client = new Discord.Client({ fetchAllMembers: false, partials: true, apiRequestMethod: 'sequential' }); +const client = new Discord.Client({ fetchAllMembers: false, partials: [], apiRequestMethod: 'sequential' }); const auth = require('./auth.js'); From 3f6d08a499e93840054742de28db2783058c7104 Mon Sep 17 00:00:00 2001 From: didinele Date: Tue, 23 Apr 2019 19:47:42 +0300 Subject: [PATCH 124/428] fix(typings): Collection#find & findKey can return undefined (#3228) * fix(typings): [Collection#find](https://github.com/didinele/discord.js/blob/master/src/util/Collection.js#L172) can also return undefined * same thing for findKey --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index f6488599..45f2b582 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -294,8 +294,8 @@ declare module 'discord.js' { public equals(collection: Collection): boolean; public every(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; public filter(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): Collection; - public find(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): V; - public findKey(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): K; + public find(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): V | undefined; + public findKey(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): K | undefined; public first(): V | undefined; public first(count: number): V[]; public firstKey(): K | undefined; From 75cd2608082f9f587c9beca84870d5b144c118eb Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Tue, 23 Apr 2019 21:44:02 +0300 Subject: [PATCH 125/428] src: Don't use the GuildMemberRoleStore to patch the GuildMember _roles (#3226) * src: Don't use the GuildMemberRoleStore for patching GuildMember#_roles * src: Remove usage of _patch from the store * src: Finish up clone changes --- src/stores/GuildMemberRoleStore.js | 10 +++------- src/structures/GuildMember.js | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 7b44a353..52770629 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -84,7 +84,7 @@ class GuildMemberRoleStore extends Collection { await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason }); const clone = this.member._clone(); - clone.roles._patch([...this.keys(), roleOrRoles.id]); + clone._roles = [...this.keys(), roleOrRoles.id]; return clone; } } @@ -116,7 +116,7 @@ class GuildMemberRoleStore extends Collection { const clone = this.member._clone(); const newRoles = this.filter(role => role.id !== roleOrRoles.id); - clone.roles._patch([...newRoles.keys()]); + clone._roles = [...newRoles.keys()]; return clone; } } @@ -141,13 +141,9 @@ class GuildMemberRoleStore extends Collection { return this.member.edit({ roles }, reason); } - _patch(roles) { - this.member._roles = roles; - } - clone() { const clone = new this.constructor(this.member); - clone._patch(this.keyArray()); + clone.member._roles = [...this.keyArray()]; return clone; } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index e0abea44..567c27e8 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -76,7 +76,7 @@ class GuildMember extends Base { if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); if (data.user) this.user = this.guild.client.users.add(data.user); - if (data.roles) this.roles._patch(data.roles); + if (data.roles) this._roles = data.roles; } _clone() { From 6a07715c1de19a27dce8c38153ee5ddb06093967 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 23 Apr 2019 21:02:16 +0200 Subject: [PATCH 126/428] fix(Guild): only update emojis when they are present in the payload --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9aeb3097..c1d901a4 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -266,7 +266,7 @@ class Guild extends Base { */ this.emojis = new GuildEmojiStore(this); if (data.emojis) for (const emoji of data.emojis) this.emojis.add(emoji); - } else { + } else if (data.emojis) { this.client.actions.GuildEmojisUpdate.handle({ guild_id: this.id, emojis: data.emojis, From 39115c8acc89ba3997c23e841533f36c5c5d6b7f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 23 Apr 2019 21:21:41 +0200 Subject: [PATCH 127/428] fix(MessageCreateAction): remove redundant GuildMemberStore#add call This was also causing a bug where GuildMember#_roles was patched with a GuildMemberRoleStore --- src/client/actions/MessageCreate.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index d4ef6400..3772c4a1 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -12,9 +12,7 @@ class MessageCreateAction extends Action { if (existing) return { message: existing }; const message = channel.messages.add(data); const user = message.author; - let member = null; - if (message.member && channel.guild) member = channel.guild.members.add(message.member); - else if (channel.guild) member = channel.guild.member(user); + let member = message.member; channel.lastMessageID = data.id; if (user) { user.lastMessageID = data.id; From 7b531648e09d9b461cee52897926e11a5debfcea Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 23 Apr 2019 20:59:52 +0100 Subject: [PATCH 128/428] feat(GuildMemberStore) add options.count to prune (#3189) * add GuildMemberStore#prune(options.count) * typings: proper typings for null return value --- src/stores/GuildMemberStore.js | 11 ++++++++--- typings/index.d.ts | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 397c119b..9f9c4211 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -106,11 +106,13 @@ class GuildMemberStore extends DataStore { /** * Prunes members from the guild based on how long they have been inactive. + * It's recommended to set options.count to `false` for large guilds. * @param {Object} [options] Prune options * @param {number} [options.days=7] Number of days of inactivity required to kick * @param {boolean} [options.dry=false] Get number of users that will be kicked, without actually kicking them + * @param {boolean} [options.count=true] Whether or not to return the number of users that have been kicked. * @param {string} [options.reason] Reason for this prune - * @returns {Promise} The number of members that were/will be kicked + * @returns {Promise} The number of members that were/will be kicked * @example * // See how many members will be pruned * guild.members.prune({ dry: true }) @@ -122,9 +124,12 @@ class GuildMemberStore extends DataStore { * .then(pruned => console.log(`I just pruned ${pruned} people!`)) * .catch(console.error); */ - prune({ days = 7, dry = false, reason } = {}) { + prune({ days = 7, dry = false, count = true, reason } = {}) { if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE'); - return this.client.api.guilds(this.guild.id).prune[dry ? 'get' : 'post']({ query: { days }, reason }) + return this.client.api.guilds(this.guild.id).prune[dry ? 'get' : 'post']({ query: { + days, + compute_prune_count: count, + }, reason }) .then(data => data.pruned); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 45f2b582..99b0962e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1430,6 +1430,7 @@ declare module 'discord.js' { public fetch(options: UserResolvable | FetchMemberOptions): Promise; public fetch(): Promise; public fetch(options: FetchMembersOptions): Promise>; + public prune(options: GuildPruneMembersOptions & { dry?: false, count: false }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; public unban(user: UserResolvable, reason?: string): Promise; } @@ -1901,6 +1902,7 @@ declare module 'discord.js' { type GuildResolvable = Guild | Snowflake; interface GuildPruneMembersOptions { + count?: boolean; days?: number; dry?: boolean; reason?: string; From de79bba965c5d5200c1ccdc3b8c7d581b23d4a23 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 24 Apr 2019 17:53:41 +0300 Subject: [PATCH 129/428] src: Fix type error in WebSocketShard#onError (#3231) --- src/client/websocket/WebSocketShard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index a317bc04..ca68fa21 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -270,7 +270,7 @@ class WebSocketShard extends EventEmitter { * @private */ onError(event) { - const error = event && event.error ? event.error : error; + const error = event && event.error ? event.error : event; if (!error) return; if (error.message === 'uWs client connection error') { From 577636a46df5ec4ac2e4fae591604a60fb22f6d2 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sat, 27 Apr 2019 10:25:24 +0300 Subject: [PATCH 130/428] src: fix random broken reconnects (#3233) --- src/client/websocket/WebSocketShard.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index ca68fa21..814edb03 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -311,11 +311,15 @@ class WebSocketShard extends EventEmitter { onClose(event) { this.closeSequence = this.sequence; this.sequence = -1; + this.debug(`WebSocket was closed. Event Code: ${event.code} Clean: ${event.wasClean} Reason: ${event.reason || 'No reason received'}`); + this.setHeartbeatTimer(-1); + this.setHelloTimeout(-1); + this.status = Status.DISCONNECTED; /** From 4d7fc036a1c8e0b80c67242ab6f1eebf69a3fad6 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 27 Apr 2019 11:46:48 +0100 Subject: [PATCH 131/428] fix: channels being removed from guild.channels --- src/stores/ChannelStore.js | 7 +++++-- src/stores/GuildChannelStore.js | 9 ++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index aad7927a..e745eea3 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -54,8 +54,11 @@ class ChannelStore extends DataStore { add(data, guild, cache = true) { const existing = this.get(data.id); - if (existing && existing.partial && cache) existing._patch(data); - if (existing) return existing; + if (existing && existing._patch && cache) existing._patch(data); + if (existing) { + guild.channels.add(existing); + return existing; + } const channel = Channel.create(this.client, data, guild); diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 7d95cfe2..fc150f17 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -1,6 +1,5 @@ 'use strict'; -const Channel = require('../structures/Channel'); const { ChannelTypes } = require('../util/Constants'); const DataStore = require('./DataStore'); const GuildChannel = require('../structures/GuildChannel'); @@ -16,11 +15,11 @@ class GuildChannelStore extends DataStore { this.guild = guild; } - add(data) { - const existing = this.get(data.id); + add(channel) { + const existing = this.get(channel.id); if (existing) return existing; - - return Channel.create(this.client, data, this.guild); + this.set(channel.id, channel); + return channel; } /** From e0cfb7fb36132abe4367d5ced1650e13a19dbb66 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 27 Apr 2019 13:39:23 +0100 Subject: [PATCH 132/428] fix: not checking for guild when creating a channel --- src/stores/ChannelStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index e745eea3..64c482cb 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -55,7 +55,7 @@ class ChannelStore extends DataStore { add(data, guild, cache = true) { const existing = this.get(data.id); if (existing && existing._patch && cache) existing._patch(data); - if (existing) { + if (existing && guild) { guild.channels.add(existing); return existing; } From 4e0cf87d0fd717d781d6e633402fb2b1b6ac6fd3 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 27 Apr 2019 18:52:26 +0100 Subject: [PATCH 133/428] fix: typing map being reset for ClientUser (#3216) --- src/structures/ClientUser.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 20bc42b9..7b54b885 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -8,6 +8,12 @@ const DataResolver = require('../util/DataResolver'); * @extends {User} */ class ClientUser extends Structures.get('User') { + + constructor(client, data) { + super(client, data); + this._typing = new Map(); + } + _patch(data) { super._patch(data); @@ -23,8 +29,6 @@ class ClientUser extends Structures.get('User') { */ this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; - this._typing = new Map(); - if (data.token) this.client.token = data.token; } From aa253d95518b1c6fad1328372a832702a5ec66f0 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sun, 28 Apr 2019 13:14:26 +0200 Subject: [PATCH 134/428] fix(ClientUser): lint --- src/structures/ClientUser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 7b54b885..16dd13d0 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -8,7 +8,6 @@ const DataResolver = require('../util/DataResolver'); * @extends {User} */ class ClientUser extends Structures.get('User') { - constructor(client, data) { super(client, data); this._typing = new Map(); From bc317466215a222f3b51b9c4082e0dfd6bd071b1 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 29 Apr 2019 19:03:29 +0300 Subject: [PATCH 135/428] src: Client#readyAt should be updated when triggerReady is called (#3234) --- src/client/websocket/WebSocketManager.js | 3 +++ src/client/websocket/handlers/READY.js | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 71a24757..8c2f532f 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -424,8 +424,11 @@ class WebSocketManager { this.debug('Tried to mark self as ready, but already ready'); return; } + this.status = Status.READY; + this.client.readyAt = new Date(); + /** * Emitted when the client becomes ready to start working. * @event Client#ready diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 74b1e1b9..89f535f3 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -6,7 +6,6 @@ module.exports = (client, { d: data }, shard) => { if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); const clientUser = new ClientUser(client, data.user); client.user = clientUser; - client.readyAt = new Date(); client.users.set(clientUser.id, clientUser); for (const guild of data.guilds) { From 23191da13d63ba3a8ceaf7c77f751ed27a22e6f4 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 29 Apr 2019 17:05:52 +0100 Subject: [PATCH 136/428] feat(Partials.GuildMember): GuildMemberRemove & Guild#me (#3229) * use partials for GuildMemberRemove & Guild#me * oops * guild.members instead of Action.members Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> --- src/client/actions/Action.js | 9 +++++++++ src/client/actions/GuildMemberRemove.js | 2 +- src/structures/Guild.js | 4 +++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 0c798089..5746d276 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -56,6 +56,15 @@ class GenericAction { } return existing; } + + getMember(data, guild) { + const userID = data.user.id; + const existing = guild.members.get(userID); + if (!existing && this.client.options.partials.includes(PartialTypes.GUILD_MEMBER)) { + return guild.members.add({ user: { id: userID } }); + } + return existing; + } } module.exports = GenericAction; diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 6a4ae397..108a35da 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -9,7 +9,7 @@ class GuildMemberRemoveAction extends Action { const guild = client.guilds.get(data.guild_id); let member = null; if (guild) { - member = guild.members.get(data.user.id); + member = this.getMember(data, guild); guild.memberCount--; if (member) { guild.voiceStates.delete(member.id); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index c1d901a4..1b5987f1 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -393,7 +393,9 @@ class Guild extends Base { * @readonly */ get me() { - return this.members.get(this.client.user.id) || null; + return this.members.get(this.client.user.id) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? + this.members.add({ user: { id: this.client.user.id } }, true) : + null); } /** From 870528ed33b22b7a803915965da9f701cf62ccd2 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 29 Apr 2019 17:35:48 +0100 Subject: [PATCH 137/428] feat(VoiceChannel): add editable (#3173) * add VoiceChannel#editable * replace unnecessary super with this Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> --- src/structures/VoiceChannel.js | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 8b49715f..860241e9 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -71,6 +71,15 @@ class VoiceChannel extends GuildChannel { return super.deletable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false); } + /** + * Whether the channel is editable by the client user + * @type {boolean} + * @readonly + */ + get editable() { + return this.manageable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false); + } + /** * Whether the channel is joinable by the client user * @type {boolean} diff --git a/typings/index.d.ts b/typings/index.d.ts index 99b0962e..dd117d17 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1148,6 +1148,7 @@ declare module 'discord.js' { constructor(guild: Guild, data?: object); public bitrate: number; public readonly connection: VoiceConnection; + public readonly editable: boolean; public readonly full: boolean; public readonly joinable: boolean; public readonly members: Collection; From 9b0f4b298d12ce95863265c32e8a0308ae51661a Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 29 Apr 2019 11:37:57 -0500 Subject: [PATCH 138/428] feature: public raw events (#3159) * add a public alternative to the private raw event while retaining raw for use in debugging privately * only emit dispatch packets * requested changes TIL, that's neat * fix padding * requested changes * Update WebSocketManager.js --- src/client/websocket/WebSocketManager.js | 8 +++++++- src/client/websocket/WebSocketShard.js | 1 + typings/index.d.ts | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 8c2f532f..23dbadce 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,5 +1,6 @@ 'use strict'; +const EventEmitter = require('events'); const { Error: DJSError } = require('../../errors'); const Collection = require('../../util/Collection'); const Util = require('../../util/Util'); @@ -21,9 +22,14 @@ const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011]; /** * The WebSocket manager for this client. + * This class forwards raw dispatch events, + * read more about it here {@link https://discordapp.com/developers/docs/topics/gateway} + * @extends EventEmitter */ -class WebSocketManager { +class WebSocketManager extends EventEmitter { constructor(client) { + super(); + /** * The client that instantiated this WebSocketManager * @type {Client} diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 814edb03..67d494e2 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -257,6 +257,7 @@ class WebSocketShard extends EventEmitter { try { packet = WebSocket.unpack(this.inflate.result); this.manager.client.emit(Events.RAW, packet, this.id); + if (packet.op === OPCodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id); } catch (err) { this.manager.client.emit(Events.SHARD_ERROR, err, this.id); return; diff --git a/typings/index.d.ts b/typings/index.d.ts index dd117d17..4a57b1ec 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1291,7 +1291,7 @@ declare module 'discord.js' { constructor(id: string, token: string, options?: ClientOptions); } - export class WebSocketManager { + export class WebSocketManager extends EventEmitter { constructor(client: Client); private totalShards: number | string; private shardQueue: Set; @@ -1306,6 +1306,8 @@ declare module 'discord.js' { public status: Status; public readonly ping: number; + public on(event: WSEventType, listener: (data: any, shardID: number) => void): this; + public once(event: WSEventType, listener: (data: any, shardID: number) => void): this; private debug(message: string, shard?: WebSocketShard): void; private connect(): Promise; private createShards(): Promise; From d7a9b7452312a7309d4451dcde99533fec50d766 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 29 Apr 2019 19:49:41 +0300 Subject: [PATCH 139/428] src: Replace instanceof Array checks with Array.isArray and instanceof Buffer with Buffer.isBuffer (#3227) * src: Replace instanceof Array checks with Array.isArray * src: Buffer.isBuffer instead of instanceof Buffer --- src/WebSocket.js | 2 +- src/client/Client.js | 8 ++++---- src/client/websocket/WebSocketManager.js | 5 +++-- src/stores/GuildEmojiRoleStore.js | 4 ++-- src/stores/GuildMemberRoleStore.js | 4 ++-- src/structures/APIMessage.js | 8 ++++---- src/structures/Webhook.js | 2 +- src/structures/interfaces/TextBasedChannel.js | 4 ++-- src/util/BitField.js | 6 +++--- src/util/DataResolver.js | 4 ++-- src/util/Util.js | 4 ++-- 11 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/WebSocket.js b/src/WebSocket.js index c8f73972..f3f893b4 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -22,7 +22,7 @@ exports.pack = erlpack ? erlpack.pack : JSON.stringify; exports.unpack = data => { if (!erlpack || data[0] === '{') return JSON.parse(data); - if (!(data instanceof Buffer)) data = Buffer.from(new Uint8Array(data)); + if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data)); return erlpack.unpack(data); }; diff --git a/src/client/Client.js b/src/client/Client.js index bdbc442c..58d3c7db 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -49,7 +49,7 @@ class Client extends BaseClient { if (this.options.totalShardCount === DefaultOptions.totalShardCount) { if ('TOTAL_SHARD_COUNT' in data) { this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); - } else if (this.options.shards instanceof Array) { + } else if (Array.isArray(this.options.shards)) { this.options.totalShardCount = this.options.shards.length; } else { this.options.totalShardCount = this.options.shardCount; @@ -365,7 +365,7 @@ class Client extends BaseClient { if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); } - if (options.shards && !(options.shards instanceof Array)) { + if (options.shards && !Array.isArray(options.shards)) { throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); } if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS'); @@ -385,7 +385,7 @@ class Client extends BaseClient { if (typeof options.disableEveryone !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean'); } - if (!(options.partials instanceof Array)) { + if (!Array.isArray(options.partials)) { throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); } if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { @@ -394,7 +394,7 @@ class Client extends BaseClient { if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); } - if (!(options.disabledEvents instanceof Array)) { + if (!Array.isArray(options.disabledEvents)) { throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array'); } if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 23dbadce..6dd1a3f2 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -160,8 +160,9 @@ class WebSocketManager extends EventEmitter { } } - if (this.client.options.shards instanceof Array) { - const { shards } = this.client.options; + const { shards } = this.client.options; + + if (Array.isArray(shards)) { this.totalShards = shards.length; this.debug(`Spawning shards: ${shards.join(', ')}`); this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js index 2051161d..ad3793b2 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/stores/GuildEmojiRoleStore.js @@ -33,7 +33,7 @@ class GuildEmojiRoleStore extends Collection { */ add(roleOrRoles) { if (roleOrRoles instanceof Collection) return this.add(roleOrRoles.keyArray()); - if (!(roleOrRoles instanceof Array)) return this.add([roleOrRoles]); + if (!Array.isArray(roleOrRoles)) return this.add([roleOrRoles]); roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { @@ -52,7 +52,7 @@ class GuildEmojiRoleStore extends Collection { */ remove(roleOrRoles) { if (roleOrRoles instanceof Collection) return this.remove(roleOrRoles.keyArray()); - if (!(roleOrRoles instanceof Array)) return this.remove([roleOrRoles]); + if (!Array.isArray(roleOrRoles)) return this.remove([roleOrRoles]); roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolveID(r)); if (roleOrRoles.includes(null)) { diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 52770629..824bca68 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -65,7 +65,7 @@ class GuildMemberRoleStore extends Collection { * @returns {Promise} */ async add(roleOrRoles, reason) { - if (roleOrRoles instanceof Collection || roleOrRoles instanceof Array) { + if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) { roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { throw new TypeError('INVALID_TYPE', 'roles', @@ -96,7 +96,7 @@ class GuildMemberRoleStore extends Collection { * @returns {Promise} */ async remove(roleOrRoles, reason) { - if (roleOrRoles instanceof Collection || roleOrRoles instanceof Array) { + if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) { roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { throw new TypeError('INVALID_TYPE', 'roles', diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index e6d67b68..a925892d 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -198,7 +198,7 @@ class APIMessage { split() { if (!this.data) this.resolveData(); - if (!(this.data.content instanceof Array)) return [this]; + if (!Array.isArray(this.data.content)) return [this]; const apiMessages = []; @@ -287,7 +287,7 @@ class APIMessage { * @returns {MessageOptions|WebhookMessageOptions} */ static transformOptions(content, options, extra = {}, isWebhook = false) { - if (!options && typeof content === 'object' && !(content instanceof Array)) { + if (!options && typeof content === 'object' && !Array.isArray(content)) { options = content; content = undefined; } @@ -300,10 +300,10 @@ class APIMessage { return { content, files: [options], ...extra }; } - if (options instanceof Array) { + if (Array.isArray(options)) { const [embeds, files] = this.partitionMessageAdditions(options); return isWebhook ? { content, embeds, files, ...extra } : { content, embed: embeds[0], files, ...extra }; - } else if (content instanceof Array) { + } else if (Array.isArray(content)) { const [embeds, files] = this.partitionMessageAdditions(content); if (embeds.length || files.length) { return isWebhook ? { embeds, files, ...extra } : { embed: embeds[0], files, ...extra }; diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 1ad7cba9..6c1a3db5 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -135,7 +135,7 @@ class Webhook { apiMessage = content.resolveData(); } else { apiMessage = APIMessage.create(this, content, options).resolveData(); - if (apiMessage.data.content instanceof Array) { + if (Array.isArray(apiMessage.data.content)) { return Promise.all(apiMessage.split().map(this.send.bind(this))); } } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 075bc675..634dff33 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -138,7 +138,7 @@ class TextBasedChannel { apiMessage = content.resolveData(); } else { apiMessage = APIMessage.create(this, content, options).resolveData(); - if (apiMessage.data.content instanceof Array) { + if (Array.isArray(apiMessage.data.content)) { return Promise.all(apiMessage.split().map(this.send.bind(this))); } } @@ -296,7 +296,7 @@ class TextBasedChannel { * .catch(console.error); */ async bulkDelete(messages, filterOld = false) { - if (messages instanceof Array || messages instanceof Collection) { + if (Array.isArray(messages) || messages instanceof Collection) { let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id || m); if (filterOld) { messageIDs = messageIDs.filter(id => diff --git a/src/util/BitField.js b/src/util/BitField.js index 96b07b82..424a8443 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -32,7 +32,7 @@ class BitField { * @returns {boolean} */ has(bit) { - if (bit instanceof Array) return bit.every(p => this.has(p)); + if (Array.isArray(bit)) return bit.every(p => this.has(p)); bit = this.constructor.resolve(bit); return (this.bitfield & bit) === bit; } @@ -44,7 +44,7 @@ class BitField { * @returns {string[]} */ missing(bits, ...hasParams) { - if (!(bits instanceof Array)) bits = new this.constructor(bits).toArray(false); + if (!Array.isArray(bits)) bits = new this.constructor(bits).toArray(false); return bits.filter(p => !this.has(p, ...hasParams)); } @@ -136,7 +136,7 @@ class BitField { static resolve(bit = 0) { if (typeof bit === 'number' && bit >= 0) return bit; if (bit instanceof BitField) return bit.bitfield; - if (bit instanceof Array) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); + if (Array.isArray(bit)) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); if (typeof bit === 'string') return this.FLAGS[bit]; throw new RangeError('BITFIELD_INVALID'); } diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index 1b905727..91933eb9 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -62,7 +62,7 @@ class DataResolver { * @returns {?string} */ static resolveBase64(data) { - if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`; + if (Buffer.isBuffer(data)) return `data:image/jpg;base64,${data.toString('base64')}`; return data; } @@ -85,7 +85,7 @@ class DataResolver { * @returns {Promise} */ static resolveFile(resource) { - if (!browser && resource instanceof Buffer) return Promise.resolve(resource); + if (!browser && Buffer.isBuffer(resource)) return Promise.resolve(resource); if (browser && resource instanceof ArrayBuffer) return Promise.resolve(Util.convertToBuffer(resource)); if (typeof resource === 'string') { diff --git a/src/util/Util.js b/src/util/Util.js index 82856949..181d5589 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -237,7 +237,7 @@ class Util { */ static resolveString(data) { if (typeof data === 'string') return data; - if (data instanceof Array) return data.join('\n'); + if (Array.isArray(data)) return data.join('\n'); return String(data); } @@ -286,7 +286,7 @@ class Util { if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1)); if (color === 'DEFAULT') return 0; color = Colors[color] || parseInt(color.replace('#', ''), 16); - } else if (color instanceof Array) { + } else if (Array.isArray(color)) { color = (color[0] << 16) + (color[1] << 8) + color[2]; } From 2666a9d6dbb6596b5893e7fbdb57c1d907a1ab6b Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 29 Apr 2019 11:53:32 -0500 Subject: [PATCH 140/428] feat(MessageStore): add remove() (#2468) * MessageStore#remove() * typings --- src/stores/MessageStore.js | 10 ++++++++++ src/structures/Message.js | 12 +++++------- typings/index.d.ts | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index db72114f..e179daa2 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -82,6 +82,16 @@ class MessageStore extends DataStore { }); } + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user + */ + async remove(message, reason) { + message = this.resolveID(message); + if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); + } + async _fetchId(messageID, cache) { const existing = this.get(messageID); if (existing && !existing.partial) return existing; diff --git a/src/structures/Message.js b/src/structures/Message.js index b57f4299..c390ac23 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -454,13 +454,11 @@ class Message extends Base { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.client.api.channels(this.channel.id).messages(this.id) - .delete({ reason }) - .then(() => - this.client.actions.MessageDelete.handle({ - id: this.id, - channel_id: this.channel.id, - }).message); + return this.channel.messages.remove(this.id, reason).then(() => + this.client.actions.MessageDelete.handle({ + id: this.id, + channel_id: this.channel.id, + }).message); } else { return new Promise(resolve => { this.client.setTimeout(() => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 4a57b1ec..1186be6e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1448,6 +1448,7 @@ declare module 'discord.js' { public fetch(message: Snowflake, cache?: boolean): Promise; public fetch(options?: ChannelLogsQueryOptions, cache?: boolean): Promise>; public fetchPinned(cache?: boolean): Promise>; + public remove(message: MessageResolvable, reason?: string): Promise; } export class PresenceStore extends DataStore { From ce1e3d20847ea226839a4cba15dcefa875086d12 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 29 Apr 2019 19:13:41 +0100 Subject: [PATCH 141/428] feat(VoiceConnection): add .voice --- src/client/voice/VoiceConnection.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 727f2196..b532d0c5 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -166,6 +166,14 @@ class VoiceConnection extends EventEmitter { }); } + /** + * The voice state of this connection + * @type {VoiceState} + */ + get voice() { + return this.channel.guild.voiceStates.get(this.client.user.id); + } + /** * Sends a request to the main gateway to join a voice channel. * @param {Object} [options] The options to provide From dd446475371cdbd476e0cb11f79b996d0756e10f Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 29 Apr 2019 19:24:27 +0100 Subject: [PATCH 142/428] voice: Guild.voiceConnection => Guild.voice.connection --- src/client/voice/VoiceConnection.js | 2 +- src/structures/Guild.js | 9 +++++++++ src/structures/VoiceState.js | 11 +++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b532d0c5..947e1912 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -171,7 +171,7 @@ class VoiceConnection extends EventEmitter { * @type {VoiceState} */ get voice() { - return this.channel.guild.voiceStates.get(this.client.user.id); + return this.channel.guild.voice; } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1b5987f1..4f5f02c5 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -398,6 +398,15 @@ class Guild extends Base { null); } + /** + * The voice state for the client user of this guild, if any + * @type {?VoiceState} + * @readonly + */ + get voice() { + return this.me ? this.me.voice : null; + } + /** * Returns the GuildMember form of a User object, if the user is present in the guild. * @param {UserResolvable} user The user that you want to obtain the GuildMember of diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index d74fc6aa..0b5f2b05 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -1,6 +1,7 @@ 'use strict'; const Base = require('./Base'); +const { browser } = require('../util/Constants'); /** * Represents the voice state for a Guild Member. @@ -77,6 +78,16 @@ class VoiceState extends Base { return this.guild.channels.get(this.channelID) || null; } + /** + * If this is a voice state of the client user, then this will refer to the active VoiceConnection for this guild + * @type {?VoiceConnection} + * @readonly + */ + get connection() { + if (browser || this.id !== this.guild.me.id) return null; + return this.client.voice.connections.get(this.guild.id) || null; + } + /** * Whether this member is either self-deafened or server-deafened * @type {?boolean} From bcb0cd838b9fbcbb7467d4618280a820baf2e0a2 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 29 Apr 2019 19:29:16 +0100 Subject: [PATCH 143/428] voice: remove Guild.voiceConnection and VoiceChannel.connection --- src/client/actions/GuildDelete.js | 2 +- src/client/voice/VoiceBroadcast.js | 2 +- src/structures/Guild.js | 12 +----------- src/structures/VoiceChannel.js | 11 ----------- 4 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 7c61ebac..193abc47 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -37,7 +37,7 @@ class GuildDeleteAction extends Action { } for (const channel of guild.channels.values()) this.client.channels.remove(channel.id); - if (guild.voiceConnection) guild.voiceConnection.disconnect(); + if (guild.voice && guild.voice.connection) guild.voice.connection.disconnect(); // Delete guild client.guilds.remove(guild.id); diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index e8751c05..6b5da074 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -13,7 +13,7 @@ const PlayInterface = require('./util/PlayInterface'); * const broadcast = client.voice.createBroadcast(); * broadcast.play('./music.mp3'); * // Play "music.mp3" in all voice connections that the client is in - * for (const connection of client.voiceConnections.values()) { + * for (const connection of client.voice.connections.values()) { * connection.play(broadcast); * } * ``` diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 4f5f02c5..6f86dc14 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -5,7 +5,7 @@ const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); -const { ChannelTypes, DefaultMessageNotifications, PartialTypes, browser } = require('../util/Constants'); +const { ChannelTypes, DefaultMessageNotifications, PartialTypes } = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); @@ -368,16 +368,6 @@ class Guild extends Base { return this.client.channels.get(this.systemChannelID) || null; } - /** - * If the client is connected to any voice channel in this guild, this will be the relevant VoiceConnection - * @type {?VoiceConnection} - * @readonly - */ - get voiceConnection() { - if (browser) return null; - return this.client.voice.connections.get(this.id) || null; - } - /** * The `@everyone` role of the guild * @type {?Role} diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 860241e9..15f2e496 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -42,17 +42,6 @@ class VoiceChannel extends GuildChannel { return coll; } - /** - * The voice connection for this voice channel, if the client is connected - * @type {?VoiceConnection} - * @readonly - */ - get connection() { - const connection = this.guild.voiceConnection; - if (connection && connection.channel.id === this.id) return connection; - return null; - } - /** * Checks if the voice channel is full * @type {boolean} From 0d9bc8664dc2dc3b3bf7d35ba7b9050f2797a211 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 29 Apr 2019 19:31:31 +0100 Subject: [PATCH 144/428] voice: make Guild.voice more robust --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 6f86dc14..df18ea34 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -394,7 +394,7 @@ class Guild extends Base { * @readonly */ get voice() { - return this.me ? this.me.voice : null; + return this.voiceStates.get(this.client.user.id); } /** From a59968f7de048118e307faa8e7a5990813b6f338 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Fri, 3 May 2019 18:08:07 +0300 Subject: [PATCH 145/428] src: add news and store channels, and missing guild props (#3168) * src: Implement store and news channels! * src: Remove code dupe * src: Add missing guild properties * docs: Add a small notice that the channel type may also change * src: Remove re-creation of the MessageStore * lint: Unused Import * src: Requested changes for StoreChannels * typings: Fix typings * src: Moar guild updates * src: Set maximumPresence to the data prop, the already existent one, or default to 5000 * typings: afkChannel is a VC I keep confusing them, ffs Co-Authored-By: vladfrangu * docs: Document that maximumMembers and maximumPresences may be inaccurate before fetching * src Appels requested changes --- src/client/actions/ChannelUpdate.js | 13 ++- .../websocket/handlers/CHANNEL_UPDATE.js | 2 +- src/structures/Channel.js | 10 ++ src/structures/Guild.js | 97 ++++++++++++++++++- src/structures/NewsChannel.js | 18 ++++ src/structures/StoreChannel.js | 22 +++++ src/structures/TextChannel.js | 1 - src/util/Constants.js | 4 + src/util/Structures.js | 2 + typings/index.d.ts | 34 ++++++- 10 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 src/structures/NewsChannel.js create mode 100644 src/structures/StoreChannel.js diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js index b610ea7c..7b716de0 100644 --- a/src/client/actions/ChannelUpdate.js +++ b/src/client/actions/ChannelUpdate.js @@ -1,14 +1,25 @@ 'use strict'; const Action = require('./Action'); +const Channel = require('../../structures/Channel'); +const { ChannelTypes } = require('../../util/Constants'); class ChannelUpdateAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.id); + let channel = client.channels.get(data.id); if (channel) { const old = channel._update(data); + + if (ChannelTypes[channel.type.toUpperCase()] !== data.type) { + const newChannel = Channel.create(this.client, data, channel.guild); + for (const [id, message] of channel.messages) newChannel.messages.set(id, message); + newChannel._typing = new Map(channel._typing); + channel = newChannel; + this.client.channels.set(channel.id, channel); + } + return { old, updated: channel, diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js index 7a0df486..01f5becc 100644 --- a/src/client/websocket/handlers/CHANNEL_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -6,7 +6,7 @@ module.exports = (client, packet) => { const { old, updated } = client.actions.ChannelUpdate.handle(packet.d); if (old && updated) { /** - * Emitted whenever a channel is updated - e.g. name change, topic change. + * Emitted whenever a channel is updated - e.g. name change, topic change, channel type change. * @event Client#channelUpdate * @param {DMChannel|GuildChannel} oldChannel The channel before the update * @param {DMChannel|GuildChannel} newChannel The channel after the update diff --git a/src/structures/Channel.js b/src/structures/Channel.js index de5f462e..ab6adb81 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -116,6 +116,16 @@ class Channel extends Base { channel = new CategoryChannel(guild, data); break; } + case ChannelTypes.NEWS: { + const NewsChannel = Structures.get('NewsChannel'); + channel = new NewsChannel(guild, data); + break; + } + case ChannelTypes.STORE: { + const StoreChannel = Structures.get('StoreChannel'); + channel = new StoreChannel(guild, data); + break; + } } if (channel) guild.channels.set(channel.id, channel); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index df18ea34..a5d13fa6 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -186,6 +186,27 @@ class Guild extends Base { */ this.embedEnabled = data.embed_enabled; + /** + * Whether widget images are enabled on this guild + * @type {?boolean} + * @name Guild#widgetEnabled + */ + if (typeof data.widget_enabled !== 'undefined') this.widgetEnabled = data.widget_enabled; + + /** + * The widget channel ID, if enabled + * @type {?string} + * @name Guild#widgetChannelID + */ + if (typeof data.widget_channel_id !== 'undefined') this.widgetChannelID = data.widget_channel_id; + + /** + * The embed channel ID, if enabled + * @type {?string} + * @name Guild#embedChannelID + */ + if (typeof data.embed_channel_id !== 'undefined') this.embedChannelID = data.embed_channel_id; + /** * The verification level of the guild * @type {number} @@ -211,12 +232,46 @@ class Guild extends Base { this.joinedTimestamp = data.joined_at ? new Date(data.joined_at).getTime() : this.joinedTimestamp; /** - * The value set for a guild's default message notifications + * The value set for the guild's default message notifications * @type {DefaultMessageNotifications|number} */ this.defaultMessageNotifications = DefaultMessageNotifications[data.default_message_notifications] || data.default_message_notifications; + /** + * The maximum amount of members the guild can have + * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter + * @type {?number} + * @name Guild#maximumMembers + */ + if (typeof data.max_members !== 'undefined') this.maximumMembers = data.max_members || 250000; + + /** + * The maximum amount of presences the guild can have + * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter + * @type {?number} + * @name Guild#maximumPresences + */ + if (typeof data.max_presences !== 'undefined') this.maximumPresences = data.max_presences || 5000; + + /** + * The vanity URL code of the guild, if any + * @type {?string} + */ + this.vanityURLCode = data.vanity_url_code; + + /** + * The description of the guild, if any + * @type {?string} + */ + this.description = data.description; + + /** + * The hash of the guild banner + * @type {?string} + */ + this.banner = data.banner; + this.id = data.id; this.available = !data.unavailable; this.features = data.features || this.features || []; @@ -274,6 +329,16 @@ class Guild extends Base { } } + /** + * The URL to this guild's banner. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + bannerURL({ format, size } = {}) { + if (!this.banner) return null; + return this.client.rest.cdn.Banner(this.id, this.banner, format, size); + } + /** * The timestamp the guild was created at * @type {number} @@ -368,6 +433,24 @@ class Guild extends Base { return this.client.channels.get(this.systemChannelID) || null; } + /** + * Widget channel for this guild + * @type {?TextChannel} + * @readonly + */ + get widgetChannel() { + return this.client.channels.get(this.widgetChannelID) || null; + } + + /** + * Embed channel for this guild + * @type {?TextChannel} + * @readonly + */ + get embedChannel() { + return this.client.channels.get(this.embedChannelID) || null; + } + /** * The `@everyone` role of the guild * @type {?Role} @@ -409,6 +492,17 @@ class Guild extends Base { return this.members.resolve(user); } + /** + * Fetches this guild. + * @returns {Promise} + */ + fetch() { + return this.client.api.guilds(this.id).get().then(data => { + this._patch(data); + return this; + }); + } + /** * An object containing information about a guild member's ban. * @typedef {Object} BanInfo @@ -975,6 +1069,7 @@ class Guild extends Base { }); json.iconURL = this.iconURL(); json.splashURL = this.splashURL(); + json.bannerURL = this.bannerURL(); return json; } diff --git a/src/structures/NewsChannel.js b/src/structures/NewsChannel.js new file mode 100644 index 00000000..76727fcd --- /dev/null +++ b/src/structures/NewsChannel.js @@ -0,0 +1,18 @@ +'use strict'; + +const TextChannel = require('./TextChannel'); + +/** + * Represents a guild news channel on Discord. + * @extends {TextChannel} + */ +class NewsChannel extends TextChannel { + _patch(data) { + super._patch(data); + + // News channels don't have a rate limit per user, remove it + this.rateLimitPerUser = undefined; + } +} + +module.exports = NewsChannel; diff --git a/src/structures/StoreChannel.js b/src/structures/StoreChannel.js new file mode 100644 index 00000000..87cb0406 --- /dev/null +++ b/src/structures/StoreChannel.js @@ -0,0 +1,22 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); + +/** + * Represents a guild store channel on Discord. + * @extends {GuildChannel} + */ +class StoreChannel extends GuildChannel { + _patch(data) { + super._patch(data); + + /** + * If the guild considers this channel NSFW + * @type {boolean} + * @readonly + */ + this.nsfw = data.nsfw; + } +} + +module.exports = StoreChannel; diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index e50bee05..836c55a0 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -139,7 +139,6 @@ class TextChannel extends GuildChannel { awaitMessages() {} bulkDelete() {} acknowledge() {} - _cacheMessage() {} } TextBasedChannel.applyToClass(TextChannel, true); diff --git a/src/util/Constants.js b/src/util/Constants.js index f314d3d6..0d8f3b6e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -131,6 +131,8 @@ exports.Endpoints = { if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); }, + Banner: (guildID, hash, format = 'webp', size) => + makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }), Icon: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }), AppIcon: (clientID, hash, { format = 'webp', size } = {}) => @@ -409,6 +411,8 @@ exports.ChannelTypes = { VOICE: 2, GROUP: 3, CATEGORY: 4, + NEWS: 5, + STORE: 6, }; exports.ClientApplicationAssetTypes = { diff --git a/src/util/Structures.js b/src/util/Structures.js index 2dc26770..02529d26 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -75,6 +75,8 @@ const structures = { TextChannel: require('../structures/TextChannel'), VoiceChannel: require('../structures/VoiceChannel'), CategoryChannel: require('../structures/CategoryChannel'), + NewsChannel: require('../structures/NewsChannel'), + StoreChannel: require('../structures/StoreChannel'), GuildMember: require('../structures/GuildMember'), Guild: require('../structures/Guild'), Message: require('../structures/Message'), diff --git a/typings/index.d.ts b/typings/index.d.ts index 1186be6e..ed0430dd 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -122,7 +122,7 @@ declare module 'discord.js' { public readonly createdTimestamp: number; public deleted: boolean; public id: Snowflake; - public type: 'dm' | 'text' | 'voice' | 'category' | 'unknown'; + public type: 'dm' | 'text' | 'voice' | 'category' | 'news' | 'store' | 'unknown'; public delete(reason?: string): Promise; public fetch(): Promise; public toString(): string; @@ -393,9 +393,7 @@ declare module 'discord.js' { private _sortedChannels(channel: Channel): Collection; private _memberSpeakUpdate(user: Snowflake, speaking: boolean): void; - protected setup(data: any): void; - - public readonly afkChannel: VoiceChannel | null; + public readonly afkChannel: VoiceChannel; public afkChannelID: Snowflake; public afkTimeout: number; public applicationID: Snowflake; @@ -432,13 +430,25 @@ declare module 'discord.js' { public readonly systemChannel: TextChannel | null; public systemChannelID: Snowflake; public verificationLevel: number; + public maximumMembers: number; + public maximumPresences: number; + public vanityURLCode: string; + public description: string; + public banner: string; + public widgetEnabled: boolean; + public widgetChannelID: Snowflake; + public readonly widgetChannel: TextChannel; + public embedChannelID: Snowflake; + public readonly embedChannel: TextChannel; public readonly verified: boolean; public readonly voiceConnection: VoiceConnection | null; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; + public bannerURL(options?: AvatarOptions): string; public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; public equals(guild: Guild): boolean; + public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchBans(): Promise>; public fetchIntegrations(): Promise>; @@ -532,6 +542,11 @@ declare module 'discord.js' { public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } + export class StoreChannel extends GuildChannel { + constructor(guild: Guild, data?: object); + public nsfw: boolean; + } + export class GuildEmoji extends Emoji { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; @@ -1064,6 +1079,17 @@ declare module 'discord.js' { public fetchWebhooks(): Promise>; } + export class NewsChannel extends TextBasedChannel(GuildChannel) { + constructor(guild: Guild, data?: object); + public readonly members: Collection; + public messages: MessageStore; + public nsfw: boolean; + public topic: string; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; + public setNSFW(nsfw: boolean, reason?: string): Promise; + public fetchWebhooks(): Promise>; + } + export class User extends PartialTextBasedChannel(Base) { constructor(client: Client, data: object); public avatar: string | null; From 692494dc043e0c98f9e5075138e461c9a972a615 Mon Sep 17 00:00:00 2001 From: Jacz <23615291+MrJacz@users.noreply.github.com> Date: Sat, 4 May 2019 01:11:11 +1000 Subject: [PATCH 146/428] feat(VoiceState): self mute/deaf methods (#3243) * Implemented setSelfMute/Deaf, done typings, fixed bug in VoiceState with errors. * Completed requested changes * return send in sendVoiceStateUpdate so its a promise, update typings * Updated methods to return a boolean * Requested changes * Fix bug * Update src/structures/VoiceState.js Co-Authored-By: MrJacz <23615291+MrJacz@users.noreply.github.com> * fix --- src/client/voice/VoiceConnection.js | 7 ++++--- src/errors/Messages.js | 2 ++ src/structures/VoiceState.js | 29 +++++++++++++++++++++++++++++ typings/index.d.ts | 7 +++++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 947e1912..cd2d29a7 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -177,20 +177,21 @@ class VoiceConnection extends EventEmitter { /** * Sends a request to the main gateway to join a voice channel. * @param {Object} [options] The options to provide + * @returns {Promise} * @private */ sendVoiceStateUpdate(options = {}) { options = Util.mergeDefault({ guild_id: this.channel.guild.id, channel_id: this.channel.id, - self_mute: false, - self_deaf: false, + self_mute: this.voice ? this.voice.selfMute : false, + self_deaf: this.voice ? this.voice.selfDeaf : false, }, options); const queueLength = this.channel.guild.shard.ratelimit.queue.length; this.emit('debug', `Sending voice state update (queue length is ${queueLength}): ${JSON.stringify(options)}`); - this.channel.guild.shard.send({ + return this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, }); diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 1cfe4da9..1b2bdcdb 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -51,6 +51,8 @@ const Messages = { VOICE_PRISM_DEMUXERS_NEED_STREAM: 'To play a webm/ogg stream, you need to pass a ReadableStream.', VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.', + VOICE_STATE_NOT_OWN: 'You cannot self-deafen/mute on VoiceStates that do not belong to the ClientUser.', + VOICE_STATE_INVALID_TYPE: name => `${name} must be a boolean.`, UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', UDP_ADDRESS_MALFORMED: 'Malformed UDP address or port.', diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 0b5f2b05..165aa7b6 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -2,6 +2,7 @@ const Base = require('./Base'); const { browser } = require('../util/Constants'); +const { Error, TypeError } = require('../errors'); /** * Represents the voice state for a Guild Member. @@ -138,6 +139,34 @@ class VoiceState extends Base { return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); } + /** + * Self-mutes/unmutes the bot for this voice state. + * @param {boolean} mute Whether or not the bot should be self-muted + * @returns {Promise} true if the voice state was successfully updated, otherwise false + */ + async setSelfMute(mute) { + if (this.id !== this.client.user.id) throw new Error('VOICE_STATE_NOT_OWN'); + if (typeof mute !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'mute'); + if (!this.connection) return false; + this.selfMute = mute; + await this.connection.sendVoiceStateUpdate(); + return true; + } + + /** + * Self-deafens/undeafens the bot for this voice state. + * @param {boolean} deaf Whether or not the bot should be self-deafened + * @returns {Promise} true if the voice state was successfully updated, otherwise false + */ + async setSelfDeaf(deaf) { + if (this.id !== this.client.user.id) return new Error('VOICE_STATE_NOT_OWN'); + if (typeof deaf !== 'boolean') return new TypeError('VOICE_STATE_INVALID_TYPE', 'deaf'); + if (!this.connection) return false; + this.selfDeaf = deaf; + await this.connection.sendVoiceStateUpdate(); + return true; + } + toJSON() { return super.toJSON({ id: true, diff --git a/typings/index.d.ts b/typings/index.d.ts index ed0430dd..215abc3a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1202,7 +1202,7 @@ declare module 'discord.js' { private onSessionDescription(mode: string, secret: string): void; private onSpeaking(data: object): void; private reconnect(token: string, endpoint: string): void; - private sendVoiceStateUpdate(options: object): void; + private sendVoiceStateUpdate(options: object): Promise; private setSessionID(sessionID: string): void; private setSpeaking(value: BitFieldResolvable): void; private setTokenAndEndpoint(token: string, endpoint: string): void; @@ -1215,6 +1215,7 @@ declare module 'discord.js' { public receiver: VoiceReceiver; public speaking: Readonly; public status: VoiceStatus; + public readonly voice: VoiceState; public voiceManager: ClientVoiceManager; public disconnect(): void; public play(input: VoiceBroadcast | Readable | string, options?: StreamOptions): StreamDispatcher; @@ -1284,8 +1285,10 @@ declare module 'discord.js' { public sessionID?: string; public readonly speaking: boolean | null; - public setDeaf(mute: boolean, reason?: string): Promise; + public setDeaf(deaf: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; + public setSelfDeaf(deaf: boolean): Promise; + public setSelfMute(mute: boolean): Promise; } class VolumeInterface extends EventEmitter { From 176fc47699e43c0eeb5fc9d097ffea692e2cbc75 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Fri, 3 May 2019 16:38:57 +0100 Subject: [PATCH 147/428] feat(Actions): use partials for messageDeleteBulk (#3240) * make use of partials * don't cache the messages * pass each message within the for..of iteration --- src/client/actions/Action.js | 4 ++-- src/client/actions/MessageDeleteBulk.js | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 5746d276..0b03c071 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -33,14 +33,14 @@ class GenericAction { this.client.channels.get(id)); } - getMessage(data, channel) { + getMessage(data, channel, cache = true) { const id = data.message_id || data.id; return data.message || (this.client.options.partials.includes(PartialTypes.MESSAGE) ? channel.messages.add({ id, channel_id: channel.id, guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), - }) : + }, cache) : channel.messages.get(id)); } diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index 53f4ba05..f80bc7c4 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -13,7 +13,10 @@ class MessageDeleteBulkAction extends Action { const ids = data.ids; const messages = new Collection(); for (const id of ids) { - const message = channel.messages.get(id); + const message = this.getMessage({ + id, + guild_id: data.guild_id, + }, channel, false); if (message) { message.deleted = true; messages.set(message.id, message); From d7f8fd1ae0a56e3fee7d87c75e4cdfab5f785604 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 4 May 2019 16:21:49 +0100 Subject: [PATCH 148/428] fix #3244 --- src/structures/Guild.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a5d13fa6..b1361aef 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -57,6 +57,8 @@ class Guild extends Base { */ this.presences = new PresenceStore(this.client); + this.voiceStates = new VoiceStateStore(this); + /** * Whether the bot has been removed from the guild * @type {boolean} @@ -307,8 +309,8 @@ class Guild extends Base { } } - if (!this.voiceStates) this.voiceStates = new VoiceStateStore(this); if (data.voice_states) { + this.voiceStates.clear(); for (const voiceState of data.voice_states) { this.voiceStates.add(voiceState); } From e64773e21bb4070d57ceb1910b30d753b2585c8b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 4 May 2019 16:46:42 +0100 Subject: [PATCH 149/428] Add ability to kick members from VoiceChannels and remove duplicated methods (#3242) * feat(voice): kick members from voice channels * fix(VoiceState): improve stability in checking for client user * feat(VoiceState): add setChannel for moving/kicking members * update typings * remove duplicated methods across GuildMember and VoiceState member.setDeaf => member.voice.setDeaf member.setMute => member.voice.setMute member.setVoiceChannel => member.voice.setChannel --- src/structures/GuildMember.js | 36 +++++------------------------------ src/structures/VoiceState.js | 15 ++++++++++++++- typings/index.d.ts | 6 ++---- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 567c27e8..697f8e06 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -254,7 +254,8 @@ class GuildMember extends Base { * @property {Collection|RoleResolvable[]} [roles] The roles or role IDs to apply * @property {boolean} [mute] Whether or not the member should be muted * @property {boolean} [deaf] Whether or not the member should be deafened - * @property {ChannelResolvable} [channel] Channel to move member to (if they are connected to voice) + * @property {ChannelResolvable|null} [channel] Channel to move member to (if they are connected to voice), or `null` + * if you want to kick them from voice */ /** @@ -270,8 +271,10 @@ class GuildMember extends Base { throw new Error('GUILD_VOICE_CHANNEL_RESOLVE'); } data.channel_id = data.channel.id; - data.channel = null; + } else if (data.channel === null) { + data.channel_id = null; } + data.channel = undefined; if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); let endpoint = this.client.api.guilds(this.guild.id); if (this.user.id === this.client.user.id) { @@ -289,35 +292,6 @@ class GuildMember extends Base { return clone; } - /** - * Mutes/unmutes this member. - * @param {boolean} mute Whether or not the member should be muted - * @param {string} [reason] Reason for muting or unmuting - * @returns {Promise} - */ - setMute(mute, reason) { - return this.edit({ mute }, reason); - } - - /** - * Deafens/undeafens this member. - * @param {boolean} deaf Whether or not the member should be deafened - * @param {string} [reason] Reason for deafening or undeafening - * @returns {Promise} - */ - setDeaf(deaf, reason) { - return this.edit({ deaf }, reason); - } - - /** - * Moves this member to the given channel. - * @param {ChannelResolvable} channel The channel to move the member to - * @returns {Promise} - */ - setVoiceChannel(channel) { - return this.edit({ channel }); - } - /** * Sets the nickname for this member. * @param {string} nick The nickname for the guild member diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 165aa7b6..e2cb2ab9 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -85,7 +85,7 @@ class VoiceState extends Base { * @readonly */ get connection() { - if (browser || this.id !== this.guild.me.id) return null; + if (browser || this.id !== this.client.user.id) return null; return this.client.voice.connections.get(this.guild.id) || null; } @@ -139,6 +139,19 @@ class VoiceState extends Base { return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); } + /** + * Moves the member to a different channel, or kick them from the one they're in. + * @param {ChannelResolvable|null} [channel] Channel to move the member to, or `null` if you want to kick them from + * voice + * @param {string} [reason] Reason for moving member to another channel or kicking + * @returns {Promise} + */ + setChannel(channel, reason) { + return this.member ? + this.member.edit({ channel }, reason) : + Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + /** * Self-mutes/unmutes the bot for this voice state. * @param {boolean} mute Whether or not the bot should be self-muted diff --git a/typings/index.d.ts b/typings/index.d.ts index 215abc3a..726f0742 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -591,10 +591,7 @@ declare module 'discord.js' { public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean }): boolean; public kick(reason?: string): Promise; public permissionsIn(channel: ChannelResolvable): Readonly; - public setDeaf(deaf: boolean, reason?: string): Promise; - public setMute(mute: boolean, reason?: string): Promise; public setNickname(nickname: string, reason?: string): Promise; - public setVoiceChannel(voiceChannel: ChannelResolvable): Promise; public toJSON(): object; public toString(): string; } @@ -1287,6 +1284,7 @@ declare module 'discord.js' { public setDeaf(deaf: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; + public setChannel(channel: ChannelResolvable | null, reason?: string): Promise; public setSelfDeaf(deaf: boolean): Promise; public setSelfMute(mute: boolean): Promise; } @@ -1927,7 +1925,7 @@ declare module 'discord.js' { roles?: Collection | RoleResolvable[]; mute?: boolean; deaf?: boolean; - channel?: ChannelResolvable; + channel?: ChannelResolvable | null; } type GuildMemberResolvable = GuildMember | UserResolvable; From 8b83e2fdcbead62ef018b6423f02e07ade0cd40f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 4 May 2019 19:05:04 +0200 Subject: [PATCH 150/428] typings(Presence): add missing guild property --- src/structures/Presence.js | 2 +- typings/index.d.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 55937cea..22c70e6b 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -48,7 +48,7 @@ class Presence { * The guild of this presence * @type {?Guild} */ - this.guild = data.guild; + this.guild = data.guild || null; this.patch(data); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 726f0742..35c8a6f1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -828,11 +828,12 @@ declare module 'discord.js' { export class Presence { constructor(client: Client, data?: object); public activity: Activity | null; - public flags: Readonly; - public status: PresenceStatus; public clientStatus: ClientPresenceStatusData | null; - public readonly user: User | null; + public flags: Readonly; + public guild: Guild | null; public readonly member: GuildMember | null; + public status: PresenceStatus; + public readonly user: User | null; public equals(presence: Presence): boolean; } From 8915bc1d37f1b83fc9bbd3cfef337bd5f75e4779 Mon Sep 17 00:00:00 2001 From: Darqam Date: Mon, 6 May 2019 20:08:56 +0200 Subject: [PATCH 151/428] docs:(Client): disambiguate the description of channels collection (#3251) * Disambiguate the description of .channels Although not explicitly said, the current wording makes it seem like all channels are cached and available at any time in this store. Hopefully this variation makes it a bit clearer. * make more explicit (I think) * remove trailing white spaces --- src/client/Client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index 58d3c7db..257a5cd8 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -113,7 +113,8 @@ class Client extends BaseClient { /** * All of the {@link Channel}s that the client is currently handling, mapped by their IDs - * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot - * is a member of, and all DM channels + * is a member of. Note that DM channels will not be initially cached, and thus not be present + * in the store without their explicit fetching or use. * @type {ChannelStore} */ this.channels = new ChannelStore(this); From 3d4513268d0aa42a2e822a50ecac47d3996272dd Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 7 May 2019 08:30:34 -0500 Subject: [PATCH 152/428] Add optional zstd for faster WebSocket data inflation (#3223) * zstd --- README.md | 3 +- package.json | 4 +- src/client/websocket/WebSocketShard.js | 60 +++++++++++++++++--------- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 69a779fc..12ee3a2b 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ Using opusscript is only recommended for development environments where node-opu For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) +- [zucc](https://www.npmjs.com/package/zucc) for significantly faster WebSocket data inflation (`npm install zucc`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) diff --git a/package.json b/package.json index fc5d8d1e..717fe065 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "erlpack": "discordapp/erlpack", "libsodium-wrappers": "^0.7.4", "sodium": "^3.0.2", - "zlib-sync": "^0.1.4" + "zlib-sync": "^0.1.4", + "zucc": "^0.1.0" }, "devDependencies": { "@types/node": "^10.12.24", @@ -80,6 +81,7 @@ "sodium": false, "worker_threads": false, "zlib-sync": false, + "zucc": false, "src/sharding/Shard.js": false, "src/sharding/ShardClientUtil.js": false, "src/sharding/ShardingManager.js": false, diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 67d494e2..861462c5 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -4,12 +4,21 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); +let zstd; +let decoder; + let zlib; + try { - zlib = require('zlib-sync'); - if (!zlib.Inflate) zlib = require('pako'); -} catch (err) { - zlib = require('pako'); + zstd = require('zucc'); + decoder = new TextDecoder('utf8'); +} catch (e) { + try { + zlib = require('zlib-sync'); + if (!zlib.Inflate) zlib = require('pako'); + } catch (err) { + zlib = require('pako'); + } } /** @@ -206,11 +215,15 @@ class WebSocketShard extends EventEmitter { return; } - this.inflate = new zlib.Inflate({ - chunkSize: 65535, - flush: zlib.Z_SYNC_FLUSH, - to: WebSocket.encoding === 'json' ? 'string' : '', - }); + if (zstd) { + this.inflate = new zstd.DecompressStream(); + } else { + this.inflate = new zlib.Inflate({ + chunkSize: 65535, + flush: zlib.Z_SYNC_FLUSH, + to: WebSocket.encoding === 'json' ? 'string' : '', + }); + } this.debug(`Trying to connect to ${gateway}, version ${client.options.ws.version}`); @@ -219,7 +232,7 @@ class WebSocketShard extends EventEmitter { const ws = this.connection = WebSocket.create(gateway, { v: client.options.ws.version, - compress: 'zlib-stream', + compress: zstd ? 'zstd-stream' : 'zlib-stream', }); ws.onopen = this.onOpen.bind(this); ws.onmessage = this.onMessage.bind(this); @@ -243,19 +256,26 @@ class WebSocketShard extends EventEmitter { * @private */ onMessage({ data }) { - if (data instanceof ArrayBuffer) data = new Uint8Array(data); - const l = data.length; - const flush = l >= 4 && - data[l - 4] === 0x00 && - data[l - 3] === 0x00 && - data[l - 2] === 0xFF && - data[l - 1] === 0xFF; + let raw; + if (zstd) { + const ab = this.inflate.decompress(new Uint8Array(data).buffer); + raw = decoder.decode(ab); + } else { + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + const l = data.length; + const flush = l >= 4 && + data[l - 4] === 0x00 && + data[l - 3] === 0x00 && + data[l - 2] === 0xFF && + data[l - 1] === 0xFF; - this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH); - if (!flush) return; + this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH); + if (!flush) return; + raw = this.inflate.result; + } let packet; try { - packet = WebSocket.unpack(this.inflate.result); + packet = WebSocket.unpack(raw); this.manager.client.emit(Events.RAW, packet, this.id); if (packet.op === OPCodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id); } catch (err) { From 0dd3ed72ef3e2c76fe330e13ec572ad05be505a2 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 7 May 2019 20:56:39 +0100 Subject: [PATCH 153/428] fix(Partials): Client#event:messageUpdate(oldMessage) and MessageReactionAdd on guild channels (#3250) * ref: add getPayload and use for other get* methods * return existing data.* * use Action.getUser() --- src/client/actions/Action.js | 69 +++++++++++---------- src/client/actions/MessageReactionAdd.js | 2 +- src/client/actions/MessageReactionRemove.js | 2 +- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 0b03c071..7f21318b 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -23,47 +23,52 @@ class GenericAction { return data; } - getChannel(data) { - const id = data.channel_id || data.id; - return data.channel || (this.client.options.partials.includes(PartialTypes.CHANNEL) ? - this.client.channels.add({ - id, - guild_id: data.guild_id, - }) : - this.client.channels.get(id)); + getPayload(data, store, id, partialType, cache) { + const existing = store.get(id); + if (!existing && this.client.options.partials.includes(partialType)) { + return store.add(data, cache); + } + return existing; } - getMessage(data, channel, cache = true) { + getChannel(data) { + const id = data.channel_id || data.id; + return data.channel || this.getPayload({ + id, + guild_id: data.guild_id, + }, this.client.channels, id, PartialTypes.CHANNEL); + } + + getMessage(data, channel, cache) { const id = data.message_id || data.id; - return data.message || (this.client.options.partials.includes(PartialTypes.MESSAGE) ? - channel.messages.add({ - id, - channel_id: channel.id, - guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), - }, cache) : - channel.messages.get(id)); + return data.message || this.getPayload({ + id, + channel_id: channel.id, + guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), + }, channel.messages, id, PartialTypes.MESSAGE, cache); } getReaction(data, message, user) { - const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); - const existing = message.reactions.get(emojiID); - if (!existing && this.client.options.partials.includes(PartialTypes.MESSAGE)) { - return message.reactions.add({ - emoji: data.emoji, - count: 0, - me: user.id === this.client.user.id, - }); - } - return existing; + const id = data.emoji.id || decodeURIComponent(data.emoji.name); + return this.getPayload({ + emoji: data.emoji, + count: 0, + me: user.id === this.client.user.id, + }, message.reactions, id, PartialTypes.MESSAGE); } getMember(data, guild) { - const userID = data.user.id; - const existing = guild.members.get(userID); - if (!existing && this.client.options.partials.includes(PartialTypes.GUILD_MEMBER)) { - return guild.members.add({ user: { id: userID } }); - } - return existing; + const id = data.user.id; + return this.getPayload({ + user: { + id, + }, + }, guild.members, id, PartialTypes.GUILD_MEMBER); + } + + getUser(data) { + const id = data.user_id; + return data.user || this.getPayload({ id }, this.client.users, id, PartialTypes.USER); } } diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 185b980a..e7ae7e26 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -14,7 +14,7 @@ class MessageReactionAdd extends Action { handle(data) { if (!data.emoji) return false; - const user = data.user || this.client.users.get(data.user_id); + const user = this.getUser(data); if (!user) return false; // Verify channel diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 4e7995f8..c6c0d664 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -14,7 +14,7 @@ class MessageReactionRemove extends Action { handle(data) { if (!data.emoji) return false; - const user = this.client.users.get(data.user_id); + const user = this.getUser(data); if (!user) return false; // Verify channel From 75d5598fdada9ad1913b533e70d049de0d4ff7af Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Wed, 8 May 2019 21:03:19 +0100 Subject: [PATCH 154/428] import TextDecoder from Util (#3258) --- src/client/websocket/WebSocketShard.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 861462c5..1ad7065d 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -3,6 +3,7 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); +const { TextDecoder } = require('util'); let zstd; let decoder; From 72dd872fce70754b0f7616f3d31b5616a48c7188 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 12 May 2019 20:29:28 +0100 Subject: [PATCH 155/428] VoiceBroadcast.{dispatchers -> subscribers} --- src/client/voice/VoiceBroadcast.js | 16 ++++++++-------- .../voice/dispatcher/BroadcastDispatcher.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 6b5da074..2f30f355 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -28,10 +28,10 @@ class VoiceBroadcast extends EventEmitter { */ this.client = client; /** - * The dispatchers playing this broadcast + * The subscribed StreamDispatchers of this broadcast * @type {StreamDispatcher[]} */ - this.dispatchers = []; + this.subscribers = []; this.player = new BroadcastAudioPlayer(this); } @@ -66,19 +66,19 @@ class VoiceBroadcast extends EventEmitter { * Ends the broadcast, unsubscribing all subscribed channels and deleting the broadcast */ end() { - for (const dispatcher of this.dispatchers) this.delete(dispatcher); + for (const dispatcher of this.subscribers) this.delete(dispatcher); const index = this.client.voice.broadcasts.indexOf(this); if (index !== -1) this.client.voice.broadcasts.splice(index, 1); } add(dispatcher) { - const index = this.dispatchers.indexOf(dispatcher); + const index = this.subscribers.indexOf(dispatcher); if (index === -1) { - this.dispatchers.push(dispatcher); + this.subscribers.push(dispatcher); /** * Emitted whenever a stream dispatcher subscribes to the broadcast. * @event VoiceBroadcast#subscribe - * @param {StreamDispatcher} dispatcher The subscribed dispatcher + * @param {StreamDispatcher} subscriber The subscribed dispatcher */ this.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); return true; @@ -88,9 +88,9 @@ class VoiceBroadcast extends EventEmitter { } delete(dispatcher) { - const index = this.dispatchers.indexOf(dispatcher); + const index = this.subscribers.indexOf(dispatcher); if (index !== -1) { - this.dispatchers.splice(index, 1); + this.subscribers.splice(index, 1); dispatcher.destroy(); /** * Emitted whenever a stream dispatcher unsubscribes to the broadcast. diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index d7fd8dfa..ae8d412e 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -15,7 +15,7 @@ class BroadcastDispatcher extends StreamDispatcher { _write(chunk, enc, done) { if (!this.startTime) this.startTime = Date.now(); - for (const dispatcher of this.broadcast.dispatchers) { + for (const dispatcher of this.broadcast.subscribers) { dispatcher._write(chunk, enc); } this._step(done); From 16fcfa3db37ac72d0e0b13a5bc03585903b8905e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 15 May 2019 22:13:12 +0200 Subject: [PATCH 156/428] fix(WebSocketManager): rethrow unknown errors when connecting a WebSocketShard --- src/client/websocket/WebSocketManager.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 6dd1a3f2..6dec7c40 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -264,9 +264,12 @@ class WebSocketManager extends EventEmitter { } catch (error) { if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) { throw new DJSError(WSCodes[error.code]); - } else { + // Undefined if session is invalid, error event (or uws' event mimicking it) for regular closes + } else if (!error || error.code) { this.debug('Failed to connect to the gateway, requeueing...', shard); this.shardQueue.add(shard); + } else { + throw error; } } // If we have more shards, add a 5s delay From 55447fd4da08ae9e633dca3c4faff5a9b77d440e Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Wed, 15 May 2019 15:33:27 -0500 Subject: [PATCH 157/428] docs(TextChanne): specify unit of rateLimitPerUser (#3272) * Update TextChannel.js Update `setRateLimitPerUser` description to specify the `number` is in seconds, per the Discord docs * Update TextChannel.js Add unit to the rateLimitPerUser property * Update GuildChannel.js --- src/structures/GuildChannel.js | 2 +- src/structures/TextChannel.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 4ba8eb99..8ffdcfe8 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -291,7 +291,7 @@ class GuildChannel extends Channel { * Lock the permissions of the channel to what the parent's permissions are * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] * Permission overwrites for the channel - * @property {number} [rateLimitPerUser] The ratelimit per user for the channel + * @property {number} [rateLimitPerUser] The ratelimit per user for the channel in seconds */ /** diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 836c55a0..55c3b0d2 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -50,7 +50,7 @@ class TextChannel extends GuildChannel { this.lastMessageID = data.last_message_id; /** - * The ratelimit per user for this channel + * The ratelimit per user for this channel in seconds * @type {number} */ this.rateLimitPerUser = data.rate_limit_per_user || 0; @@ -66,7 +66,7 @@ class TextChannel extends GuildChannel { /** * Sets the rate limit per user for this channel. - * @param {number} rateLimitPerUser The new ratelimit + * @param {number} rateLimitPerUser The new ratelimit in seconds * @param {string} [reason] Reason for changing the channel's ratelimits * @returns {Promise} */ From 1bafa4b86bcedb62b2fb349dda06f0867273481b Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 16 May 2019 19:56:19 +0200 Subject: [PATCH 158/428] fix(READY): do not overwrite Client#user when reidentifying See #3216, this commit attempts to fix losing ClientUser#_typing, which results in no longer being able to clear typing intervals --- src/client/websocket/handlers/READY.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 89f535f3..039f8f23 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -3,10 +3,14 @@ let ClientUser; module.exports = (client, { d: data }, shard) => { - if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); - const clientUser = new ClientUser(client, data.user); - client.user = clientUser; - client.users.set(clientUser.id, clientUser); + if (client.user) { + client.user._patch(data.user); + } else { + if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); + const clientUser = new ClientUser(client, data.user); + client.user = clientUser; + client.users.set(clientUser.id, clientUser); + } for (const guild of data.guilds) { guild.shardID = shard.id; From 97de79bd5e34ab043a75db1f4fdb738bd20f21c3 Mon Sep 17 00:00:00 2001 From: didinele Date: Thu, 16 May 2019 22:14:46 +0300 Subject: [PATCH 159/428] fix(typings): Guild#member can return null (#3274) * fix(typings): Guild#member did not have undefined as a return type * oops, it can apparently return null --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 35c8a6f1..18575b30 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -459,7 +459,7 @@ declare module 'discord.js' { public fetchEmbed(): Promise; public iconURL(options?: AvatarOptions): string; public leave(): Promise; - public member(user: UserResolvable): GuildMember; + public member(user: UserResolvable): GuildMember | null; public setAFKChannel(afkChannel: ChannelResolvable, reason?: string): Promise; public setAFKTimeout(afkTimeout: number, reason?: string): Promise; public setChannelPositions(channelPositions: ChannelPosition[]): Promise; From 3ad16fa351cc6a806cd88f1839569b54198664af Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 18 May 2019 14:08:12 +0200 Subject: [PATCH 160/428] fix(GuildMember): do not create a channel key when editing This is to not break GuildMember#setNickname for the current user --- src/structures/GuildMember.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 697f8e06..bc2d01e6 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -271,10 +271,11 @@ class GuildMember extends Base { throw new Error('GUILD_VOICE_CHANNEL_RESOLVE'); } data.channel_id = data.channel.id; + data.channel = undefined; } else if (data.channel === null) { data.channel_id = null; + data.channel = undefined; } - data.channel = undefined; if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); let endpoint = this.client.api.guilds(this.guild.id); if (this.user.id === this.client.user.id) { From abebeac193f70e25c842cf4e51e12dd53e1096ca Mon Sep 17 00:00:00 2001 From: bdistin Date: Sat, 18 May 2019 12:02:23 -0500 Subject: [PATCH 161/428] fix(Message#pinnable): you can't pin system messages (#3279) --- src/structures/Message.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index c390ac23..8e5f423a 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -357,8 +357,8 @@ class Message extends Base { * @readonly */ get pinnable() { - return !this.guild || - this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false); + return this.type === 'DEFAULT' && (!this.guild || + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false)); } /** From b3060ea229e5a5395f7fb67291ead0440e6c55ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Mon, 20 May 2019 20:49:28 +0200 Subject: [PATCH 162/428] typings(Collection): use T in accumulator and initialValue when reducing (#3284) This brings some consistency with Array#reduce's typings and to reality. --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 18575b30..c98e7a16 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -311,7 +311,7 @@ declare module 'discord.js' { public random(count: number): V[]; public randomKey(): K | undefined; public randomKey(count: number): K[]; - public reduce(fn: (accumulator: any, value: V, key: K, collection: Collection) => T, initialValue?: any): T; + public reduce(fn: (accumulator: T, value: V, key: K, collection: Collection) => T, initialValue?: T): T; public some(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; public sort(compareFunction?: (a: V, b: V, c?: K, d?: K) => number): Collection; public sweep(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): number; From 97f3b6c5eb5596501ca4dbbf18b01de4ac5f358e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 24 May 2019 15:42:09 +0200 Subject: [PATCH 163/428] typings(Guild): remove voiceConnection, add voice, cleanup rest Fixes #3293 --- typings/index.d.ts | 48 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c98e7a16..ee03f4c3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -398,21 +398,27 @@ declare module 'discord.js' { public afkTimeout: number; public applicationID: Snowflake; public available: boolean; + public banner: string | null; public channels: GuildChannelStore; public readonly createdAt: Date; public readonly createdTimestamp: number; - public readonly defaultRole: Role | null; public defaultMessageNotifications: DefaultMessageNotifications | number; + public readonly defaultRole: Role | null; public deleted: boolean; + public description: string | null; + public embedChannel: GuildChannel | null; + public embedChannelID: Snowflake | null; public embedEnabled: boolean; public emojis: GuildEmojiStore; public explicitContentFilter: number; public features: GuildFeatures[]; - public icon: string; + public icon: string | null; public id: Snowflake; public readonly joinedAt: Date; public joinedTimestamp: number; public large: boolean; + public maximumMembers: number | null; + public maximumPresences: number | null; public readonly me: GuildMember | null; public memberCount: number; public members: GuildMemberStore; @@ -426,24 +432,18 @@ declare module 'discord.js' { public roles: RoleStore; public readonly shard: WebSocketShard; public shardID: number; - public splash: string; + public splash: string | null; public readonly systemChannel: TextChannel | null; - public systemChannelID: Snowflake; + public systemChannelID: Snowflake | null; + public vanityURLCode: string | null; public verificationLevel: number; - public maximumMembers: number; - public maximumPresences: number; - public vanityURLCode: string; - public description: string; - public banner: string; - public widgetEnabled: boolean; - public widgetChannelID: Snowflake; - public readonly widgetChannel: TextChannel; - public embedChannelID: Snowflake; - public readonly embedChannel: TextChannel; public readonly verified: boolean; - public readonly voiceConnection: VoiceConnection | null; + public readonly voice: VoiceState | null; + public readonly widgetChannel: TextChannel | null; + public widgetChannelID: Snowflake | null; + public widgetEnabled: boolean | null; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; - public bannerURL(options?: AvatarOptions): string; + public bannerURL(options?: AvatarOptions): string | null; public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; @@ -451,29 +451,29 @@ declare module 'discord.js' { public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchBans(): Promise>; + public fetchEmbed(): Promise; public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; - public fetchEmbed(): Promise; - public iconURL(options?: AvatarOptions): string; + public iconURL(options?: AvatarOptions): string | null; public leave(): Promise; public member(user: UserResolvable): GuildMember | null; - public setAFKChannel(afkChannel: ChannelResolvable, reason?: string): Promise; + public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; public setAFKTimeout(afkTimeout: number, reason?: string): Promise; public setChannelPositions(channelPositions: ChannelPosition[]): Promise; public setDefaultMessageNotifications(defaultMessageNotifications: DefaultMessageNotifications | number, reason?: string): Promise; + public setEmbed(embed: GuildEmbedData, reason?: string): Promise; public setExplicitContentFilter(explicitContentFilter: number, reason?: string): Promise; - public setIcon(icon: Base64Resolvable, reason?: string): Promise; + public setIcon(icon: Base64Resolvable | null, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; public setRegion(region: string, reason?: string): Promise; - public setSplash(splash: Base64Resolvable, reason?: string): Promise; - public setSystemChannel(systemChannel: ChannelResolvable, reason?: string): Promise; + public setSplash(splash: Base64Resolvable | null, reason?: string): Promise; + public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; - public setEmbed(embed: GuildEmbedData, reason?: string): Promise; - public splashURL(options?: AvatarOptions): string; + public splashURL(options?: AvatarOptions): string | null; public toJSON(): object; public toString(): string; } From 34006cb51efd7ea43056c194a65b53e7a080039f Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Fri, 24 May 2019 13:33:40 -0500 Subject: [PATCH 164/428] docs(StreamDispatcher): specify pausedTime is in milliseconds (#3295) * Update TextChannel.js Update `setRateLimitPerUser` description to specify the `number` is in seconds, per the Discord docs * Update TextChannel.js Add unit to the rateLimitPerUser property * Update GuildChannel.js * Update StreamDispatcher.js Specify unit for `StreamDispatcher.pausedTime` --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 340e40c5..0a3f5220 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -151,7 +151,7 @@ class StreamDispatcher extends Writable { get paused() { return Boolean(this.pausedSince); } /** - * Total time that this dispatcher has been paused + * Total time that this dispatcher has been paused in milliseconds * @type {number} * @readonly */ From 3f1232ebf3ca9b3a08bee241fc4d2d90b8087d46 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sat, 25 May 2019 12:50:32 +0100 Subject: [PATCH 165/428] feat: throw custom error for uncached Guild#me (#3271) * handle cases where Guild#me is uncached * fix id prop * remove unnecessary checks * space's requested changes --- src/errors/Messages.js | 1 + src/structures/GuildAuditLogs.js | 23 ++++++++++++----------- src/structures/GuildEmoji.js | 8 ++++++-- src/structures/GuildMember.js | 1 + src/structures/Invite.js | 1 + 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 1b2bdcdb..ae42f73c 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -79,6 +79,7 @@ const Messages = { GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', GUILD_OWNED: 'Guild is owned by the client.', GUILD_MEMBERS_TIMEOUT: 'Members didn\'t arrive in time.', + GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.', INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index b466a3aa..92421919 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -349,19 +349,20 @@ class GuildAuditLogsEntry { guild_id: guild.id, })); } else if (targetType === Targets.INVITE) { - if (guild.me.permissions.has('MANAGE_GUILD')) { - const change = this.changes.find(c => c.key === 'code'); - this.target = guild.fetchInvites() - .then(invites => { + this.target = guild.members.fetch(guild.client.user.id).then(me => { + if (me.permissions.has('MANAGE_GUILD')) { + const change = this.changes.find(c => c.key === 'code'); + return guild.fetchInvites().then(invites => { this.target = invites.find(i => i.code === (change.new || change.old)); - return this.target; }); - } else { - this.target = this.changes.reduce((o, c) => { - o[c.key] = c.new || c.old; - return o; - }, {}); - } + } else { + this.target = this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, {}); + return this.target; + } + }); } else if (targetType === Targets.MESSAGE) { this.target = guild.client.users.get(data.target_id); } else { diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 30c67263..f117a90b 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -60,6 +60,7 @@ class GuildEmoji extends Emoji { * @readonly */ get deletable() { + if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); return !this.managed && this.guild.me.hasPermission(Permissions.FLAGS.MANAGE_EMOJIS); } @@ -80,8 +81,11 @@ class GuildEmoji extends Emoji { fetchAuthor() { if (this.managed) { return Promise.reject(new Error('EMOJI_MANAGED')); - } else if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) { - return Promise.reject(new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild)); + } else { + if (!this.guild.me) return Promise.reject(new Error('GUILD_UNCACHED_ME')); + if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) { + return Promise.reject(new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild)); + } } return this.client.api.guilds(this.guild.id).emojis(this.id).get() .then(emoji => this.client.users.add(emoji.user)); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index bc2d01e6..6e575d62 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -201,6 +201,7 @@ class GuildMember extends Base { get manageable() { if (this.user.id === this.guild.ownerID) return false; if (this.user.id === this.client.user.id) return false; + if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); return this.guild.me.roles.highest.comparePositionTo(this.roles.highest) > 0; } diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 43d8c652..b73076a6 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -100,6 +100,7 @@ class Invite extends Base { get deletable() { const guild = this.guild; if (!guild || !this.client.guilds.has(guild.id)) return false; + if (!guild.me) throw new Error('GUILD_UNCACHED_ME'); return this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) || guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD); } From 9ca36b8eea5906fbfd583810230cd220369b762c Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Sat, 25 May 2019 07:18:44 -0700 Subject: [PATCH 166/428] typings(VoiceState): add connection getter (#3292) * fix: add connection to voice state typings * Update typings/index.d.ts Co-Authored-By: SpaceEEC --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index ee03f4c3..4cd971ea 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1271,6 +1271,7 @@ declare module 'discord.js' { constructor(guild: Guild, data: object); public readonly channel: VoiceChannel | null; public channelID?: Snowflake; + public readonly connection: VoiceConnection | null; public readonly deaf?: boolean; public guild: Guild; public id: Snowflake; From 1ce670daa96f56ac6225e890b38be0470bebe0c1 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 26 May 2019 10:33:58 +0100 Subject: [PATCH 167/428] Create FUNDING.yml Just trialling it out --- .github/FUNDING.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..a0273f11 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,11 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL + +github: amishshah +patreon: discordjs From 949488bbbdfe0f6a433e33712ea1cadb4aeab018 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 27 May 2019 14:04:13 +0100 Subject: [PATCH 168/428] Fix #3218 --- src/client/voice/VoiceConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index cd2d29a7..b7de316b 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -433,7 +433,7 @@ class VoiceConnection extends EventEmitter { * @private */ onReady(data) { - this.authentication = data; + Object.assign(this.authentication, data); for (let mode of data.modes) { if (SUPPORTED_MODES.includes(mode)) { this.authentication.mode = mode; From db56e0cbae67f221db9265f6889ae2b4f5c987d4 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 27 May 2019 18:09:54 +0100 Subject: [PATCH 169/428] fix: delete VoiceStates even for uncached members --- src/client/actions/GuildMemberRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 108a35da..5a9700c3 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -11,8 +11,8 @@ class GuildMemberRemoveAction extends Action { if (guild) { member = this.getMember(data, guild); guild.memberCount--; + guild.voiceStates.delete(data.user.id); if (member) { - guild.voiceStates.delete(member.id); member.deleted = true; guild.members.remove(member.id); /** From 065908956bae6c937de367cddf6ba3a5a092facb Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 27 May 2019 12:13:25 -0500 Subject: [PATCH 170/428] fix websocket unpacking (#3301) --- src/WebSocket.js | 13 ++++++++++++- src/client/websocket/WebSocketShard.js | 7 +------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/WebSocket.js b/src/WebSocket.js index f3f893b4..6e6b4e7e 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -6,9 +6,13 @@ try { if (!erlpack.pack) erlpack = null; } catch (err) {} // eslint-disable-line no-empty +let TextDecoder; + if (browser) { + TextDecoder = window.TextDecoder; // eslint-disable-line no-undef exports.WebSocket = window.WebSocket; // eslint-disable-line no-undef } else { + TextDecoder = require('util').TextDecoder; try { exports.WebSocket = require('@discordjs/uws'); } catch (err) { @@ -16,12 +20,19 @@ if (browser) { } } +const ab = new TextDecoder(); + exports.encoding = erlpack ? 'etf' : 'json'; exports.pack = erlpack ? erlpack.pack : JSON.stringify; exports.unpack = data => { - if (!erlpack || data[0] === '{') return JSON.parse(data); + if (exports.encoding === 'json') { + if (typeof data !== 'string') { + data = ab.decode(data); + } + return JSON.parse(data); + } if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data)); return erlpack.unpack(data); }; diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 1ad7065d..3266c578 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -3,16 +3,12 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); -const { TextDecoder } = require('util'); let zstd; -let decoder; - let zlib; try { zstd = require('zucc'); - decoder = new TextDecoder('utf8'); } catch (e) { try { zlib = require('zlib-sync'); @@ -259,8 +255,7 @@ class WebSocketShard extends EventEmitter { onMessage({ data }) { let raw; if (zstd) { - const ab = this.inflate.decompress(new Uint8Array(data).buffer); - raw = decoder.decode(ab); + raw = this.inflate.decompress(new Uint8Array(data).buffer); } else { if (data instanceof ArrayBuffer) data = new Uint8Array(data); const l = data.length; From d34b62414bca3a73cd5a3df9d2d5b2fcb81557b5 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Mon, 27 May 2019 12:56:40 -0700 Subject: [PATCH 171/428] fix: StreamOptions#volume typings (#3303) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4cd971ea..e3ead1e7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2183,7 +2183,7 @@ declare module 'discord.js' { interface StreamOptions { type?: StreamType; seek?: number; - volume?: number; + volume?: number | boolean; passes?: number; plp?: number; fec?: boolean; From b5aff6d12006697d77dd10cd49cfca591065d9b3 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 28 May 2019 10:00:57 +0100 Subject: [PATCH 172/428] remove member voice state after emitting leave event --- src/client/actions/GuildMemberRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 5a9700c3..28aa5038 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -11,7 +11,6 @@ class GuildMemberRemoveAction extends Action { if (guild) { member = this.getMember(data, guild); guild.memberCount--; - guild.voiceStates.delete(data.user.id); if (member) { member.deleted = true; guild.members.remove(member.id); @@ -22,6 +21,7 @@ class GuildMemberRemoveAction extends Action { */ if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } + guild.voiceStates.delete(data.user.id); } return { guild, member }; } From 8652e47c14eccd1c8cab27f0d1acb8d7335349e8 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 28 May 2019 14:51:41 +0100 Subject: [PATCH 173/428] fix: decode voice ws data as json --- src/WebSocket.js | 4 ++-- src/client/voice/networking/VoiceWebSocket.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WebSocket.js b/src/WebSocket.js index 6e6b4e7e..8c61dfb4 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -26,8 +26,8 @@ exports.encoding = erlpack ? 'etf' : 'json'; exports.pack = erlpack ? erlpack.pack : JSON.stringify; -exports.unpack = data => { - if (exports.encoding === 'json') { +exports.unpack = (data, type) => { + if (exports.encoding === 'json' || type === 'json') { if (typeof data !== 'string') { data = ab.decode(data); } diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 8f1e8340..6ddabe62 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -136,7 +136,7 @@ class VoiceWebSocket extends EventEmitter { */ onMessage(event) { try { - return this.onPacket(WebSocket.unpack(event.data)); + return this.onPacket(WebSocket.unpack(event.data, 'json')); } catch (error) { return this.onError(error); } From 5154850a54ad4a4d253c14d1ae34db94c68d28fe Mon Sep 17 00:00:00 2001 From: bdistin Date: Thu, 30 May 2019 02:26:49 -0500 Subject: [PATCH 174/428] Add Stream permission (#3309) * Add Stream permission * update docs, and DEFAULT Created a new guild to test DEFAULT * update typings --- src/util/Permissions.js | 5 +++-- typings/index.d.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 9f01df39..ef5273ff 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -41,6 +41,7 @@ class Permissions extends BitField { * * `ADD_REACTIONS` (add new reactions to messages) * * `VIEW_AUDIT_LOG` * * `PRIORITY_SPEAKER` + * * `STREAM` * * `VIEW_CHANNEL` * * `SEND_MESSAGES` * * `SEND_TTS_MESSAGES` @@ -74,7 +75,7 @@ Permissions.FLAGS = { ADD_REACTIONS: 1 << 6, VIEW_AUDIT_LOG: 1 << 7, PRIORITY_SPEAKER: 1 << 8, - + STREAM: 1 << 9, VIEW_CHANNEL: 1 << 10, SEND_MESSAGES: 1 << 11, SEND_TTS_MESSAGES: 1 << 12, @@ -109,6 +110,6 @@ Permissions.ALL = Object.values(Permissions.FLAGS).reduce((all, p) => all | p, 0 * Bitfield representing the default permissions for users * @type {number} */ -Permissions.DEFAULT = 104324097; +Permissions.DEFAULT = 104324673; module.exports = Permissions; diff --git a/typings/index.d.ts b/typings/index.d.ts index e3ead1e7..9fb53f40 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2068,6 +2068,7 @@ declare module 'discord.js' { | 'ADD_REACTIONS' | 'VIEW_AUDIT_LOG' | 'PRIORITY_SPEAKER' + | 'STREAM' | 'VIEW_CHANNEL' | 'SEND_MESSAGES' | 'SEND_TTS_MESSAGES' From 5aa9425040e3057f878f74d892ed9e354543d88f Mon Sep 17 00:00:00 2001 From: DeJay Date: Thu, 30 May 2019 13:57:34 -0500 Subject: [PATCH 175/428] Removes the trace packet (#3312) * Removes the trace packet * Update src/client/websocket/WebSocketShard.js Co-Authored-By: Amish Shah * Update src/client/websocket/WebSocketShard.js Co-Authored-By: Amish Shah --- src/client/websocket/WebSocketShard.js | 13 ++----------- typings/index.d.ts | 1 - 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 3266c578..13ffa9f6 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -84,13 +84,6 @@ class WebSocketShard extends EventEmitter { */ this.lastHeartbeatAcked = true; - /** - * List of servers the shard is connected to - * @type {string[]} - * @private - */ - this.trace = []; - /** * Contains the rate limit queue and metadata * @type {Object} @@ -368,9 +361,8 @@ class WebSocketShard extends EventEmitter { this.emit(ShardEvents.READY); this.sessionID = packet.d.session_id; - this.trace = packet.d._trace; this.status = Status.READY; - this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}.`); + this.debug(`READY | Session ${this.sessionID}.`); this.lastHeartbeatAcked = true; this.sendHeartbeat(); break; @@ -381,10 +373,9 @@ class WebSocketShard extends EventEmitter { */ this.emit(ShardEvents.RESUMED); - this.trace = packet.d._trace; this.status = Status.READY; const replayed = packet.s - this.closeSequence; - this.debug(`RESUMED ${this.trace.join(' -> ')} | Session ${this.sessionID} | Replayed ${replayed} events.`); + this.debug(`RESUMED | Session ${this.sessionID} | Replayed ${replayed} events.`); this.lastHeartbeatAcked = true; this.sendHeartbeat(); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 9fb53f40..5e077f2b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1356,7 +1356,6 @@ declare module 'discord.js' { private sessionID?: string; private lastPingTimestamp: number; private lastHeartbeatAcked: boolean; - private trace: string[]; private ratelimit: { queue: object[]; total: number; remaining: number; time: 60e3; timer: NodeJS.Timeout | null; }; private connection: WebSocket | null; private helloTimeout: NodeJS.Timeout | null; From 405bdb5b558eff478d0bde739cee1ee8e40c330b Mon Sep 17 00:00:00 2001 From: "Deivu (Saya)" <36309350+Deivu@users.noreply.github.com> Date: Fri, 31 May 2019 15:06:28 +0800 Subject: [PATCH 176/428] cleanup(Constants): remove duplicate VOICE_STATE_UPDATE (#3313) --- src/util/Constants.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 0d8f3b6e..31f7a890 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -323,7 +323,6 @@ exports.PartialTypes = keyMirror([ * * USER_UPDATE * * USER_SETTINGS_UPDATE * * PRESENCE_UPDATE - * * VOICE_STATE_UPDATE * * TYPING_START * * VOICE_STATE_UPDATE * * VOICE_SERVER_UPDATE @@ -360,7 +359,6 @@ exports.WSEvents = keyMirror([ 'MESSAGE_REACTION_REMOVE_ALL', 'USER_UPDATE', 'PRESENCE_UPDATE', - 'VOICE_STATE_UPDATE', 'TYPING_START', 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE', From c87758086b81fda632a2f678ed46c4ab2ae8673f Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Wed, 5 Jun 2019 20:34:33 +0100 Subject: [PATCH 177/428] feat: add support for premium guilds (#3316) * add premiumTier and premiumSubscriptionCount * add premiumSinceTimestamp and premiumSince * add premium message types * typings * add GuildEmoji#available * fix doc description --- src/structures/Guild.js | 24 ++++++++++++++++++++++++ src/structures/GuildEmoji.js | 7 +++++++ src/structures/GuildMember.js | 17 ++++++++++++++++- src/util/Constants.js | 8 ++++++++ typings/index.d.ts | 13 ++++++++++++- 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index b1361aef..71eb494e 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -188,6 +188,30 @@ class Guild extends Base { */ this.embedEnabled = data.embed_enabled; + /** + * The type of premium tier: + * * 0: NONE + * * 1: TIER_1 + * * 2: TIER_2 + * * 3: TIER_3 + * @typedef {number} PremiumTier + */ + + /** + * The premium tier on this guild + * @type {PremiumTier} + */ + this.premiumTier = data.premium_tier; + + /** + * The total number of users currently boosting this server + * @type {?number} + * @name Guild#premiumSubscriptionCount + */ + if (typeof data.premium_subscription_count !== 'undefined') { + this.premiumSubscriptionCount = data.premium_subscription_count; + } + /** * Whether widget images are enabled on this guild * @type {?boolean} diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index f117a90b..79fff52f 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -45,6 +45,13 @@ class GuildEmoji extends Emoji { */ if (typeof data.managed !== 'undefined') this.managed = data.managed; + /** + * Whether this emoji is available + * @type {boolean} + * @name GuildEmoji#available + */ + if (typeof data.available !== 'undefined') this.available = data.available; + if (data.roles) this._roles = data.roles; } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 6e575d62..dcd5a93d 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -39,7 +39,6 @@ class GuildMember extends Base { /** * The timestamp the member joined the guild at * @type {?number} - * @name GuildMember#joinedTimestamp */ this.joinedTimestamp = null; @@ -55,6 +54,12 @@ class GuildMember extends Base { */ this.lastMessageChannelID = null; + /** + * The timestamp of when the member used their Nitro boost on the guild, if it was used + * @type {?number} + */ + this.premiumSinceTimestamp = null; + /** * Whether the member has been removed from the guild * @type {boolean} @@ -74,6 +79,7 @@ class GuildMember extends Base { if (typeof data.nick !== 'undefined') this.nickname = data.nick; if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); + if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime(); if (data.user) this.user = this.guild.client.users.add(data.user); if (data.roles) this._roles = data.roles; @@ -131,6 +137,15 @@ class GuildMember extends Base { return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null; } + /** + * The time of when the member used their Nitro boost on the guild, if it was used + * @type {?Date} + * @readonly + */ + get premiumSince() { + return this.premiumSinceTimestamp ? new Date(this.premiumSinceTimestamp) : null; + } + /** * The presence of this guild member * @type {Presence} diff --git a/src/util/Constants.js b/src/util/Constants.js index 31f7a890..a036fb4f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -375,6 +375,10 @@ exports.WSEvents = keyMirror([ * * CHANNEL_ICON_CHANGE * * PINS_ADD * * GUILD_MEMBER_JOIN + * * USER_PREMIUM_GUILD_SUBSCRIPTION + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -386,6 +390,10 @@ exports.MessageTypes = [ 'CHANNEL_ICON_CHANGE', 'PINS_ADD', 'GUILD_MEMBER_JOIN', + 'USER_PREMIUM_GUILD_SUBSCRIPTION', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', ]; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 5e077f2b..c56bfb0e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -427,6 +427,8 @@ declare module 'discord.js' { public readonly nameAcronym: string; public readonly owner: GuildMember | null; public ownerID: Snowflake; + public premiumSubscriptionCount: number | null; + public premiumTier: PremiumTier; public presences: PresenceStore; public region: string; public roles: RoleStore; @@ -551,6 +553,7 @@ declare module 'discord.js' { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; + public available: boolean; public deleted: boolean; public guild: Guild; public managed: boolean; @@ -579,6 +582,8 @@ declare module 'discord.js' { public nickname: string; public readonly partial: boolean; public readonly permissions: Readonly; + public readonly premiumSince: Date | null; + public premiumSinceTimestamp: number | null; public readonly presence: Presence; public roles: GuildMemberRoleStore; public user: User; @@ -2039,7 +2044,11 @@ declare module 'discord.js' { | 'CHANNEL_NAME_CHANGE' | 'CHANNEL_ICON_CHANGE' | 'PINS_ADD' - | 'GUILD_MEMBER_JOIN'; + | 'GUILD_MEMBER_JOIN' + | 'USER_PREMIUM_GUILD_SUBSCRIPTION' + | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1' + | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2' + | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3'; interface OverwriteData { allow?: PermissionResolvable; @@ -2099,6 +2108,8 @@ declare module 'discord.js' { id: UserResolvable | RoleResolvable; } + type PremiumTier = number; + interface PresenceData { status?: PresenceStatusData; afk?: boolean; From ddcc6cfec9ccb80d42e1d074bf03db2e0dae274e Mon Sep 17 00:00:00 2001 From: Saya <36309350+Deivu@users.noreply.github.com> Date: Thu, 6 Jun 2019 03:37:10 +0800 Subject: [PATCH 178/428] docs(Constants): add missing GUILD_EMOJIS_UPDATE to WSEvents (#3325) --- src/util/Constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index a036fb4f..dc5ddb45 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -309,6 +309,7 @@ exports.PartialTypes = keyMirror([ * * GUILD_ROLE_UPDATE * * GUILD_BAN_ADD * * GUILD_BAN_REMOVE + * * GUILD_EMOJIS_UPDATE * * CHANNEL_CREATE * * CHANNEL_DELETE * * CHANNEL_UPDATE From e87e4a6f0e23aad52744c0254ba224c6518d85a6 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Wed, 5 Jun 2019 15:46:11 -0400 Subject: [PATCH 179/428] typings(GuildChannelStore): add CategoryChannel as possible return value (#3326) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c56bfb0e..7288c015 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1440,7 +1440,7 @@ declare module 'discord.js' { export class GuildChannelStore extends DataStore { constructor(guild: Guild, iterable?: Iterable); - public create(name: string, options?: GuildCreateChannelOptions): Promise; + public create(name: string, options?: GuildCreateChannelOptions): Promise; } // Hacky workaround because changing the signature of an overriden method errors From 8bc8ffe168f9bd37a9fb372bd2b47d8be15d9f40 Mon Sep 17 00:00:00 2001 From: brian <29586909+brxxn@users.noreply.github.com> Date: Wed, 5 Jun 2019 15:57:31 -0400 Subject: [PATCH 180/428] feat(Guild): add setRolePositions method(#3317) Allows for role positions to be batch-updated similar to how channel positions are. It uses the Discord API endpoint PATCH /guild/:id/roles --- src/structures/Guild.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 71eb494e..fc000487 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -991,6 +991,40 @@ class Guild extends Base { ); } + /** + * The data needed for updating a guild role's position + * @typedef {Object} GuildRolePosition + * @property {GuildRoleResolveable} role The ID of the role + * @property {number} position The position to update + */ + + /** + * Batch-updates the guild's role positions + * @param {GuildRolePosition[]} rolePositions Role positions to update + * @returns {Promise} + * @example + * guild.setRolePositions([{ role: roleID, position: updatedRoleIndex }]) + * .then(guild => console.log(`Role permissions updated for ${guild}`)) + * .catch(console.error); + */ + setRolePositions(rolePositions) { + // Make sure rolePositions are prepared for API + rolePositions = rolePositions.map(o => ({ + id: o.role, + position: o.position, + })); + + // Call the API to update role positions + return this.client.api.guilds(this.id).roles.patch({ + data: rolePositions, + }).then(() => + this.client.actions.GuildRolePositionUpdate.handle({ + guild_id: this.id, + roles: rolePositions, + }).guild + ); + } + /** * Edits the guild's embed. * @param {GuildEmbedData} embed The embed for the guild From 8e1857286d5a9a2033b3ff7eb3bbb68500e46715 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 5 Jun 2019 22:18:01 +0200 Subject: [PATCH 181/428] typings(Guild): add typings for setRolePositions See: PR: #3317 Commit: 8bc8ffe168f9bd37a9fb372bd2b47d8be15d9f40 --- src/structures/Guild.js | 2 +- typings/index.d.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index fc000487..b2adfbc8 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -994,7 +994,7 @@ class Guild extends Base { /** * The data needed for updating a guild role's position * @typedef {Object} GuildRolePosition - * @property {GuildRoleResolveable} role The ID of the role + * @property {RoleResolveable} role The ID of the role * @property {number} position The position to update */ diff --git a/typings/index.d.ts b/typings/index.d.ts index 7288c015..7dfe5eed 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -472,6 +472,7 @@ declare module 'discord.js' { public setName(name: string, reason?: string): Promise; public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; public setRegion(region: string, reason?: string): Promise; + public setRolePositions(rolePositions: RolePosition[]): Promise; public setSplash(splash: Base64Resolvable | null, reason?: string): Promise; public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; @@ -2176,6 +2177,11 @@ declare module 'discord.js' { mentionable?: boolean; } + interface RolePosition { + role: RoleResolvable; + position: number; + } + type RoleResolvable = Role | string; type ShardingManagerMode = 'process' | 'worker'; From 4a2335c69c2e243f7f39db357acca393273d71ac Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 5 Jun 2019 22:18:06 +0200 Subject: [PATCH 182/428] docs(*Resolvable): make them appear on the docs I don't know what part of the docgen is not working properly, but this seems to fix those typedef to not appear on the documentation --- src/stores/ChannelStore.js | 38 ++++++++++++------------- src/stores/GuildChannelStore.js | 50 ++++++++++++++++----------------- src/stores/MessageStore.js | 49 ++++++++++++++++---------------- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 64c482cb..d0e1caa7 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -78,25 +78,6 @@ class ChannelStore extends DataStore { super.remove(id); } - /** - * Obtains a channel from Discord, or the channel cache if it's already available. - * @param {Snowflake} id ID of the channel - * @param {boolean} [cache=true] Whether to cache the new channel object if it isn't already - * @returns {Promise} - * @example - * // Fetch a channel by its id - * client.channels.fetch('222109930545610754') - * .then(channel => console.log(channel.name)) - * .catch(console.error); - */ - async fetch(id, cache = true) { - const existing = this.get(id); - if (existing && !existing.partial) return existing; - - const data = await this.client.api.channels(id).get(); - return this.add(data, null, cache); - } - /** * Data that can be resolved to give a Channel object. This can be: * * A Channel object @@ -121,6 +102,25 @@ class ChannelStore extends DataStore { * @param {ChannelResolvable} channel The channel resolvable to resolve * @returns {?Snowflake} */ + + /** + * Obtains a channel from Discord, or the channel cache if it's already available. + * @param {Snowflake} id ID of the channel + * @param {boolean} [cache=true] Whether to cache the new channel object if it isn't already + * @returns {Promise} + * @example + * // Fetch a channel by its id + * client.channels.fetch('222109930545610754') + * .then(channel => console.log(channel.name)) + * .catch(console.error); + */ + async fetch(id, cache = true) { + const existing = this.get(id); + if (existing && !existing.partial) return existing; + + const data = await this.client.api.channels(id).get(); + return this.add(data, null, cache); + } } module.exports = ChannelStore; diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index fc150f17..55b9ebb0 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -22,6 +22,31 @@ class GuildChannelStore extends DataStore { return channel; } + /** + * Data that can be resolved to give a Guild Channel object. This can be: + * * A GuildChannel object + * * A Snowflake + * @typedef {GuildChannel|Snowflake} GuildChannelResolvable + */ + + /** + * Resolves a GuildChannelResolvable to a Channel object. + * @method resolve + * @memberof GuildChannelStore + * @instance + * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve + * @returns {?Channel} + */ + + /** + * Resolves a GuildChannelResolvable to a channel ID string. + * @method resolveID + * @memberof GuildChannelStore + * @instance + * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve + * @returns {?Snowflake} + */ + /** * Creates a new channel in the guild. * @param {string} name The name of the new channel @@ -90,31 +115,6 @@ class GuildChannelStore extends DataStore { }); return this.client.actions.ChannelCreate.handle(data).channel; } - - /** - * Data that can be resolved to give a Guild Channel object. This can be: - * * A GuildChannel object - * * A Snowflake - * @typedef {GuildChannel|Snowflake} GuildChannelResolvable - */ - - /** - * Resolves a GuildChannelResolvable to a Channel object. - * @method resolve - * @memberof GuildChannelStore - * @instance - * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve - * @returns {?Channel} - */ - - /** - * Resolves a GuildChannelResolvable to a channel ID string. - * @method resolveID - * @memberof GuildChannelStore - * @instance - * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve - * @returns {?Snowflake} - */ } module.exports = GuildChannelStore; diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index e179daa2..59b224f7 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -82,31 +82,6 @@ class MessageStore extends DataStore { }); } - /** - * Deletes a message, even if it's not cached. - * @param {MessageResolvable} message The message to delete - * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user - */ - async remove(message, reason) { - message = this.resolveID(message); - if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); - } - - async _fetchId(messageID, cache) { - const existing = this.get(messageID); - if (existing && !existing.partial) return existing; - const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); - return this.add(data, cache); - } - - async _fetchMany(options = {}, cache) { - const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); - const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message, cache)); - return messages; - } - - /** * Data that can be resolved to a Message object. This can be: * * A Message @@ -131,6 +106,30 @@ class MessageStore extends DataStore { * @param {MessageResolvable} message The message resolvable to resolve * @returns {?Snowflake} */ + + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user + */ + async remove(message, reason) { + message = this.resolveID(message); + if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); + } + + async _fetchId(messageID, cache) { + const existing = this.get(messageID); + if (existing && !existing.partial) return existing; + const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); + return this.add(data, cache); + } + + async _fetchMany(options = {}, cache) { + const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this.add(message, cache)); + return messages; + } } module.exports = MessageStore; From 19ef45130b8d3a226500a0ccd69f7d90aaa02430 Mon Sep 17 00:00:00 2001 From: Alex <41408947+S0ftwareUpd8@users.noreply.github.com> Date: Sat, 8 Jun 2019 01:38:45 -0700 Subject: [PATCH 183/428] docs(Guild): add missing features (#3336) The addition of missing guild features that were added in the Nitro boost update, such as ANIMATED_ICON --- src/structures/Guild.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index b2adfbc8..820fac51 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -144,6 +144,12 @@ class Guild extends Base { /** * An array of enabled guild features, here are the possible values: + * * ANIMATED_ICON + * * COMMERCE + * * LURKABLE + * * PARTNERED + * * NEWS + * * BANNER * * INVITE_SPLASH * * MORE_EMOJI * * VERIFIED From 6cd4c27faed8b3f1b9da41a87b7922cd286313b9 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 8 Jun 2019 16:18:50 +0200 Subject: [PATCH 184/428] docs(Client): fetchVoiceRegions returns a promise --- src/client/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index 257a5cd8..85a93bb8 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -256,7 +256,7 @@ class Client extends BaseClient { /** * Obtains the available voice regions from Discord. - * @returns {Collection} + * @returns {Promise>} * @example * client.fetchVoiceRegions() * .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`)) From f82f0af9286b4ebf1350c725ba4251fef89b8b83 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 13 Jun 2019 05:54:12 +1000 Subject: [PATCH 185/428] docs(Presence): document client property (#3342) --- src/structures/Presence.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 22c70e6b..8c7d22b0 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -37,6 +37,12 @@ class Presence { * @param {Object} [data={}] The data for the presence */ constructor(client, data = {}) { + /** + * The client that instantiated this + * @name Presence#client + * @type {Client} + * @readonly + */ Object.defineProperty(this, 'client', { value: client }); /** * The user ID of this presence From 1bec28bd8105aa09f43492491be9dd9cc7f7d701 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 13 Jun 2019 14:14:47 +0200 Subject: [PATCH 186/428] feat(Guild): default iconURL to gif if animated (#3338) * feat(Guild): default iconURL to gif if animated * Icon, not Banner * fix url --- src/util/Constants.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index dc5ddb45..993563c0 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -133,8 +133,10 @@ exports.Endpoints = { }, Banner: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }), - Icon: (guildID, hash, format = 'webp', size) => - makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }), + Icon: (guildID, hash, format = 'default', size) => { + if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; + return makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }); + }, AppIcon: (clientID, hash, { format = 'webp', size } = {}) => makeImageUrl(`${root}/app-icons/${clientID}/${hash}`, { size, format }), AppAsset: (clientID, hash, { format = 'webp', size } = {}) => From 6100aceef24abfff0d32fcf5a6ff79c2102d046f Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Wed, 19 Jun 2019 11:49:33 -0500 Subject: [PATCH 187/428] docs(RoleStore): update -> create in create method (#3349) Under `create`, change `update` -> `create` in the description --- src/stores/RoleStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 6f97d277..db3e2c0b 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -76,7 +76,7 @@ class RoleStore extends DataStore { * Creates a new role in the guild with given information. * The position will silently reset to 1 if an invalid one is provided, or none. * @param {Object} [options] Options - * @param {RoleData} [options.data] The data to update the role with + * @param {RoleData} [options.data] The data to create the role with * @param {string} [options.reason] Reason for creating this role * @returns {Promise} * @example From a22aabf6a827f8643cf84939e9065b6c37b289e0 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 25 Jun 2019 13:31:48 -0500 Subject: [PATCH 188/428] feature: teams support (#3350) * basic teams support * export Team & TeamMember * use typedef * typings and some fixes * Update src/structures/TeamMember.js Co-Authored-By: Vlad Frangu * fix Team#iconURL() * fix typings and a bug * fix states start at 1 * team icon hash can be null * fix owner typings --- src/index.js | 2 + src/structures/ClientApplication.js | 5 +- src/structures/Team.js | 109 ++++++++++++++++++++++++++++ src/structures/TeamMember.js | 62 ++++++++++++++++ src/util/Constants.js | 15 ++++ typings/index.d.ts | 33 ++++++++- 6 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/structures/Team.js create mode 100644 src/structures/TeamMember.js diff --git a/src/index.js b/src/index.js index b3a92f60..916056fe 100644 --- a/src/index.js +++ b/src/index.js @@ -85,6 +85,8 @@ module.exports = { ReactionEmoji: require('./structures/ReactionEmoji'), RichPresenceAssets: require('./structures/Presence').RichPresenceAssets, Role: require('./structures/Role'), + Team: require('./structures/Team'), + TeamMember: require('./structures/TeamMember'), TextChannel: require('./structures/TextChannel'), User: require('./structures/User'), VoiceChannel: require('./structures/VoiceChannel'), diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index d2bcd17a..ac712bb6 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -3,6 +3,7 @@ const Snowflake = require('../util/Snowflake'); const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants'); const Base = require('./Base'); +const Team = require('./Team'); const AssetTypes = Object.keys(ClientApplicationAssetTypes); @@ -67,9 +68,9 @@ class ClientApplication extends Base { /** * The owner of this OAuth application - * @type {?User} + * @type {User|Team} */ - this.owner = data.owner ? this.client.users.add(data.owner) : null; + this.owner = data.team ? new Team(this.client, data.team) : this.client.users.add(data.owner); } /** diff --git a/src/structures/Team.js b/src/structures/Team.js new file mode 100644 index 00000000..0832a31f --- /dev/null +++ b/src/structures/Team.js @@ -0,0 +1,109 @@ +'use strict'; + +const Snowflake = require('../util/Snowflake'); +const Collection = require('../util/Collection'); +const Base = require('./Base'); +const TeamMember = require('./TeamMember'); + +/** + * Represents a Client OAuth2 Application Team. + * @extends {Base} + */ +class Team extends Base { + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(data) { + /** + * The ID of the Team + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The name of the Team + * @type {string} + */ + this.name = data.name; + + /** + * The Team's icon hash + * @type {?string} + */ + this.icon = data.icon || null; + + /** + * The Team's owner id + * @type {?string} + */ + this.ownerID = data.owner_user_id || null; + + /** + * The Team's members + * @type {Collection} + */ + this.members = new Collection(); + + for (const memberData of data.members) { + const member = new TeamMember(this.client, this, memberData); + this.members.set(member.id, member); + } + } + + /** + * The owner of this team + * @type {?TeamMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerID) || null; + } + + /** + * The timestamp the app was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the app was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the application's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} URL to the icon + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.TeamIcon(this.id, this.icon, { format, size }); + } + + /** + * When concatenated with a string, this automatically returns the Team's name instead of the + * Team object. + * @returns {string} + * @example + * // Logs: Team name: My Team + * console.log(`Team name: ${team}`); + */ + toString() { + return this.name; + } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } +} + +module.exports = Team; diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js new file mode 100644 index 00000000..f37fcbe0 --- /dev/null +++ b/src/structures/TeamMember.js @@ -0,0 +1,62 @@ +'use strict'; + +const Base = require('./Base'); +const { MembershipStates } = require('../util/Constants'); + +/** + * Represents a Client OAuth2 Application Team Member. + * @extends {Base} + */ +class TeamMember extends Base { + constructor(client, team, data) { + super(client); + + /** + * The Team this member is part of + * @type {Team} + */ + this.team = team; + + this._patch(data); + } + + _patch(data) { + /** + * The permissions this Team Member has with reguard to the team + * @type {string[]} + */ + this.permissions = data.permissions; + + /** + * The permissions this Team Member has with reguard to the team + * @type {MembershipStates} + */ + this.membershipState = MembershipStates[data.membership_state]; + + /** + * The user for this Team Member + * @type {User} + */ + this.user = this.client.users.add(data.user); + + /** + * The ID of the Team Member + * @type {Snowflake} + */ + this.id = this.user.id; + } + + /** + * When concatenated with a string, this automatically returns the team members's tag instead of the + * TeamMember object. + * @returns {string} + * @example + * // Logs: Team Member's tag: @Hydrabolt + * console.log(`Team Member's tag: ${teamMember}`); + */ + toString() { + return this.user.toString(); + } +} + +module.exports = TeamMember; diff --git a/src/util/Constants.js b/src/util/Constants.js index 993563c0..0587163c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -145,6 +145,8 @@ exports.Endpoints = { makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }), Splash: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/splashes/${guildID}/${hash}`, { size, format }), + TeamIcon: (teamID, hash, { format = 'webp', size } = {}) => + makeImageUrl(`${root}/team-icons/${teamID}/${hash}`, { size, format }), }; }, invite: (root, code) => `${root}/${code}`, @@ -569,6 +571,19 @@ exports.DefaultMessageNotifications = [ 'MENTIONS', ]; +/** + * The value set for a team members's membership state: + * * INVITED + * * ACCEPTED + * @typedef {string} MembershipStates + */ +exports.MembershipStates = [ + // They start at 1 + null, + 'INVITED', + 'ACCEPTED', +]; + function keyMirror(arr) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; diff --git a/typings/index.d.ts b/typings/index.d.ts index 7dfe5eed..558e12eb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -255,7 +255,7 @@ declare module 'discord.js' { public icon: string; public id: Snowflake; public name: string; - public owner: User | null; + public owner: User | Team; public rpcOrigins: string[]; public coverImage(options?: AvatarOptions): string; public fetchAssets(): Promise; @@ -264,6 +264,34 @@ declare module 'discord.js' { public toString(): string; } + export class Team extends Base { + constructor(client: Client, data: object); + public id: Snowflake; + public name: string; + public icon: string | null; + public ownerID: Snowflake | null; + public members: Collection; + + public readonly owner: TeamMember; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + + public iconURL(options?: AvatarOptions): string; + public toJSON(): object; + public toString(): string; + } + + export class TeamMember extends Base { + constructor(client: Client, team: Team, data: object); + public team: Team; + public id: Snowflake; + public permissions: string[]; + public membershipState: MembershipStates; + public user: User; + + public toString(): string; + } + export interface ActivityOptions { name?: string; url?: string; @@ -1992,6 +2020,9 @@ declare module 'discord.js' { type InviteResolvable = string; + type MembershipStates = 'INVITED' + | 'ACCEPTED'; + interface MessageCollectorOptions extends CollectorOptions { max?: number; maxProcessed?: number; From d7b2146c81d401d0fdefd8ec040a54dfce6067e5 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 25 Jun 2019 20:40:15 +0200 Subject: [PATCH 189/428] refactor(TeamMember): make id a getter --- src/structures/TeamMember.js | 13 ++++++++----- typings/index.d.ts | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js index f37fcbe0..e5567143 100644 --- a/src/structures/TeamMember.js +++ b/src/structures/TeamMember.js @@ -38,12 +38,15 @@ class TeamMember extends Base { * @type {User} */ this.user = this.client.users.add(data.user); + } - /** - * The ID of the Team Member - * @type {Snowflake} - */ - this.id = this.user.id; + /** + * The ID of the Team Member + * @type {Snowflake} + * @readonly + */ + get id() { + return this.user.id; } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 558e12eb..8d4c0294 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -284,7 +284,7 @@ declare module 'discord.js' { export class TeamMember extends Base { constructor(client: Client, team: Team, data: object); public team: Team; - public id: Snowflake; + public readonly id: Snowflake; public permissions: string[]; public membershipState: MembershipStates; public user: User; From 61e2e3e1add12d4ad6db63d3199387e8fc026d3e Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 26 Jun 2019 12:36:12 +0300 Subject: [PATCH 190/428] docs: add zucc to docsite welcome page (#3355) --- docs/general/welcome.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 2bc0525c..da1e02e8 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -47,6 +47,7 @@ For production bots, using node-opus should be considered a necessity, especiall ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`) +- [zucc](https://www.npmjs.com/package/zucc) for significantly faster WebSocket data inflation (`npm install zucc`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) From 4c11347511e24021ec4765c44ffb3cdf53e53619 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 26 Jun 2019 11:42:03 +0200 Subject: [PATCH 191/428] docs(Team*): fix appliction -> team, tag -> mention --- src/structures/Team.js | 6 +++--- src/structures/TeamMember.js | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/structures/Team.js b/src/structures/Team.js index 0832a31f..cec984b6 100644 --- a/src/structures/Team.js +++ b/src/structures/Team.js @@ -62,7 +62,7 @@ class Team extends Base { } /** - * The timestamp the app was created at + * The timestamp the team was created at * @type {number} * @readonly */ @@ -71,7 +71,7 @@ class Team extends Base { } /** - * The time the app was created at + * The time the team was created at * @type {Date} * @readonly */ @@ -80,7 +80,7 @@ class Team extends Base { } /** - * A link to the application's icon. + * A link to the teams's icon. * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} URL to the icon */ diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js index e5567143..03bcf6ec 100644 --- a/src/structures/TeamMember.js +++ b/src/structures/TeamMember.js @@ -22,13 +22,13 @@ class TeamMember extends Base { _patch(data) { /** - * The permissions this Team Member has with reguard to the team + * The permissions this Team Member has with regard to the team * @type {string[]} */ this.permissions = data.permissions; /** - * The permissions this Team Member has with reguard to the team + * The permissions this Team Member has with regard to the team * @type {MembershipStates} */ this.membershipState = MembershipStates[data.membership_state]; @@ -50,12 +50,12 @@ class TeamMember extends Base { } /** - * When concatenated with a string, this automatically returns the team members's tag instead of the + * When concatenated with a string, this automatically returns the team members's mention instead of the * TeamMember object. * @returns {string} * @example - * // Logs: Team Member's tag: @Hydrabolt - * console.log(`Team Member's tag: ${teamMember}`); + * // Logs: Team Member's mention: <@123456789012345678> + * console.log(`Team Member's mention: ${teamMember}`); */ toString() { return this.user.toString(); From 1dd4c041e08068a8979a14cbdc5be95233634f2d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 26 Jun 2019 20:12:05 +0200 Subject: [PATCH 192/428] fix(ClientApplication): owner is still nullable Fixes #3358 --- src/structures/ClientApplication.js | 8 ++++++-- typings/index.d.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index ac712bb6..9288d900 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -68,9 +68,13 @@ class ClientApplication extends Base { /** * The owner of this OAuth application - * @type {User|Team} + * @type {?User|Team} */ - this.owner = data.team ? new Team(this.client, data.team) : this.client.users.add(data.owner); + this.owner = data.team ? + new Team(this.client, data.team) ? + data.owner : + this.client.users.add(data.owner) : + null; } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 8d4c0294..de325591 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -255,7 +255,7 @@ declare module 'discord.js' { public icon: string; public id: Snowflake; public name: string; - public owner: User | Team; + public owner: User | Team | null; public rpcOrigins: string[]; public coverImage(options?: AvatarOptions): string; public fetchAssets(): Promise; From b65a4f05da40a45e33f78fc43c9b8172a170a3df Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 26 Jun 2019 20:19:05 +0200 Subject: [PATCH 193/428] fix(ClientApplication): fix ternaries --- src/structures/ClientApplication.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 9288d900..395d1db5 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -71,10 +71,10 @@ class ClientApplication extends Base { * @type {?User|Team} */ this.owner = data.team ? - new Team(this.client, data.team) ? - data.owner : + new Team(this.client, data.team) : + data.owner ? this.client.users.add(data.owner) : - null; + null; } /** From 20d7b3de5905bb0317483436df979f4250c67c1f Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Thu, 4 Jul 2019 10:20:28 -0400 Subject: [PATCH 194/428] docs/typings(VoiceStateStore): document and type the class (#3294) * Update index.d.ts * Update Guild.js * Update Guild.js * docs/typings(VoiceStateStore): document and add typings --- src/stores/VoiceStateStore.js | 4 ++++ src/structures/Guild.js | 4 ++++ typings/index.d.ts | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js index ece3c2bb..a5eaac2d 100644 --- a/src/stores/VoiceStateStore.js +++ b/src/stores/VoiceStateStore.js @@ -3,6 +3,10 @@ const DataStore = require('./DataStore'); const VoiceState = require('../structures/VoiceState'); +/** + * Stores voice states. + * @extends {DataStore} + */ class VoiceStateStore extends DataStore { constructor(guild, iterable) { super(guild.client, iterable, VoiceState); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 820fac51..30f3175f 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -57,6 +57,10 @@ class Guild extends Base { */ this.presences = new PresenceStore(this.client); + /** + * A collection of voice states in this guild + * @type {VoiceStateStore} + */ this.voiceStates = new VoiceStateStore(this); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index de325591..df4831d0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -469,6 +469,7 @@ declare module 'discord.js' { public verificationLevel: number; public readonly verified: boolean; public readonly voice: VoiceState | null; + public readonly voiceStates: VoiceStateStore; public readonly widgetChannel: TextChannel | null; public widgetChannelID: Snowflake | null; public widgetEnabled: boolean | null; @@ -1542,6 +1543,10 @@ declare module 'discord.js' { public fetch(id: Snowflake, cache?: boolean): Promise; } + export class VoiceStateStore extends DataStore { + constructor(guild: Guild, iterable?: Iterable); + } + //#endregion //#region Mixins From 4069d009d1a99393ec53a0e8bbcbbefa2c7765d2 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 11 Jul 2019 13:59:19 +0300 Subject: [PATCH 195/428] misc: automatically add labels to issues that use a template (#3367) * misc: Automatically add labels to issues that use the template * misc: Ditto * misc: DITTO --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 1 + .github/ISSUE_TEMPLATE/question---general-support-request.md | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d78410cc..2cfa2298 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,7 @@ --- name: Bug report about: Report incorrect or unexpected behaviour of discord.js +labels: s: unverified, type: bug --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ee9d888a..45ef822b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,7 @@ --- name: Feature request about: Request a feature for the core discord.js library +labels: type: enhancement --- diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md index 8ec717c9..aa3153ea 100644 --- a/.github/ISSUE_TEMPLATE/question---general-support-request.md +++ b/.github/ISSUE_TEMPLATE/question---general-support-request.md @@ -1,6 +1,7 @@ --- name: Question / General support request about: Ask for help in Discord instead - https://discord.gg/bRCvFy9 +labels: question (please use Discord instead) --- From adb082305d41194f0392353f1da29d43f2bc5617 Mon Sep 17 00:00:00 2001 From: Jisagi Date: Thu, 11 Jul 2019 13:09:43 +0200 Subject: [PATCH 196/428] feat(Guild): add banner to edit method and add setBanner method (#3364) * add setBanner method to Guild * typos fixed & typings added * more typings * docs(Guild): add banner to GuildEditData --- src/structures/Guild.js | 16 ++++++++++++++++ typings/index.d.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 30f3175f..90728501 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -770,6 +770,7 @@ class Guild extends Base { * @property {Base64Resolvable} [icon] The icon of the guild * @property {GuildMemberResolvable} [owner] The owner of the guild * @property {Base64Resolvable} [splash] The splash screen of the guild + * @property {Base64Resolvable} [banner] The banner of the guild * @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications */ @@ -802,6 +803,7 @@ class Guild extends Base { if (typeof data.icon !== 'undefined') _data.icon = data.icon; if (data.owner) _data.owner_id = this.client.users.resolve(data.owner).id; if (data.splash) _data.splash = data.splash; + if (data.banner) _data.banner = data.banner; if (typeof data.explicitContentFilter !== 'undefined') { _data.explicit_content_filter = Number(data.explicitContentFilter); } @@ -971,6 +973,20 @@ class Guild extends Base { return this.edit({ splash: await DataResolver.resolveImage(splash), reason }); } + /** + * Sets a new guild banner. + * @param {Base64Resolvable|BufferResolvable} banner The new banner of the guild + * @param {string} [reason] Reason for changing the guild's banner + * @returns {Promise} + * @example + * guild.setBanner('./banner.png') + * .then(updated => console.log('Updated the guild banner')) + * .catch(console.error); + */ + async setBanner(banner, reason) { + return this.edit({ banner: await DataResolver.resolveImage(banner), reason }); + } + /** * The data needed for updating a channel's position. * @typedef {Object} ChannelPosition diff --git a/typings/index.d.ts b/typings/index.d.ts index df4831d0..42e8d2dc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -493,6 +493,7 @@ declare module 'discord.js' { public member(user: UserResolvable): GuildMember | null; public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; public setAFKTimeout(afkTimeout: number, reason?: string): Promise; + public setBanner(banner: Base64Resolvable | null, reason?: string): Promise; public setChannelPositions(channelPositions: ChannelPosition[]): Promise; public setDefaultMessageNotifications(defaultMessageNotifications: DefaultMessageNotifications | number, reason?: string): Promise; public setEmbed(embed: GuildEmbedData, reason?: string): Promise; @@ -1947,6 +1948,7 @@ declare module 'discord.js' { icon?: Base64Resolvable; owner?: GuildMemberResolvable; splash?: Base64Resolvable; + banner?: Base64Resolvable; } interface GuildEmbedData { From f1433a2d97fb037660a33dc577d005d7f9676dda Mon Sep 17 00:00:00 2001 From: Eduardo Londero Date: Thu, 11 Jul 2019 16:40:12 -0300 Subject: [PATCH 197/428] feat(Collector): add idle time for a Collector to stop itself (#2942) * Implement idle feature * Add typings * Minimal fixes * Make everything in Collector and not attached to ReactionCollector * set this._idletimeout to null when collector ends * also set this._timeout to null when collector ends --- src/structures/interfaces/Collector.js | 23 ++++++++++++++++++++++- typings/index.d.ts | 2 ++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 8e6d8463..cadb44c6 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -16,6 +16,7 @@ const EventEmitter = require('events'); * Options to be applied to the collector. * @typedef {Object} CollectorOptions * @property {number} [time] How long to run the collector for in milliseconds + * @property {number} [idle] How long to stop the collector after inactivity in milliseconds * @property {boolean} [dispose=false] Whether to dispose data when it's deleted */ @@ -66,10 +67,18 @@ class Collector extends EventEmitter { */ this._timeout = null; + /** + * Timeout for cleanup due to inactivity + * @type {?Timeout} + * @private + */ + this._idletimeout = null; + this.handleCollect = this.handleCollect.bind(this); this.handleDispose = this.handleDispose.bind(this); if (options.time) this._timeout = this.client.setTimeout(() => this.stop('time'), options.time); + if (options.idle) this._idletimeout = this.client.setTimeout(() => this.stop('idle'), options.idle); } /** @@ -89,6 +98,11 @@ class Collector extends EventEmitter { * @param {...*} args The arguments emitted by the listener */ this.emit('collect', ...args); + + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = this.client.setTimeout(() => this.stop('idle'), this.options.idle); + } } this.checkEnd(); } @@ -155,7 +169,14 @@ class Collector extends EventEmitter { stop(reason = 'user') { if (this.ended) return; - if (this._timeout) this.client.clearTimeout(this._timeout); + if (this._timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = null; + } + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = null; + } this.ended = true; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 42e8d2dc..b97c62a8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -350,6 +350,7 @@ declare module 'discord.js' { export abstract class Collector extends EventEmitter { constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); private _timeout: NodeJS.Timer | null; + private _idletimeout: NodeJS.Timer | null; public readonly client: Client; public collected: Collection; @@ -1759,6 +1760,7 @@ declare module 'discord.js' { interface CollectorOptions { time?: number; + idle?: number; dispose?: boolean; } From 00c4098bb315a92322d708fe4cf71d070508ab69 Mon Sep 17 00:00:00 2001 From: bdistin Date: Thu, 11 Jul 2019 15:08:40 -0500 Subject: [PATCH 198/428] refactor(Util.escapeMarkdown): allow separate escaping and add tests (#3241) * wip refactor * add escapeMarkdown tests * italics can be done with a single underscore too * more refined * fix test name * unnecessary eslint ignores * use jest * make eslint less annoying in this test file * more testing * fix lib usage * more tests and a small fix --- package.json | 1 + src/structures/APIMessage.js | 2 +- src/util/Util.js | 151 ++++++++++++++++++++++++++-- test/escapeMarkdown.test.js | 184 +++++++++++++++++++++++++++++++++++ typings/index.d.ts | 10 +- 5 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 test/escapeMarkdown.test.js diff --git a/package.json b/package.json index 717fe065..6c454dec 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@types/ws": "^6.0.1", "discord.js-docgen": "discordjs/docgen", "eslint": "^5.13.0", + "jest": "^24.7.1", "json-filter-loader": "^1.0.0", "terser-webpack-plugin": "^1.2.2", "tslint": "^5.12.1", diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index a925892d..6edd6578 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -93,7 +93,7 @@ class APIMessage { if (content || mentionPart) { if (isCode) { const codeName = typeof this.options.code === 'string' ? this.options.code : ''; - content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content || '', true)}\n\`\`\``; + content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content || '')}\n\`\`\``; if (isSplit) { splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; diff --git a/src/util/Util.js b/src/util/Util.js index 181d5589..2f12d587 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -75,14 +75,144 @@ class Util { /** * Escapes any Discord-flavour markdown in a string. * @param {string} text Content to escape - * @param {boolean} [onlyCodeBlock=false] Whether to only escape codeblocks (takes priority) - * @param {boolean} [onlyInlineCode=false] Whether to only escape inline code + * @param {Object} [options={}] What types of markdown to escape + * @param {boolean} [options.codeBlock=true] Whether to escape code blocks or not + * @param {boolean} [options.inlineCode=true] Whether to escape inline code or not + * @param {boolean} [options.bold=true] Whether to escape bolds or not + * @param {boolean} [options.italic=true] Whether to escape italics or not + * @param {boolean} [options.underline=true] Whether to escape underlines or not + * @param {boolean} [options.strikethrough=true] Whether to escape strikethroughs or not + * @param {boolean} [options.spoiler=true] Whether to escape spoilers or not + * @param {boolean} [options.codeBlockContent=true] Whether to escape text inside code blocks or not + * @param {boolean} [options.inlineCodeContent=true] Whether to escape text inside inline code or not * @returns {string} */ - static 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'); + static escapeMarkdown(text, { + codeBlock = true, + inlineCode = true, + bold = true, + italic = true, + underline = true, + strikethrough = true, + spoiler = true, + codeBlockContent = true, + inlineCodeContent = true, + } = {}) { + if (!codeBlockContent) { + return text.split('```').map((subString, index, array) => { + if ((index % 2) && index !== array.length - 1) return subString; + return Util.escapeMarkdown(subString, { + inlineCode, + bold, + italic, + underline, + strikethrough, + spoiler, + inlineCodeContent, + }); + }).join(codeBlock ? '\\`\\`\\`' : '```'); + } + if (!inlineCodeContent) { + return text.split(/(?<=^|[^`])`(?=[^`]|$)/g).map((subString, index, array) => { + if ((index % 2) && index !== array.length - 1) return subString; + return Util.escapeMarkdown(subString, { + codeBlock, + bold, + italic, + underline, + strikethrough, + spoiler, + }); + }).join(inlineCode ? '\\`' : '`'); + } + if (inlineCode) text = Util.escapeInlineCode(text); + if (codeBlock) text = Util.escapeCodeBlock(text); + if (italic) text = Util.escapeItalic(text); + if (bold) text = Util.escapeBold(text); + if (underline) text = Util.escapeUnderline(text); + if (strikethrough) text = Util.escapeStrikethrough(text); + if (spoiler) text = Util.escapeSpoiler(text); + return text; + } + + /** + * Escapes code block markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeCodeBlock(text) { + return text.replace(/```/g, '\\`\\`\\`'); + } + + /** + * Escapes inline code markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeInlineCode(text) { + return text.replace(/(?<=^|[^`])`(?=[^`]|$)/g, '\\`'); + } + + /** + * Escapes italic markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeItalic(text) { + let i = 0; + text = text.replace(/(?<=^|[^*])\*([^*]|\*\*|$)/g, (_, match) => { + if (match === '**') return ++i % 2 ? `\\*${match}` : `${match}\\*`; + return `\\*${match}`; + }); + i = 0; + return text.replace(/(?<=^|[^_])_([^_]|__|$)/g, (_, match) => { + if (match === '__') return ++i % 2 ? `\\_${match}` : `${match}\\_`; + return `\\_${match}`; + }); + } + + /** + * Escapes bold markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeBold(text) { + let i = 0; + return text.replace(/\*\*(\*)?/g, (_, match) => { + if (match) return ++i % 2 ? `${match}\\*\\*` : `\\*\\*${match}`; + return '\\*\\*'; + }); + } + + /** + * Escapes underline markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeUnderline(text) { + let i = 0; + return text.replace(/__(_)?/g, (_, match) => { + if (match) return ++i % 2 ? `${match}\\_\\_` : `\\_\\_${match}`; + return '\\_\\_'; + }); + } + + /** + * Escapes strikethrough markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeStrikethrough(text) { + return text.replace(/~~/g, '\\~\\~'); + } + + /** + * Escapes spoiler markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeSpoiler(text) { + return text.replace(/\|\|/g, '\\|\\|'); } /** @@ -421,6 +551,15 @@ class Util { }); } + /** + * The content to put in a codeblock with all codeblock fences replaced by the equivalent backticks. + * @param {string} text The string to be converted + * @returns {string} + */ + static cleanCodeBlockContent(text) { + return text.replace('```', '`\u200b``'); + } + /** * Creates a Promise that resolves after a specified duration. * @param {number} ms How long to wait before resolving (in milliseconds) diff --git a/test/escapeMarkdown.test.js b/test/escapeMarkdown.test.js new file mode 100644 index 00000000..b791fdb9 --- /dev/null +++ b/test/escapeMarkdown.test.js @@ -0,0 +1,184 @@ +'use strict'; + +/* eslint-disable max-len, no-undef */ + +const Util = require('../src/util/Util'); +const testString = '`_Behold!_`\n||___~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~___||'; + +describe('escapeCodeblock', () => { + test('shared', () => { + expect(Util.escapeCodeBlock(testString)) + .toBe('`_Behold!_`\n||___~~***\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`***~~___||'); + }); + + test('basic', () => { + expect(Util.escapeCodeBlock('```test```')) + .toBe('\\`\\`\\`test\\`\\`\\`'); + }); +}); + + +describe('escapeInlineCode', () => { + test('shared', () => { + expect(Util.escapeInlineCode(testString)) + .toBe('\\`_Behold!_\\`\n||___~~***```js\n\\`use strict\\`;\nrequire(\'discord.js\');```***~~___||'); + }); + + test('basic', () => { + expect(Util.escapeInlineCode('`test`')) + .toBe('\\`test\\`'); + }); +}); + + +describe('escapeBold', () => { + test('shared', () => { + expect(Util.escapeBold(testString)) + .toBe('`_Behold!_`\n||___~~*\\*\\*```js\n`use strict`;\nrequire(\'discord.js\');```\\*\\**~~___||'); + }); + + test('basic', () => { + expect(Util.escapeBold('**test**')) + .toBe('\\*\\*test\\*\\*'); + }); +}); + + +describe('escapeItalic', () => { + test('shared', () => { + expect(Util.escapeItalic(testString)) + .toBe('`\\_Behold!\\_`\n||\\___~~\\***```js\n`use strict`;\nrequire(\'discord.js\');```**\\*~~__\\_||'); + }); + + test('basic (_)', () => { + expect(Util.escapeItalic('_test_')) + .toBe('\\_test\\_'); + }); + + test('basic (*)', () => { + expect(Util.escapeItalic('*test*')) + .toBe('\\*test\\*'); + }); +}); + + +describe('escapeUnderline', () => { + test('shared', () => { + expect(Util.escapeUnderline(testString)) + .toBe('`_Behold!_`\n||_\\_\\_~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~\\_\\__||'); + }); + + test('basic', () => { + expect(Util.escapeUnderline('__test__')) + .toBe('\\_\\_test\\_\\_'); + }); +}); + + +describe('escapeStrikethrough', () => { + test('shared', () => { + expect(Util.escapeStrikethrough(testString)) + .toBe('`_Behold!_`\n||___\\~\\~***```js\n`use strict`;\nrequire(\'discord.js\');```***\\~\\~___||'); + }); + + test('basic', () => { + expect(Util.escapeStrikethrough('~~test~~')) + .toBe('\\~\\~test\\~\\~'); + }); +}); + + +describe('escapeSpoiler', () => { + test('shared', () => { + expect(Util.escapeSpoiler(testString)) + .toBe('`_Behold!_`\n\\|\\|___~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~___\\|\\|'); + }); + + test('basic', () => { + expect(Util.escapeSpoiler('||test||')) + .toBe('\\|\\|test\\|\\|'); + }); +}); + + +describe('escapeMarkdown', () => { + test('shared', () => { + expect(Util.escapeMarkdown(testString)) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no codeBlock', () => { + expect(Util.escapeMarkdown(testString, { codeBlock: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n\\`use strict\\`;\nrequire(\'discord.js\');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no inlineCode', () => { + expect(Util.escapeMarkdown(testString, { inlineCode: false })) + .toBe('`\\_Behold!\\_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no bold', () => { + expect(Util.escapeMarkdown(testString, { bold: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\***\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`**\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no italic', () => { + expect(Util.escapeMarkdown(testString, { italic: false })) + .toBe('\\`_Behold!_\\`\n\\|\\|_\\_\\_\\~\\~*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\**\\~\\~\\_\\__\\|\\|'); + }); + + test('no underline', () => { + expect(Util.escapeMarkdown(testString, { underline: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\___\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~__\\_\\|\\|'); + }); + + test('no strikethrough', () => { + expect(Util.escapeMarkdown(testString, { strikethrough: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_~~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*~~\\_\\_\\_\\|\\|'); + }); + + test('no spoiler', () => { + expect(Util.escapeMarkdown(testString, { spoiler: false })) + .toBe('\\`\\_Behold!\\_\\`\n||\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_||'); + }); + + describe('code content', () => { + test('no code block content', () => { + expect(Util.escapeMarkdown(testString, { codeBlockContent: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no inline code content', () => { + expect(Util.escapeMarkdown(testString, { inlineCodeContent: false })) + .toBe('\\`_Behold!_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('neither inline code or code block content', () => { + expect(Util.escapeMarkdown(testString, { inlineCodeContent: false, codeBlockContent: false })) + // eslint-disable-next-line max-len + .toBe('\\`_Behold!_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('neither code blocks or code block content', () => { + expect(Util.escapeMarkdown(testString, { codeBlock: false, codeBlockContent: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n`use strict`;\nrequire(\'discord.js\');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('neither inline code or inline code content', () => { + expect(Util.escapeMarkdown(testString, { inlineCode: false, inlineCodeContent: false })) + .toBe('`_Behold!_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('edge-case odd number of fenses with no code block content', () => { + expect(Util.escapeMarkdown('**foo** ```**bar**``` **fizz** ``` **buzz**', { codeBlock: false, codeBlockContent: false })) + .toBe('\\*\\*foo\\*\\* ```**bar**``` \\*\\*fizz\\*\\* ``` \\*\\*buzz\\*\\*'); + }); + + test('edge-case odd number of backticks with no inline code content', () => { + expect(Util.escapeMarkdown('**foo** `**bar**` **fizz** ` **buzz**', { inlineCode: false, inlineCodeContent: false })) + .toBe('\\*\\*foo\\*\\* `**bar**` \\*\\*fizz\\*\\* ` \\*\\*buzz\\*\\*'); + }); + }); +}); + +/* eslint-enable max-len, no-undef */ diff --git a/typings/index.d.ts b/typings/index.d.ts index b97c62a8..168e8480 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1160,7 +1160,15 @@ declare module 'discord.js' { public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; public static discordSort(collection: Collection): Collection; - public static escapeMarkdown(text: string, onlyCodeBlock?: boolean, onlyInlineCode?: boolean): string; + public static escapeMarkdown(text: string, options?: { codeBlock?: boolean, inlineCode?: boolean, bold?: boolean, italic?: boolean, underline?: boolean, strikethrough?: boolean, spoiler?: boolean, inlineCodeContent?: boolean, codeBlockContent?: boolean }): string; + public static escapeCodeBlock(text: string): string; + public static escapeInlineCode(text: string): string; + public static escapeBold(text: string): string; + public static escapeItalic(text: string): string; + public static escapeUnderline(text: string): string; + public static escapeStrikethrough(text: string): string; + public static escapeSpoiler(text: string): string; + public static cleanCodeBlockContent(text: string): string; public static fetchRecommendedShards(token: string, guildsPerShard?: number): Promise; public static flatten(obj: object, ...props: { [key: string]: boolean | string }[]): object; public static idToBinary(num: Snowflake): string; From 547bf8310067503e76ec287a7928b0c44c78ed3e Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Fri, 12 Jul 2019 16:02:45 +0100 Subject: [PATCH 199/428] feat(typings): constants export (#2915) * Added typings for Constants export * Full typing of list Constants * Fix mistake in Package typing * Cleanup for requested changes moved fs import to import cluster WSEvents using WSEventType to build type * Satisfy tslint rules * Update Constants.js * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts --- src/util/Constants.js | 2 +- typings/index.d.ts | 258 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 0587163c..1286e18c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -126,7 +126,7 @@ exports.Endpoints = { return { Emoji: (emojiID, format = 'png') => `${root}/emojis/${emojiID}.${format}`, Asset: name => `${root}/assets/${name}`, - DefaultAvatar: number => `${root}/embed/avatars/${number}.png`, + DefaultAvatar: discriminator => `${root}/embed/avatars/${discriminator}.png`, Avatar: (userID, hash, format = 'default', size) => { if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 168e8480..7ad1cc06 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2,6 +2,7 @@ declare module 'discord.js' { import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; + import { PathLike } from 'fs'; import * as WebSocket from 'ws'; export const version: string; @@ -378,6 +379,258 @@ declare module 'discord.js' { public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; } + type AllowedImageFormat = 'webp' | 'png' | 'jpg' | 'gif'; + + export interface Constants { + Package: { + name: string; + version: string; + description: string; + author: string; + license: string; + main: PathLike; + types: PathLike; + homepage: string; + keywords: string[]; + bugs: { url: string }; + repository: { type: string, url: string }; + browser: { [key: string]: boolean }; + scripts: { [key: string]: string }; + engines: { [key: string]: string }; + dependencies: { [key: string]: string }; + peerDependencies: { [key: string]: string }; + devDependencies: { [key: string]: string }; + [key: string]: any; + }; + browser: boolean; + DefaultOptions: ClientOptions; + UserAgent: string | null; + Endpoints: { + botGateway: string; + invite: (root: string, code: string) => string; + CDN: (root: string) => { + Asset: (name: string) => string; + DefaultAvatar: (id: string | number) => string; + Emoji: (emojiID: string, format: 'png' | 'gif') => string; + Avatar: (userID: string | number, hash: string, format: 'default' | AllowedImageFormat, size: number) => string; + Banner: (guildID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + Icon: (userID: string | number, hash: string, format: 'default' | AllowedImageFormat, size: number) => string; + AppIcon: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + AppAsset: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + GDMIcon: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + Splash: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + TeamIcon: (teamID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + }; + }; + WSCodes: { + 1000: 'WS_CLOSE_REQUESTED'; + 4004: 'TOKEN_INVALID'; + 4010: 'SHARDING_INVALID'; + 4011: 'SHARDING_REQUIRED'; + }; + Events: { + RATE_LIMIT: 'rateLimit'; + CLIENT_READY: 'ready'; + RESUMED: 'resumed'; + GUILD_CREATE: 'guildCreate'; + GUILD_DELETE: 'guildDelete'; + GUILD_UPDATE: 'guildUpdate'; + GUILD_UNAVAILABLE: 'guildUnavailable'; + GUILD_MEMBER_ADD: 'guildMemberAdd'; + GUILD_MEMBER_REMOVE: 'guildMemberRemove'; + GUILD_MEMBER_UPDATE: 'guildMemberUpdate'; + GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable'; + GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking'; + GUILD_MEMBERS_CHUNK: 'guildMembersChunk'; + GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate'; + GUILD_ROLE_CREATE: 'roleCreate'; + GUILD_ROLE_DELETE: 'roleDelete'; + GUILD_ROLE_UPDATE: 'roleUpdate'; + GUILD_EMOJI_CREATE: 'emojiCreate'; + GUILD_EMOJI_DELETE: 'emojiDelete'; + GUILD_EMOJI_UPDATE: 'emojiUpdate'; + GUILD_BAN_ADD: 'guildBanAdd'; + GUILD_BAN_REMOVE: 'guildBanRemove'; + CHANNEL_CREATE: 'channelCreate'; + CHANNEL_DELETE: 'channelDelete'; + CHANNEL_UPDATE: 'channelUpdate'; + CHANNEL_PINS_UPDATE: 'channelPinsUpdate'; + MESSAGE_CREATE: 'message'; + MESSAGE_DELETE: 'messageDelete'; + MESSAGE_UPDATE: 'messageUpdate'; + MESSAGE_BULK_DELETE: 'messageDeleteBulk'; + MESSAGE_REACTION_ADD: 'messageReactionAdd'; + MESSAGE_REACTION_REMOVE: 'messageReactionRemove'; + MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll'; + USER_UPDATE: 'userUpdate'; + PRESENCE_UPDATE: 'presenceUpdate'; + VOICE_STATE_UPDATE: 'voiceStateUpdate'; + VOICE_BROADCAST_SUBSCRIBE: 'subscribe'; + VOICE_BROADCAST_UNSUBSCRIBE: 'unsubscribe'; + TYPING_START: 'typingStart'; + WEBHOOKS_UPDATE: 'webhookUpdate'; + DISCONNECT: 'disconnect'; + RECONNECTING: 'reconnecting'; + ERROR: 'error'; + WARN: 'warn'; + DEBUG: 'debug'; + SHARD_DISCONNECTED: 'shardDisconnected'; + SHARD_ERROR: 'shardError'; + SHARD_RECONNECTING: 'shardReconnecting'; + SHARD_READY: 'shardReady'; + SHARD_RESUMED: 'shardResumed'; + INVALIDATED: 'invalidated'; + RAW: 'raw'; + }; + ShardEvents: { + CLOSE: 'close'; + DESTROYED: 'destroyed'; + INVALID_SESSION: 'invalidSession'; + READY: 'ready'; + RESUMED: 'resumed'; + }; + PartialTypes: { + [K in PartialType]: K; + }; + WSEvents: { + [K in WSEventType]: K; + }; + Colors: { + DEFAULT: 0x000000; + WHITE: 0xFFFFFF; + AQUA: 0x1ABC9C; + GREEN: 0x2ECC71; + BLUE: 0x3498DB; + YELLOW: 0xFFFF00; + PURPLE: 0x9B59B6; + LUMINOUS_VIVID_PINK: 0xE91E63; + GOLD: 0xF1C40F; + ORANGE: 0xE67E22; + RED: 0xE74C3C; + GREY: 0x95A5A6; + NAVY: 0x34495E; + DARK_AQUA: 0x11806A; + DARK_GREEN: 0x1F8B4C; + DARK_BLUE: 0x206694; + DARK_PURPLE: 0x71368A; + DARK_VIVID_PINK: 0xAD1457; + DARK_GOLD: 0xC27C0E; + DARK_ORANGE: 0xA84300; + DARK_RED: 0x992D22; + DARK_GREY: 0x979C9F; + DARKER_GREY: 0x7F8C8D; + LIGHT_GREY: 0xBCC0C0; + DARK_NAVY: 0x2C3E50; + BLURPLE: 0x7289DA; + GREYPLE: 0x99AAB5; + DARK_BUT_NOT_BLACK: 0x2C2F33; + NOT_QUITE_BLACK: 0x23272A; + }; + Status: { + READY: 0; + CONNECTING: 1; + RECONNECTING: 2; + IDLE: 3; + NEARLY: 4; + DISCONNECTED: 5; + }; + OPCodes: { + DISPATCH: 0; + HEARTBEAT: 1; + IDENTIFY: 2; + STATUS_UPDATE: 3; + VOICE_STATE_UPDATE: 4; + VOICE_GUILD_PING: 5; + RESUME: 6; + RECONNECT: 7; + REQUEST_GUILD_MEMBERS: 8; + INVALID_SESSION: 9; + HELLO: 10; + HEARTBEAT_ACK: 11; + }; + APIErrors: { + UNKNOWN_ACCOUNT: 10001; + UNKNOWN_APPLICATION: 10002; + UNKNOWN_CHANNEL: 10003; + UNKNOWN_GUILD: 10004; + UNKNOWN_INTEGRATION: 10005; + UNKNOWN_INVITE: 10006; + UNKNOWN_MEMBER: 10007; + UNKNOWN_MESSAGE: 10008; + UNKNOWN_OVERWRITE: 10009; + UNKNOWN_PROVIDER: 10010; + UNKNOWN_ROLE: 10011; + UNKNOWN_TOKEN: 10012; + UNKNOWN_USER: 10013; + UNKNOWN_EMOJI: 10014; + UNKNOWN_WEBHOOK: 10015; + BOT_PROHIBITED_ENDPOINT: 20001; + BOT_ONLY_ENDPOINT: 20002; + MAXIMUM_GUILDS: 30001; + MAXIMUM_FRIENDS: 30002; + MAXIMUM_PINS: 30003; + MAXIMUM_ROLES: 30005; + MAXIMUM_REACTIONS: 30010; + UNAUTHORIZED: 40001; + MISSING_ACCESS: 50001; + INVALID_ACCOUNT_TYPE: 50002; + CANNOT_EXECUTE_ON_DM: 50003; + EMBED_DISABLED: 50004; + CANNOT_EDIT_MESSAGE_BY_OTHER: 50005; + CANNOT_SEND_EMPTY_MESSAGE: 50006; + CANNOT_MESSAGE_USER: 50007; + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008; + CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009; + OAUTH2_APPLICATION_BOT_ABSENT: 50010; + MAXIMUM_OAUTH2_APPLICATIONS: 50011; + INVALID_OAUTH_STATE: 50012; + MISSING_PERMISSIONS: 50013; + INVALID_AUTHENTICATION_TOKEN: 50014; + NOTE_TOO_LONG: 50015; + INVALID_BULK_DELETE_QUANTITY: 50016; + CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019; + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021; + BULK_DELETE_MESSAGE_TOO_OLD: 50034; + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036; + REACTION_BLOCKED: 90001; + }; + VoiceStatus: { + CONNECTED: 0; + CONNECTING: 1; + AUTHENTICATING: 2; + RECONNECTING: 3; + DISCONNECTED: 4; + }; + VoiceOPCodes: { + IDENTIFY: 0; + SELECT_PROTOCOL: 1; + READY: 2; + HEARTBEAT: 3; + SESSION_DESCRIPTION: 4; + SPEAKING: 5; + HELLO: 8; + CLIENT_CONNECT: 12; + CLIENT_DISCONNECT: 13; + }; + ChannelTypes: { + TEXT: 0; + DM: 1; + VOICE: 2; + GROUP: 3; + CATEGORY: 4; + NEWS: 5; + STORE: 6; + }; + ClientApplicationAssetTypes: { + SMALL: 1; + BIG: 2; + }; + MessageTypes: MessageType[]; + ActivityTypes: ActivityType[]; + DefaultMessageNotifications: DefaultMessageNotifications[]; + MembershipStates: 'INVITED' | 'ACCEPTED'; + } + export class DataResolver { public static resolveBase64(data: Base64Resolvable): string; public static resolveFile(resource: BufferResolvable | Stream): Promise; @@ -2290,6 +2543,11 @@ declare module 'discord.js' { compress?: boolean; } + type PartialType = 'USER' + | 'CHANNEL' + | 'GUILD_MEMBER' + | 'MESSAGE'; + type WSEventType = 'READY' | 'RESUMED' | 'GUILD_CREATE' From 651ff81bd522c03b5c9724a3a8c14eeb88dcd6b9 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 13 Jul 2019 18:02:07 +0200 Subject: [PATCH 200/428] fix: update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +++- .github/ISSUE_TEMPLATE/feature_request.md | 4 +++- .github/ISSUE_TEMPLATE/question---general-support-request.md | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2cfa2298..d5061af2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,9 @@ --- name: Bug report about: Report incorrect or unexpected behaviour of discord.js -labels: s: unverified, type: bug +title: '' +labels: 's: unverified, type: bug' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 45ef822b..4d021ad3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,9 @@ --- name: Feature request about: Request a feature for the core discord.js library -labels: type: enhancement +title: '' +labels: 'type: enhancement' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md index aa3153ea..d2cef51f 100644 --- a/.github/ISSUE_TEMPLATE/question---general-support-request.md +++ b/.github/ISSUE_TEMPLATE/question---general-support-request.md @@ -1,7 +1,9 @@ --- name: Question / General support request about: Ask for help in Discord instead - https://discord.gg/bRCvFy9 +title: '' labels: question (please use Discord instead) +assignees: '' --- From d8516efa36f62ef8a6e33b0b35ba5f482e22d103 Mon Sep 17 00:00:00 2001 From: Kitten King Date: Thu, 25 Jul 2019 20:48:23 +0530 Subject: [PATCH 201/428] docs: fix typos (#3404) --- src/client/websocket/WebSocketManager.js | 2 +- src/client/websocket/WebSocketShard.js | 2 +- src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 +- src/sharding/Shard.js | 2 +- src/sharding/ShardingManager.js | 2 +- travis/deploy.sh | 4 ++-- typings/index.d.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 6dec7c40..c2530ef3 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -297,7 +297,7 @@ class WebSocketManager extends EventEmitter { } catch (error) { this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); if (error.httpStatus !== 401) { - this.debug(`Possible network error occured. Retrying in 5s...`); + this.debug(`Possible network error occurred. Retrying in 5s...`); await Util.delayFor(5000); this.reconnecting = false; return this.reconnect(); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 13ffa9f6..9663052c 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -465,7 +465,7 @@ class WebSocketShard extends EventEmitter { */ sendHeartbeat() { if (!this.lastHeartbeatAcked) { - this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting."); + this.debug("Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting."); this.destroy(4009); return; } diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index 11154674..da73693c 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -14,7 +14,7 @@ module.exports = (client, { d: data }) => { * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, * not much information can be provided easily here - you need to manually check the pins yourself. * @event Client#channelPinsUpdate - * @param {DMChannel|TextChannel} channel The channel that the pins update occured in + * @param {DMChannel|TextChannel} channel The channel that the pins update occurred in * @param {Date} time The time of the pins update */ client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 4fab578d..6533b4f8 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -319,7 +319,7 @@ class Shard extends EventEmitter { } /** - * Emitted upon recieving a message from the child process/worker. + * Emitted upon receiving a message from the child process/worker. * @event Shard#message * @param {*} message Message that was received */ diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 15b74ffd..f0298054 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -73,7 +73,7 @@ class ShardingManager extends EventEmitter { if (this.shardList.length < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardList', 'at least 1 ID.'); if (this.shardList.some(shardID => typeof shardID !== 'number' || isNaN(shardID) || !Number.isInteger(shardID) || shardID < 0)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array of postive integers.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array of positive integers.'); } } diff --git a/travis/deploy.sh b/travis/deploy.sh index 39b57909..7043abf9 100644 --- a/travis/deploy.sh +++ b/travis/deploy.sh @@ -11,7 +11,7 @@ fi DONT_COMMIT=false if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - not commiting" + echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - not committing" SOURCE_TYPE="pr" DONT_COMMIT=true elif [ -n "$TRAVIS_TAG" ]; then @@ -29,7 +29,7 @@ npm run docs NODE_ENV=production npm run build:browser if [ $DONT_COMMIT == true ]; then - echo -e "\e[36m\e[1mNot commiting - exiting early" + echo -e "\e[36m\e[1mNot committing - exiting early" exit 0 fi diff --git a/typings/index.d.ts b/typings/index.d.ts index 7ad1cc06..76c8f4e8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1736,7 +1736,7 @@ declare module 'discord.js' { public create(name: string, options?: GuildCreateChannelOptions): Promise; } - // Hacky workaround because changing the signature of an overriden method errors + // Hacky workaround because changing the signature of an overridden method errors class OverridableDataStore, R = any> extends DataStore { public add(data: any, cache: any): any; public set(key: any): any; From e562564123912f09725a6e03ecf4ba44d85de35a Mon Sep 17 00:00:00 2001 From: Alex <41408947+S0ftwareUpd8@users.noreply.github.com> Date: Sun, 28 Jul 2019 06:24:27 -0700 Subject: [PATCH 202/428] docs(Guild): add missing features (#3406) * Update Guild.js * Update Guild.js * style(Guild): remove trailing space * typings(Guild): add new features --- src/structures/Guild.js | 2 ++ typings/index.d.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 90728501..21be50d1 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -159,6 +159,8 @@ class Guild extends Base { * * VERIFIED * * VIP_REGIONS * * VANITY_URL + * * DISCOVERABLE + * * FEATURABLE * @typedef {string} Features */ diff --git a/typings/index.d.ts b/typings/index.d.ts index 76c8f4e8..ad9b150e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2223,7 +2223,9 @@ declare module 'discord.js' { | 'MORE_EMOJI' | 'VERIFIED' | 'VIP_REGIONS' - | 'VANITY_URL'; + | 'VANITY_URL' + | 'DISCOVERABLE' + | 'FEATURABLE'; interface GuildMemberEditData { nick?: string; From 53722b47c1d646a23f4eb422c0256eaef699383b Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 29 Jul 2019 23:00:56 +0100 Subject: [PATCH 203/428] chore(license): update copyright notice (#3408) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 5f32c795..9a4257e6 100644 --- a/LICENSE +++ b/LICENSE @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - Copyright 2015 - 2018 Amish Shah + Copyright 2015 - 2019 Amish Shah Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From e645dd6358d1ad60f81a919ddfebf7db2092b1bc Mon Sep 17 00:00:00 2001 From: TNThacker2015 <37024464+TNThacker2015@users.noreply.github.com> Date: Mon, 29 Jul 2019 15:25:35 -0700 Subject: [PATCH 204/428] feat: Util.splitMessage always return an array (#3035) * Making Util.splitMessage always return an array Util.splitMessage sometimes returns an array, but other times it returns a string. This should make it so that it always returns an array. * jsdoc Co-Authored-By: TNThacker2015 <37024464+TNThacker2015@users.noreply.github.com> * docs(Util): remove superfluous space in docstring --- src/util/Util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 2f12d587..f9aaf21c 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -53,11 +53,11 @@ class Util { * Splits a string into multiple chunks at a designated character that do not exceed a specific length. * @param {StringResolvable} text Content to split * @param {SplitOptions} [options] Options controlling the behavior of the split - * @returns {string|string[]} + * @returns {string[]} */ static splitMessage(text, { maxLength = 2000, char = '\n', prepend = '', append = '' } = {}) { text = this.resolveString(text); - if (text.length <= maxLength) return text; + if (text.length <= maxLength) return [text]; const splitText = text.split(char); if (splitText.some(chunk => chunk.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN'); const messages = []; From 5af8cb8e6e7591e81f758a8b0f6748db7c2f12f1 Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 30 Jul 2019 00:25:45 +0200 Subject: [PATCH 205/428] feat: overload for split always returning an array (#3411) * feat: overload for split always returning an array * feat: update Util.splitMessage --- typings/index.d.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index ad9b150e..43a607ab 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1440,7 +1440,7 @@ declare module 'discord.js' { route: object, reason?: string ): Promise<{ id: Snowflake; position: number }[]>; - public static splitMessage(text: string, options?: SplitOptions): string | string[]; + public static splitMessage(text: string, options?: SplitOptions): string[]; public static str2ab(str: string): ArrayBuffer; } @@ -1828,8 +1828,10 @@ declare module 'discord.js' { readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date; - send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; } interface TextBasedChannelFields extends PartialTextBasedChannelFields { @@ -1850,8 +1852,10 @@ declare module 'discord.js' { token: string; delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; - send(content?: StringResolvable, options?: WebhookMessageOptions | MessageAdditions): Promise; - send(options?: WebhookMessageOptions | MessageAdditions | APIMessage): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions & { split?: false } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + send(options?: WebhookMessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + send(options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; sendSlackMessage(body: object): Promise; } From d14db521585f2b6b1a6c50d3a9acecc73bea25a3 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 4 Aug 2019 15:57:39 +0200 Subject: [PATCH 206/428] fix(typings): send overloads --- typings/index.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 43a607ab..09cc75ca 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1828,10 +1828,12 @@ declare module 'discord.js' { readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date; + send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; send(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; - send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; - send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; } interface TextBasedChannelFields extends PartialTextBasedChannelFields { From 9e76f233143f2f0fe0ff9a9e645f32dc8db44b26 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sat, 10 Aug 2019 13:22:11 +0300 Subject: [PATCH 207/428] fix(GuildMemberStore): reject BAN_RESOLVE_ID error instead of throwing it (#3425) --- src/stores/GuildMemberStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 9f9c4211..24946841 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -177,7 +177,7 @@ class GuildMemberStore extends DataStore { */ unban(user, reason) { const id = this.client.users.resolveID(user); - if (!id) throw new Error('BAN_RESOLVE_ID'); + if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID')); return this.client.api.guilds(this.guild.id).bans[id].delete({ reason }) .then(() => this.client.users.resolve(user)); } From f79f0243438a89c4d8a910461800b1adff2fe6e5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 12 Aug 2019 21:40:26 +0100 Subject: [PATCH 208/428] fix: bots being unable to connect --- src/rest/RESTManager.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 306fa246..1e0b1ecf 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -27,9 +27,7 @@ class RESTManager { getAuth() { const token = this.client.token || this.client.accessToken; - const prefixed = !!this.client.application || this.client.user; - if (token && prefixed) return `${this.tokenPrefix} ${token}`; - else if (token) return token; + if (token) return `${this.tokenPrefix} ${token}`; throw new Error('TOKEN_MISSING'); } From 2c4d14a71be11c36dfa661fd4dc013a381ef3988 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 14 Aug 2019 19:22:41 +0100 Subject: [PATCH 209/428] voice: remove redundant debug info --- src/client/voice/VoiceConnection.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b7de316b..67e79a3b 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -188,13 +188,12 @@ class VoiceConnection extends EventEmitter { self_deaf: this.voice ? this.voice.selfDeaf : false, }, options); - const queueLength = this.channel.guild.shard.ratelimit.queue.length; - this.emit('debug', `Sending voice state update (queue length is ${queueLength}): ${JSON.stringify(options)}`); + this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`); return this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, - }); + }, true); } /** From c6e8fccbf07a2ba2ac80d7e13f967eaa07c881f8 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 17 Aug 2019 13:42:22 +0100 Subject: [PATCH 210/428] voice: fix #3418 (kicking bot from voice channel doesn't allow it to rejoin) --- src/client/actions/VoiceStateUpdate.js | 2 +- src/client/voice/ClientVoiceManager.js | 4 ++-- src/client/voice/VoiceConnection.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 045ca931..14e1aa3f 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -25,7 +25,7 @@ class VoiceStateUpdate extends Action { } // Emit event - if (member && member.user.id === client.user.id && data.channel_id) { + if (member && member.user.id === client.user.id) { client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); client.voice.onVoiceStateUpdate(data); } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 2ca2b465..78413693 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -1,7 +1,6 @@ 'use strict'; const Collection = require('../../util/Collection'); -const { VoiceStatus } = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); const VoiceBroadcast = require('./VoiceBroadcast'); const { Error } = require('../../errors'); @@ -52,8 +51,9 @@ class ClientVoiceManager { const connection = this.connections.get(guild_id); this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`); if (!connection) return; - if (!channel_id && connection.status !== VoiceStatus.DISCONNECTED) { + if (!channel_id) { connection._disconnect(); + this.connections.delete(guild_id); return; } connection.channel = this.client.channels.get(channel_id); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 67e79a3b..b56177c6 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -388,6 +388,7 @@ class VoiceConnection extends EventEmitter { ws.removeAllListeners('ready'); ws.removeAllListeners('sessionDescription'); ws.removeAllListeners('speaking'); + ws.shutdown(); } if (udp) udp.removeAllListeners('error'); From f55e4302c9bc47427d0d9fc618ff4bcad00da87c Mon Sep 17 00:00:00 2001 From: Robin Millette Date: Sat, 17 Aug 2019 11:50:49 -0400 Subject: [PATCH 211/428] github: fix duplicate key in FUNDING.yml (#3380) Comment out (keep for reference) unused platforms. Also fixes a duplicate `github` key. --- .github/FUNDING.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a0273f11..6aa35f01 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,11 +1,11 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -custom: # Replace with a single custom sponsorship URL +# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +# patreon: # Replace with a single Patreon username +# open_collective: # Replace with a single Open Collective username +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# custom: # Replace with a single custom sponsorship URL github: amishshah patreon: discordjs From 7fae6e5bca8651fc28face08386ba876ab663cf1 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyk0@users.noreply.github.com> Date: Sat, 17 Aug 2019 09:51:52 -0600 Subject: [PATCH 212/428] typings: switch overloads of RoleStore#fetch (#3397) because compu told me to --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 09cc75ca..51b0d993 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1797,8 +1797,8 @@ declare module 'discord.js' { public readonly highest: Role; public create(options?: { data?: RoleData, reason?: string }): Promise; - public fetch(id?: Snowflake, cache?: boolean): Promise; public fetch(id: Snowflake, cache?: boolean): Promise; + public fetch(id?: Snowflake, cache?: boolean): Promise; } export class UserStore extends DataStore { From 6d3c55b68cbdb258421e017577f52da0d8deaf85 Mon Sep 17 00:00:00 2001 From: bdistin Date: Sat, 17 Aug 2019 10:57:45 -0500 Subject: [PATCH 213/428] feat(Collector): allow collectors to be consumed by for-await-of loops (#3269) --- src/structures/interfaces/Collector.js | 31 ++++++++++++++++++++++++++ typings/index.d.ts | 1 + 2 files changed, 32 insertions(+) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index cadb44c6..3cd5d387 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -196,6 +196,37 @@ class Collector extends EventEmitter { if (reason) this.stop(reason); } + /** + * Allows collectors to be consumed with for-await-of loops + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of} + */ + async *[Symbol.asyncIterator]() { + const queue = []; + const onCollect = item => queue.push(item); + this.on('collect', onCollect); + + try { + while (queue.length || !this.ended) { + if (queue.length) { + yield queue.shift(); + } else { + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => { + const tick = () => { + this.off('collect', tick); + this.off('end', tick); + return resolve(); + }; + this.on('collect', tick); + this.on('end', tick); + }); + } + } + } finally { + this.off('collect', onCollect); + } + } + toJSON() { return Util.flatten(this); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 51b0d993..0d2422b6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -363,6 +363,7 @@ declare module 'discord.js' { public handleCollect(...args: any[]): void; public handleDispose(...args: any[]): void; public stop(reason?: string): void; + public [Symbol.asyncIterator](): AsyncIterableIterator; public toJSON(): object; protected listener: Function; From 2df4f227a446a48330e941762374a262e138d2fd Mon Sep 17 00:00:00 2001 From: didinele Date: Sat, 17 Aug 2019 19:02:17 +0300 Subject: [PATCH 214/428] refactor: move Guild#defaultRole to RoleStore#everyone (#3347) * remove guild#defaultRole * add RoleStore#defaultRole * typings * fix trailing space * another one * Rename it to everyone --- src/stores/RoleStore.js | 9 +++++++++ src/structures/Guild.js | 9 --------- typings/index.d.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index db3e2c0b..649048be 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -110,6 +110,15 @@ class RoleStore extends DataStore { }); } + /** + * The `@everyone` role of the guild + * @type {?Role} + * @readonly + */ + get everyone() { + return this.get(this.guild.id) || null; + } + /** * The role with the highest position in the store * @type {Role} diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 21be50d1..04aece2f 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -489,15 +489,6 @@ class Guild extends Base { return this.client.channels.get(this.embedChannelID) || null; } - /** - * The `@everyone` role of the guild - * @type {?Role} - * @readonly - */ - get defaultRole() { - return this.roles.get(this.id) || null; - } - /** * The client user as a GuildMember of this guild * @type {?GuildMember} diff --git a/typings/index.d.ts b/typings/index.d.ts index 0d2422b6..838eb2b3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -686,7 +686,6 @@ declare module 'discord.js' { public readonly createdAt: Date; public readonly createdTimestamp: number; public defaultMessageNotifications: DefaultMessageNotifications | number; - public readonly defaultRole: Role | null; public deleted: boolean; public description: string | null; public embedChannel: GuildChannel | null; @@ -1795,6 +1794,7 @@ declare module 'discord.js' { export class RoleStore extends DataStore { constructor(guild: Guild, iterable?: Iterable); + public readonly everyone: Role | null; public readonly highest: Role; public create(options?: { data?: RoleData, reason?: string }): Promise; From b662678f210ba893a2bb2c27ba781b2bf3ccfc16 Mon Sep 17 00:00:00 2001 From: BannerBomb Date: Sat, 17 Aug 2019 12:07:58 -0400 Subject: [PATCH 215/428] feat/fix(Util): fix animated part of parseEmoji regex and make id optional (#3407) * Small changes to parseEmoji regex I just made a small change to the parseEmoji regex, this change will make an invalid emoji, like `` return as null, before this change it would return as an animated emoji because the name started with an `a` which would result in false positives, then the `?` I added to the end of `(\d{17,19})?` is used if someone provided an emoji as `:name:` or `a:name:` it will return the correct values but have an invalid id. * Update Util.js 2nd Update: I changed the regex to output the results if you provide `` and <:aemoji:123456789012345678>` which will output `{ animated: false, name: "aemoji", id: "123456789012345678" }` or `<:emojiname:>` which outputs `{ animated: false, name: "emojiname", id: null }` or `` which would output `{ animated: true, name: "emoji", id: null }`. Before this PR the method would return that the emoji was animated if you provided something like `` because the name started with an `a`. --- src/util/Util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index f9aaf21c..8e6743d9 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -244,9 +244,9 @@ class Util { static parseEmoji(text) { if (text.includes('%')) text = decodeURIComponent(text); if (!text.includes(':')) return { animated: false, name: text, id: null }; - const m = text.match(/?/); + const m = text.match(/?/); if (!m) return null; - return { animated: Boolean(m[1]), name: m[2], id: m[3] }; + return { animated: Boolean(m[1]), name: m[2], id: m[3] || null }; } /** From ab27dd0218c82492d3f0f5665a8a169af4d4641a Mon Sep 17 00:00:00 2001 From: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Date: Sat, 17 Aug 2019 12:24:16 -0400 Subject: [PATCH 216/428] refactor(TeamMember): remove client from constructor (#3409) * Remove client from TeamMember constructor part 1 * Remove client from TeamMember constructor part 2 * update typings --- src/structures/Team.js | 2 +- src/structures/TeamMember.js | 4 ++-- typings/index.d.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structures/Team.js b/src/structures/Team.js index cec984b6..25b13fac 100644 --- a/src/structures/Team.js +++ b/src/structures/Team.js @@ -47,7 +47,7 @@ class Team extends Base { this.members = new Collection(); for (const memberData of data.members) { - const member = new TeamMember(this.client, this, memberData); + const member = new TeamMember(this, memberData); this.members.set(member.id, member); } } diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js index 03bcf6ec..ba7ecd26 100644 --- a/src/structures/TeamMember.js +++ b/src/structures/TeamMember.js @@ -8,8 +8,8 @@ const { MembershipStates } = require('../util/Constants'); * @extends {Base} */ class TeamMember extends Base { - constructor(client, team, data) { - super(client); + constructor(team, data) { + super(team.client); /** * The Team this member is part of diff --git a/typings/index.d.ts b/typings/index.d.ts index 838eb2b3..3cb0c622 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -283,7 +283,7 @@ declare module 'discord.js' { } export class TeamMember extends Base { - constructor(client: Client, team: Team, data: object); + constructor(team: Team, data: object); public team: Team; public readonly id: Snowflake; public permissions: string[]; From 8ae7a30d0b4df8b6833bc754aaadbea8d75130ef Mon Sep 17 00:00:00 2001 From: Khoo Hao Yit <40757009+KhooHaoYit@users.noreply.github.com> Date: Sun, 18 Aug 2019 01:33:03 +0800 Subject: [PATCH 217/428] fix(Message): delete method caused messageDelete event to fire twice (#3252) * ref: add getPayload and use for other get* methods * return existing data.* * use Action.getUser() * Fix messageDelete double emission --- src/structures/Message.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 8e5f423a..99c006b7 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -454,11 +454,7 @@ class Message extends Base { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.channel.messages.remove(this.id, reason).then(() => - this.client.actions.MessageDelete.handle({ - id: this.id, - channel_id: this.channel.id, - }).message); + return this.channel.messages.remove(this.id, reason).then(() => this); } else { return new Promise(resolve => { this.client.setTimeout(() => { From d62db232e792ef698f16d903e075d8977136624f Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sat, 17 Aug 2019 19:31:04 +0100 Subject: [PATCH 218/428] feat(Invite): add targetUser(Type) (#3262) * add Invite#targetUser(Type) * incase discord decides to add 0 --- src/structures/Invite.js | 18 ++++++++++++++++++ typings/index.d.ts | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index b73076a6..d5e9f895 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -70,6 +70,24 @@ class Invite extends Base { */ this.inviter = data.inviter ? this.client.users.add(data.inviter) : null; + /** + * The target user for this invite + * @type {?User} + */ + this.targetUser = data.target_user ? this.client.users.add(data.target_user) : null; + + /** + * The type of the target user: + * * 1: STREAM + * @typedef {number} TargetUser + */ + + /** + * The target user type + * @type {?TargetUser} + */ + this.targetUserType = typeof data.target_user_type === 'number' ? data.target_user_type : null; + /** * The channel the invite is for * @type {Channel} diff --git a/typings/index.d.ts b/typings/index.d.ts index 3cb0c622..877dfed3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -928,6 +928,8 @@ declare module 'discord.js' { public maxUses: number | null; public memberCount: number; public presenceCount: number; + public targetUser: User | null; + public targetUserType: TargetUser | null; public temporary: boolean | null; public readonly url: string; public uses: number | null; @@ -2524,6 +2526,8 @@ declare module 'discord.js' { type StringResolvable = string | string[] | any; + type TargetUser = number; + type UserResolvable = User | Snowflake | Message | GuildMember; type VoiceStatus = number; From 12b48b7cbb74b649d523fea40237b3cace66fff8 Mon Sep 17 00:00:00 2001 From: Koyamie Date: Sat, 17 Aug 2019 20:57:14 +0200 Subject: [PATCH 219/428] fix(GuildMemberRoleStore): correctly reference the everyone role (#3434) --- src/stores/GuildMemberRoleStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 824bca68..4c63bed8 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -23,7 +23,7 @@ class GuildMemberRoleStore extends Collection { * @readonly */ get _filtered() { - const everyone = this.guild.defaultRole; + const everyone = this.guild.roles.everyone; return this.guild.roles.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); } From 1851f747707d2863ab507a679b8345f6bcc05d0e Mon Sep 17 00:00:00 2001 From: Khoo Hao Yit <40757009+KhooHaoYit@users.noreply.github.com> Date: Sun, 18 Aug 2019 03:09:29 +0800 Subject: [PATCH 220/428] fix(ReactionUserStore): remove method firing messageReactionRemove event twice (#3277) --- src/stores/ReactionUserStore.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js index 976b7991..dc250a9f 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/stores/ReactionUserStore.js @@ -48,14 +48,7 @@ class ReactionUserStore extends DataStore { return message.client.api.channels[message.channel.id].messages[message.id] .reactions[this.reaction.emoji.identifier][userID === message.client.user.id ? '@me' : userID] .delete() - .then(() => - message.client.actions.MessageReactionRemove.handle({ - user_id: userID, - message_id: message.id, - emoji: this.reaction.emoji, - channel_id: message.channel.id, - }).reaction - ); + .then(() => this.reaction); } } From c786867bd65d8f6661e3de9c9528768d1ec03d46 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 18 Aug 2019 11:45:28 +0200 Subject: [PATCH 221/428] fix(Webhook): return raw data if the channel is unavailable Fixes #3424 --- src/structures/Webhook.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 6c1a3db5..2dc1a028 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -146,8 +146,9 @@ class Webhook { query: { wait: true }, auth: false, }).then(d => { - if (!this.client.channels) return d; - return this.client.channels.get(d.channel_id).messages.add(d, false); + const channel = this.client.channels ? this.client.channels.get(d.channel_id) : undefined; + if (!channel) return d; + return channel.messages.add(d, false); }); } @@ -173,9 +174,10 @@ class Webhook { query: { wait: true }, auth: false, data: body, - }).then(data => { - if (!this.client.channels) return data; - return this.client.channels.get(data.channel_id).messages.add(data, false); + }).then(d => { + const channel = this.client.channels ? this.client.channels.get(d.channel_id) : undefined; + if (!channel) return d; + return channel.messages.add(d, false); }); } From 3fcc862c5fa336bb26c570655245d57e2e532f20 Mon Sep 17 00:00:00 2001 From: neoney <30625554+n3oney@users.noreply.github.com> Date: Sun, 18 Aug 2019 11:54:30 +0200 Subject: [PATCH 222/428] docs: fix voice broadcast example code (#3436) client.createVoiceBroadcast() -> client.voice.createBroadcast() --- docs/topics/voice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index e48b8b32..6a39f5a3 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -114,7 +114,7 @@ Make sure to consult the documentation for a full list of what you can play - th A voice broadcast is very useful for "radio" bots, that play the same audio across multiple channels. It means audio is only transcoded once, and is much better on performance. ```js -const broadcast = client.createVoiceBroadcast(); +const broadcast = client.voice.createBroadcast(); broadcast.on('subscribe', dispatcher => { console.log('New broadcast subscriber!'); From e4309b23d5f46133c79e5ff3692a13bc8c61cbdb Mon Sep 17 00:00:00 2001 From: Saya <36309350+Deivu@users.noreply.github.com> Date: Tue, 20 Aug 2019 00:55:07 +0800 Subject: [PATCH 223/428] feat: abort Requests that takes a lot of time to resolve (#3327) * Add Request Timeout * Add abort controller in packages * Fix Lint Error. * Fix Lint Errors * Make Timeout Customizable & use finally * Fixed a minor issue * Fix eslint * Update request timeout to use d.js client timeout methods. --- package.json | 1 + src/client/Client.js | 3 +++ src/rest/APIRequest.js | 6 +++++- src/util/Constants.js | 2 ++ typings/index.d.ts | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c454dec..c234bfb7 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { + "abort-controller": "^3.0.0", "form-data": "^2.3.3", "node-fetch": "^2.3.0", "pako": "^1.0.8", diff --git a/src/client/Client.js b/src/client/Client.js index 85a93bb8..3be8e311 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -392,6 +392,9 @@ class Client extends BaseClient { if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number'); } + if (typeof options.restRequestTimeout !== 'number' || isNaN(options.restRequestTimeout)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restRequestTimeout', 'a number'); + } if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); } diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 672cccd4..520febca 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -4,6 +4,7 @@ const FormData = require('form-data'); const https = require('https'); const { browser, UserAgent } = require('../util/Constants'); const fetch = require('node-fetch'); +const AbortController = require('abort-controller'); if (https.Agent) var agent = new https.Agent({ keepAlive: true }); @@ -46,12 +47,15 @@ class APIRequest { headers['Content-Type'] = 'application/json'; } + const controller = new AbortController(); + const timeout = this.client.setTimeout(() => controller.abort(), this.client.options.restRequestTimeout); return fetch(url, { method: this.method, headers, agent, body, - }); + signal: controller.signal, + }).finally(() => this.client.clearTimeout(timeout)); } } diff --git a/src/util/Constants.js b/src/util/Constants.js index 1286e18c..50033919 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -28,6 +28,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * corresponding websocket events * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST * requests (higher values will reduce rate-limiting errors on bad connections) + * @property {number} [restRequestTimeout=15000] Time to wait before cancelling a REST request * @property {number} [restSweepInterval=60] How frequently to delete inactive request buckets, in seconds * (or 0 for never) * @property {number} [retryLimit=1] How many times to retry on 5XX errors (Infinity for indefinite amount of retries) @@ -50,6 +51,7 @@ exports.DefaultOptions = { partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], + restRequestTimeout: 15000, retryLimit: 1, restTimeOffset: 500, restSweepInterval: 60, diff --git a/typings/index.d.ts b/typings/index.d.ts index 877dfed3..0bc65d4e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2018,6 +2018,7 @@ declare module 'discord.js' { partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; + restRequestTimeout?: number; restSweepInterval?: number; retryLimit?: number; presence?: PresenceData; From fbd811517a298b857f9fd963d8fb13c4c1207f90 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 19 Aug 2019 23:02:33 +0300 Subject: [PATCH 224/428] src: Update Webhook#sendSlackMessage to be accurate with what the API returns (#3429) * src: Update sendSlackMessage * typings --- src/structures/Webhook.js | 8 ++------ typings/index.d.ts | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 2dc1a028..2ed6137b 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -155,7 +155,7 @@ class Webhook { /** * Sends a raw slack message with this webhook. * @param {Object} body The raw body to send - * @returns {Promise} + * @returns {Promise} * @example * // Send a slack message * webhook.sendSlackMessage({ @@ -174,11 +174,7 @@ class Webhook { query: { wait: true }, auth: false, data: body, - }).then(d => { - const channel = this.client.channels ? this.client.channels.get(d.channel_id) : undefined; - if (!channel) return d; - return channel.messages.add(d, false); - }); + }).then(data => data.toString() === 'ok'); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 0bc65d4e..72ee1e89 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1861,7 +1861,7 @@ declare module 'discord.js' { send(content?: StringResolvable, options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; send(options?: WebhookMessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; send(options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; - sendSlackMessage(body: object): Promise; + sendSlackMessage(body: object): Promise; } //#endregion From 5e4f9d436d33129ec7d2dfdf0c5d014d623c1e93 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 21 Aug 2019 14:52:35 +0300 Subject: [PATCH 225/428] src: alphabetize guild features and make sure they're up to date (#3441) --- src/structures/Guild.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 04aece2f..f24f08e6 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -149,18 +149,17 @@ class Guild extends Base { /** * An array of enabled guild features, here are the possible values: * * ANIMATED_ICON - * * COMMERCE - * * LURKABLE - * * PARTNERED - * * NEWS * * BANNER - * * INVITE_SPLASH - * * MORE_EMOJI - * * VERIFIED - * * VIP_REGIONS - * * VANITY_URL + * * COMMERCE * * DISCOVERABLE * * FEATURABLE + * * INVITE_SPLASH + * * LURKABLE + * * NEWS + * * PARTNERED + * * VANITY_URL + * * VERIFIED + * * VIP_REGIONS * @typedef {string} Features */ From cc488a8bd3dc39354967d132e34f63ff51e92b79 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyk0@users.noreply.github.com> Date: Wed, 21 Aug 2019 09:52:08 -0600 Subject: [PATCH 226/428] fix: GuildMemberStore#_fetchMany (#3420) * added DARK_MODE_INVISIBLE added another constant color that makes embeds appear invisible on DARK mode. * travis likes trailing commas * fix: ref issue: #3414 * fix: removed a random color --- src/stores/GuildMemberStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 24946841..e02aa3dc 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -192,8 +192,8 @@ class GuildMemberStore extends DataStore { _fetchMany({ query = '', limit = 0 } = {}) { return new Promise((resolve, reject) => { - if (this.guild.memberCount === this.size) { - resolve(query || limit ? new Collection() : this); + if (this.guild.memberCount === this.size && !query && !limit) { + resolve(this); return; } this.guild.shard.send({ From c715ed9f8b6a7487d335901cf4b06d276069962b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 22 Aug 2019 12:15:20 +0100 Subject: [PATCH 227/428] voice: remove passes (discord will begin dropping duplicated audio packets from tomorrow, you should not set passes > 1) --- docs/topics/voice.md | 5 +--- .../voice/dispatcher/StreamDispatcher.js | 23 ++++++++----------- src/client/voice/util/PlayInterface.js | 1 - typings/index.d.ts | 1 - 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index 6a39f5a3..22f5c93d 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -70,13 +70,10 @@ We can also pass in options when we first play the stream: ```js const dispatcher = connection.play('/home/discord/audio.mp3', { - volume: 0.5, - passes: 3 + volume: 0.5 }); ``` -These are just a subset of the options available (consult documentation for a full list). Most users may be interested in the `passes` option, however. As audio is sent over UDP, there is a chance packets may not arrive. Increasing the number of passes, e.g. to `3` gives you a better chance that your packets reach your recipients, at the cost of triple the bandwidth. We recommend not going over 5 passes. - ### What can I play? Discord.js allows you to play a lot of things: diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 0a3f5220..2923eb61 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -33,9 +33,9 @@ const nonce = Buffer.alloc(24); class StreamDispatcher extends Writable { constructor( player, - { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, + { seek = 0, volume = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, streams) { - const streamOptions = { seek, volume, passes, fec, plp, bitrate, highWaterMark }; + const streamOptions = { seek, volume, fec, plp, bitrate, highWaterMark }; super(streamOptions); /** * The Audio Player that controls this dispatcher @@ -289,24 +289,21 @@ class StreamDispatcher extends Writable { } _sendPacket(packet) { - let repeats = this.streamOptions.passes; /** * Emitted whenever the dispatcher has debug information. * @event StreamDispatcher#debug * @param {string} info The debug info */ this._setSpeaking(1); - while (repeats--) { - if (!this.player.voiceConnection.sockets.udp) { - this.emit('debug', 'Failed to send a packet - no UDP socket'); - return; - } - this.player.voiceConnection.sockets.udp.send(packet) - .catch(e => { - this._setSpeaking(0); - this.emit('debug', `Failed to send a packet - ${e}`); - }); + if (!this.player.voiceConnection.sockets.udp) { + this.emit('debug', 'Failed to send a packet - no UDP socket'); + return; } + this.player.voiceConnection.sockets.udp.send(packet) + .catch(e => { + this._setSpeaking(0); + this.emit('debug', `Failed to send a packet - ${e}`); + }); } _setSpeaking(value) { diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 873cd81e..6f1ba018 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -11,7 +11,6 @@ const { Error } = require('../../../errors'); * @property {number} [seek=0] The time to seek to, will be ignored when playing `ogg/opus` or `webm/opus` streams * @property {number|boolean} [volume=1] The volume to play at. Set this to false to disable volume transforms for * this stream to improve performance. - * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss * @property {number} [plp] Expected packet loss percentage * @property {boolean} [fec] Enabled forward error correction * @property {number|string} [bitrate=96] The bitrate (quality) of the audio in kbps. diff --git a/typings/index.d.ts b/typings/index.d.ts index 72ee1e89..c6a69c91 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2514,7 +2514,6 @@ declare module 'discord.js' { type?: StreamType; seek?: number; volume?: number | boolean; - passes?: number; plp?: number; fec?: boolean; bitrate?: number | 'auto'; From e8451561ed742a0b49775591c57d37a6e927287f Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 24 Aug 2019 19:32:18 +0200 Subject: [PATCH 228/428] ci: github actions (#3442) * feat: eslint action * refactor: actions v2 * fix: set +x for entrypoint * ci: integrate docs * fix: give it a nice name * ci: publish docs * ci: fix yaml key error * ci: fix executable * ci: use normal sh shebang * ci: move into the workspace * ci: fix eslint path * ci: manually run the build * ci: use different container * ci: install git * ci: assume yes flag * ci: use correct branch * ci: fix branch and source * ci: fix condition * ci: remove useless steps * ci: rename action * ci: make executable again * ci: cleanup * ci: add stable branch * ci: remove semi * ci: do some logging for failing action * ci: remove actions api * ci: re-add semi * ci: use actions repo * ci: use v1 tags * ci: remove semi * chore: change job name passed to eslint * chore: dummy commit, remove different semi * ci: change job name * chore: dummy commit * ci: add gh-actions as possible branches * ci: lint all branches * ci: dummy * ci: separate pr and push * chore: run actions on gh branch * ci: try excluding branches --- .github/actions/docs/Dockerfile | 11 ++++++ .github/actions/docs/src/entrypoint.sh | 54 ++++++++++++++++++++++++++ .github/workflows/docs.yml | 25 ++++++++++++ .github/workflows/lint.yml | 28 +++++++++++++ travis/test.sh | 2 +- 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 .github/actions/docs/Dockerfile create mode 100755 .github/actions/docs/src/entrypoint.sh create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/lint.yml diff --git a/.github/actions/docs/Dockerfile b/.github/actions/docs/Dockerfile new file mode 100644 index 00000000..45877195 --- /dev/null +++ b/.github/actions/docs/Dockerfile @@ -0,0 +1,11 @@ +FROM node:12-slim + +LABEL com.github.actions.name="Docs" +LABEL com.github.actions.description="Commit docs to the docs/ branch." +LABEL com.github.actions.icon="upload-cloud" +LABEL com.github.actions.color="blue" + +RUN apt-get update && apt-get install -y git + +COPY src /actions/docs/src +ENTRYPOINT ["/actions/docs/src/entrypoint.sh"] diff --git a/.github/actions/docs/src/entrypoint.sh b/.github/actions/docs/src/entrypoint.sh new file mode 100755 index 00000000..042c166a --- /dev/null +++ b/.github/actions/docs/src/entrypoint.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -e + +cd $GITHUB_WORKSPACE + +# Run the build +npm run docs +NODE_ENV=production npm run build:browser + +# Initialise some useful variables +REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" +BRANCH_OR_TAG=`awk -F/ '{print $2}' <<< $GITHUB_REF` +CURRENT_BRANCH=`awk -F/ '{print $NF}' <<< $GITHUB_REF` + +if [ "$BRANCH_OR_TAG" == "heads" ]; then + SOURCE_TYPE="branch" +else + SOURCE_TYPE="tag" +fi + +# Checkout the repo in the target branch so we can build docs and push to it +TARGET_BRANCH="docs" +git clone $REPO out -b $TARGET_BRANCH + +# Move the generated JSON file to the newly-checked-out repo, to be committed and pushed +mv docs/docs.json out/$CURRENT_BRANCH.json + +# Commit and push +cd out +git add . +git config user.name "${GITHUB_ACTOR}" +git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" +git commit -m "Docs build for ${SOURCE_TYPE} ${CURRENT_BRANCH}: ${GITHUB_SHA}" || true +git push origin $TARGET_BRANCH + +# Clean up... +cd .. +rm -rf out + +# ...then do the same once more for the webpack +TARGET_BRANCH="webpack" +git clone $REPO out -b $TARGET_BRANCH + +# Move the generated webpack over +mv webpack/discord.min.js out/discord.$CURRENT_BRANCH.min.js + +# Commit and push +cd out +git add . +git config user.name "${GITHUB_ACTOR}" +git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" +git commit -m "Webpack build for ${SOURCE_TYPE} ${CURRENT_BRANCH}: ${GITHUB_SHA}" || true +git push origin $TARGET_BRANCH diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..5c01c2c7 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,25 @@ +name: Docs + +on: + push: + branches: + - '!gh-action' + - '!webpack' + - '!docs' + +jobs: + docs: + name: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: install node v12 + uses: actions/setup-node@master + with: + node-version: 12 + - name: npm install + run: npm install + - name: deploy docs + uses: ./.github/actions/docs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..5f41b66e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + pull_request: + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: npm install + run: npm install + - name: eslint + uses: discordjs/action-eslint@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + job-name: lint + - name: lint typings + run: npm run lint:typings + - name: lint docs + run: npm run docs:test diff --git a/travis/test.sh b/travis/test.sh index bef6134e..83eb37e1 100644 --- a/travis/test.sh +++ b/travis/test.sh @@ -20,4 +20,4 @@ else fi # Run the tests -npm test +npm run docs:test && npm run lint:typings From 4f9e7f4a239aacc4b38d1a0e908cbe279b311867 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 19:32:40 +0200 Subject: [PATCH 229/428] ci: remove travis --- .travis.yml | 19 ---------- travis/deploy-key.enc | Bin 3248 -> 0 bytes travis/deploy.sh | 83 ------------------------------------------ travis/test.sh | 23 ------------ 4 files changed, 125 deletions(-) delete mode 100644 .travis.yml delete mode 100644 travis/deploy-key.enc delete mode 100644 travis/deploy.sh delete mode 100644 travis/test.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2215629f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: node_js -node_js: - - 10 - - 11 -install: npm install -script: bash ./travis/test.sh -jobs: - include: - - stage: deploy - node_js: 10 - script: bash ./travis/deploy.sh - env: - - ENCRYPTION_LABEL="af862fa96d3e" - - COMMIT_AUTHOR_EMAIL="amishshah.2k@gmail.com" -cache: - directories: - - node_modules -dist: trusty -sudo: false diff --git a/travis/deploy-key.enc b/travis/deploy-key.enc deleted file mode 100644 index e03fc36d76feffef7706f7e3b37297e4053dd3fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3248 zcmV;h3{UfQ7qe5cltM)3H)E$<0*dYcb_y)AMS*!fAQBmoURieK{5Ji;@B_GMMkOYb zF}xIvNM3<%jLfQ%!s?X#dB_7qis+rm34dO#srpI5Pc5D+=_qL7%+Qo5{b`624Tk!3@BJYWP!^#wW}gAkJ_V)1hkHNy zjzNG5ERQ5arPF=0x8?`^`auCCoJ6eEQp*vwA6_AW%QlCQ7rQGsWN@373JMgCD{6 zp}XRPZ7-YqJj@g+Z-Mn{#g%Pikv9p|JHO~oTQnkV%;K`zh)IOTqn~ZrZyS0+KGSQz zWC_MqUeYTu*p?PzPD%EAGuUnrWfKfgV~XwnxZU$E-BM&otzBc;~(Am zYaAc|^^twGqRx>+eGCP5E4)59)D;*W@ z&3Pu=lc>Z<7A0Td+X@uko!xe50>5v(>-i`Lkv+*0;B!3jF<6MUXQN$5hxJ(DIwM5k_DiIY`CrOdkbd^@!1n$Thtdgbfm=;J*gyEJ8%#zZi+3 z)Q_X~0WDIwIA0dl1hQXHH2CTLjZvQdi}K`3N5tau{7-2m?u~X(RDqFjT^kz#c0SzC z@+(Xr-}zgA;TH|Ks2YBbzkmeuL?y*j-2|~z_k}VBcil!y#q;b+24)1|2WL*OPk=^Q zo(49b$h3>R#6?ra)GG`Vg3E37*4nzjVX8N=wR!U4KYg=wuh5A8Kn--q?_>GHiD z4D9RD!dRCK5fGX>zd@bV3y=0X$-D6ZX@DbzALQ&LBfr7|+^iEKe^Ud3KOn)h#bF&| zTft%Wfc-NcZ&}j=)IfpxgGM0;6$Se}NI65*RtDF3Gy*a??wrtvx5N-e54u%%)2_DF@`<7%PVo_^i)s z3K!?fdkLX5q^5!e#@?tDzoKdS%ksRZepz1}?@VaSrD8{=;qp~5R4G6{PQU~`EMO@c zjjdB#_hkQK4mo$t#j95*qGhAy5bMEOEl!{+^N1G+`+ zcm(+t%U%YH)UmDgPUsaj7eJhw?#|J@+!`NC>k*X(34r>48V8JYvL>~+x_r-Ar6vW0 zMAfv+qIZ?@ntd_PC&hJ=JDgtFfS~HlHxXexUj1<=hmLT&UYMt*C+k(gA(THdP=yCK zO@hVR3ay8i9%?XPnNW8NmX8pw3I06tEw$%%<$`%o@9g^-rE~d~O-nZ3gsMm)S(G}hvJuVCEc3FE-wcqGw1Rz3S zX|jOU`#e^EYj}e|O3vQ_6GtS!I?TpHROUvI>HB<>(sZIc;HADkftzD4~| zMCq6Tv#`3577YYC@z%gQ1(b*E*;SL~TD=}*Te8lyNu3$}ND%mM);e1gZCR$&A6^aN zRjKLOgt=9Gx=6vzqm&xG9#)lV@YV*HYZGLtxUkDvo6gWu&b4=^pT}$n8Jc7lw^>Lz zH^tZAF{2=zKG-3v6`hGgC(o$e3F!*`e$>z0zMr4gqvns2`FQE4R%a_G|7SZ)O`rv! zwq2W>EZbo^atWCOh;T~B(c+$>tvvc?LtxzVgQk4oMf(7bVLsX+EY=_rlH@bTL zwr)tDG_ClyK-YxS^j)V@>In)w(Ibc|Ap1BfF&*nZDlhpfN`h8Nz~3TA z#J0X#!uTEA3ak_cNFWd;fQvimIiz_Fn45ObH~_Fx)sUPHN?IJjRX-r{HY{M})l4+O z6$Hf^awj!~6^#LV0Ha|CvxU?Ks$tsz1TS9xHf2NM8|x{KKy+AlplB&ec<4!1aGoJy zu^7K@Y;57i6}vzu!jw_lOs`Ec6|7Cdti^m@b^^O}Z!V=wKW()W-_cdzs5gS5Lqij* zUafTBaBDj55HZkoDv}bbWIcI&BdaZ5T`})rS%^30+Tij@2ivsIRm(fIkxmO71$L9! zCXL+`Dc!G+4UqXy^X4kFF(w-of~J*n_iyud@cV|h z!+W6qe_=BJJRy)WXB{7mDBV4RrO%*Z7OLZ+M!}g8x>vqxZ@~=M=$?0Kiaal2L2NUk zOrl9Vl#am2=_Bkz*>+Y90{ccFWEy&9_Enr9ai735V;z1^o3rj?5{{0az-Hd>ss+{N z2I5~47xG-`&7ln2gn|ePl$At`$RhT@kXihGqH0W;RA$>UeNw-=jti4zBdVLZ*G*V?Tq!tbg2}v5L;Ft){qdv;J=1{9R$Z{W3rf$mpJ$Q z%lKVC`sfn>ePjpYV<|Q}axIy0*XoUsCdt8y!t^^f!>DzF5jaoIR zg`dMqg%gSJCB<8blzbsrnS)I-3V~B@hJ_^>h0ucfUX~yFI2V7TL6abO5iW42pI#mv z)r4_>mH9&Q|(#!7WQ_e{;rfw5rak{U=?PE7OeW(HMOG=%Ge)iwTi zZ(v;tzRe3Eo!-ee)YdsC|L0gwDbhVVu5LB3&A*iD5-P)>uhl#fhLhUW8m7Ii^Mniq zQ(NZ_yIm}9QCR&fUfiw<1ap|cgYle8aDM+esvnKAp92B_z8zEkspsNNzIc@&8&vuX zya|*P8DuGz3NN!ppWfRSYaouy%AN=GjbV3@e>!T56sxu|PiXx?0t0-Qk4jPRb>1qav@ z7qckX#}@0JyM0t~-if&7fcwS87-G(!ee3+4hPZt+iqGF=yAf>%XV#DJvdon?h6=4q z_#+~vqW?r+q4l}N+Rf?8^2P7)_^~OX>_0VB Date: Sat, 24 Aug 2019 19:45:52 +0200 Subject: [PATCH 230/428] ci: run docs excluding --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5c01c2c7..5775f484 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,6 +3,7 @@ name: Docs on: push: branches: + - '*' - '!gh-action' - '!webpack' - '!docs' From fd49082ac0fd9fcdefdc5f26e79b545570cffa16 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 20:28:44 +0200 Subject: [PATCH 231/428] ci: run ci in parallel --- .github/workflows/lint.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5f41b66e..4d5a7839 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,12 +1,12 @@ -name: Lint +name: ESLint, Typings, and Docs on: push: pull_request: jobs: - lint: - name: lint + eslint: + name: eslint runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -21,8 +21,28 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - job-name: lint + job-name: eslint + + typings-lint: + name: typings-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 - name: lint typings run: npm run lint:typings + + docs-lint: + name: docs-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 - name: lint docs run: npm run docs:test From 57033f3334a0b89167cec2264e125670a7c8a64d Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 20:31:12 +0200 Subject: [PATCH 232/428] ci: add npm install steps --- .github/workflows/lint.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4d5a7839..b85aa8b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: ESLint, Typings, and Docs +name: Lint on: push: @@ -32,6 +32,8 @@ jobs: uses: actions/setup-node@v1 with: node-version: 12 + - name: npm install + run: npm install - name: lint typings run: npm run lint:typings @@ -44,5 +46,7 @@ jobs: uses: actions/setup-node@v1 with: node-version: 12 + - name: npm install + run: npm install - name: lint docs run: npm run docs:test From 08dfdcb4eb397c9021c22d2c5301ad7bf5d252d1 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 22:17:40 +0200 Subject: [PATCH 233/428] ci: deploy docs using new action --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5775f484..d5f9e18f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,6 +21,6 @@ jobs: - name: npm install run: npm install - name: deploy docs - uses: ./.github/actions/docs + uses: discordjs/action-docs@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 6baff9e3fca155b9af99b724758aa43c86b17e18 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 22:26:08 +0200 Subject: [PATCH 234/428] ci: change job name for docs --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d5f9e18f..9bfbca24 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,7 +9,7 @@ on: - '!docs' jobs: - docs: + deploy: name: deploy runs-on: ubuntu-latest steps: From 8bbf1a922841a64b70265b5bc81d2bd530d54db1 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 25 Aug 2019 07:45:45 +0200 Subject: [PATCH 235/428] ci: remove leftover docs action --- .github/actions/docs/Dockerfile | 11 ------ .github/actions/docs/src/entrypoint.sh | 54 -------------------------- 2 files changed, 65 deletions(-) delete mode 100644 .github/actions/docs/Dockerfile delete mode 100755 .github/actions/docs/src/entrypoint.sh diff --git a/.github/actions/docs/Dockerfile b/.github/actions/docs/Dockerfile deleted file mode 100644 index 45877195..00000000 --- a/.github/actions/docs/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:12-slim - -LABEL com.github.actions.name="Docs" -LABEL com.github.actions.description="Commit docs to the docs/ branch." -LABEL com.github.actions.icon="upload-cloud" -LABEL com.github.actions.color="blue" - -RUN apt-get update && apt-get install -y git - -COPY src /actions/docs/src -ENTRYPOINT ["/actions/docs/src/entrypoint.sh"] diff --git a/.github/actions/docs/src/entrypoint.sh b/.github/actions/docs/src/entrypoint.sh deleted file mode 100755 index 042c166a..00000000 --- a/.github/actions/docs/src/entrypoint.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -set -e - -cd $GITHUB_WORKSPACE - -# Run the build -npm run docs -NODE_ENV=production npm run build:browser - -# Initialise some useful variables -REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" -BRANCH_OR_TAG=`awk -F/ '{print $2}' <<< $GITHUB_REF` -CURRENT_BRANCH=`awk -F/ '{print $NF}' <<< $GITHUB_REF` - -if [ "$BRANCH_OR_TAG" == "heads" ]; then - SOURCE_TYPE="branch" -else - SOURCE_TYPE="tag" -fi - -# Checkout the repo in the target branch so we can build docs and push to it -TARGET_BRANCH="docs" -git clone $REPO out -b $TARGET_BRANCH - -# Move the generated JSON file to the newly-checked-out repo, to be committed and pushed -mv docs/docs.json out/$CURRENT_BRANCH.json - -# Commit and push -cd out -git add . -git config user.name "${GITHUB_ACTOR}" -git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" -git commit -m "Docs build for ${SOURCE_TYPE} ${CURRENT_BRANCH}: ${GITHUB_SHA}" || true -git push origin $TARGET_BRANCH - -# Clean up... -cd .. -rm -rf out - -# ...then do the same once more for the webpack -TARGET_BRANCH="webpack" -git clone $REPO out -b $TARGET_BRANCH - -# Move the generated webpack over -mv webpack/discord.min.js out/discord.$CURRENT_BRANCH.min.js - -# Commit and push -cd out -git add . -git config user.name "${GITHUB_ACTOR}" -git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" -git commit -m "Webpack build for ${SOURCE_TYPE} ${CURRENT_BRANCH}: ${GITHUB_SHA}" || true -git push origin $TARGET_BRANCH From 4c088123026e520f46cb4ee21bb90140d2501215 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Date: Mon, 26 Aug 2019 12:52:30 -0400 Subject: [PATCH 236/428] docs(ClientOptions): document unit of restRequestTimeout (#3449) --- src/util/Constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 50033919..a344f653 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -28,7 +28,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * corresponding websocket events * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST * requests (higher values will reduce rate-limiting errors on bad connections) - * @property {number} [restRequestTimeout=15000] Time to wait before cancelling a REST request + * @property {number} [restRequestTimeout=15000] Time to wait before cancelling a REST request, in milliseconds * @property {number} [restSweepInterval=60] How frequently to delete inactive request buckets, in seconds * (or 0 for never) * @property {number} [retryLimit=1] How many times to retry on 5XX errors (Infinity for indefinite amount of retries) From 745a0ea942e277499505e69e7890a1aa768f217a Mon Sep 17 00:00:00 2001 From: didinele Date: Wed, 28 Aug 2019 12:11:15 +0300 Subject: [PATCH 237/428] typings(DataStore): correct return types for resolve, resolveID and remove (#3448) * fix(typings): DataStore#resolve & DataStore#resolveID can also return null. * fix(typings): DataStore#remove returns a boolean, not void. Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> --- typings/index.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c6a69c91..519acadf 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1715,9 +1715,9 @@ declare module 'discord.js' { public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; - public remove(key: K): void; - public resolve(resolvable: R): V; - public resolveID(resolvable: R): K; + public remove(key: K): boolean; + public resolve(resolvable: R): V | null; + public resolveID(resolvable: R): K | null; } export class GuildEmojiRoleStore extends OverridableDataStore { From 2a3fb705d0296266e3cd7e9976a47459c69653a9 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Wed, 28 Aug 2019 10:13:09 +0100 Subject: [PATCH 238/428] fix(ChannelStore): return existing DMChannels within add() (#3438) * fix: return existing DMChannels * ref: group nested conditions --- src/stores/ChannelStore.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index d0e1caa7..762f0e19 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -54,9 +54,9 @@ class ChannelStore extends DataStore { add(data, guild, cache = true) { const existing = this.get(data.id); - if (existing && existing._patch && cache) existing._patch(data); - if (existing && guild) { - guild.channels.add(existing); + if (existing) { + if (existing._patch && cache) existing._patch(data); + if (guild) guild.channels.add(existing); return existing; } From 89d9b0f498b797ab15d7d1452ef0e25a9085fd59 Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 29 Aug 2019 20:16:38 +0200 Subject: [PATCH 239/428] fix(typings): partially revert #3448 --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 519acadf..0e73a24f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1715,7 +1715,7 @@ declare module 'discord.js' { public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; - public remove(key: K): boolean; + public remove(key: K): void; public resolve(resolvable: R): V | null; public resolveID(resolvable: R): K | null; } From 9e6a73d1a02450a67555adb09c7e0e9daef689ed Mon Sep 17 00:00:00 2001 From: Yukine Date: Sat, 31 Aug 2019 22:17:46 +0200 Subject: [PATCH 240/428] typings(Client): remove 'resume', add 'replayed' parameter to 'shardResumed' (#3455) corrected shardResumed & removed old resumed event --- typings/index.d.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0e73a24f..85d0343a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -180,7 +180,6 @@ declare module 'discord.js' { public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; - public on(event: 'resume', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; @@ -192,7 +191,7 @@ declare module 'discord.js' { public on(event: 'shardError', listener: (error: Error, id: number) => void): this; public on(event: 'shardReconnecting', listener: (id: number) => void): this; public on(event: 'shardReady', listener: (id: number) => void): this; - public on(event: 'shardResumed', listener: (id: number) => void): this; + public on(event: 'shardResumed', listener: (id: number, replayed: number) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -218,7 +217,6 @@ declare module 'discord.js' { public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; - public once(event: 'resume', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; @@ -230,7 +228,7 @@ declare module 'discord.js' { public once(event: 'shardError', listener: (error: Error, id: number) => void): this; public once(event: 'shardReconnecting', listener: (id: number) => void): this; public once(event: 'shardReady', listener: (id: number) => void): this; - public once(event: 'shardResumed', listener: (id: number) => void): this; + public once(event: 'shardResumed', listener: (id: number, replayed: number) => void): this; public once(event: string, listener: Function): this; } From 4b34f1acbe85aab1dfac85c069bf2a23010ceaa1 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 31 Aug 2019 17:14:48 -0400 Subject: [PATCH 241/428] Remove past-tense naming on shard events --- src/client/websocket/WebSocketManager.js | 4 ++-- src/client/websocket/handlers/RESUMED.js | 4 ++-- src/util/Constants.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index c2530ef3..8668d2de 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -205,11 +205,11 @@ class WebSocketManager extends EventEmitter { if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { /** * Emitted when a shard's WebSocket disconnects and will no longer reconnect. - * @event Client#shardDisconnected + * @event Client#shardDisconnect * @param {CloseEvent} event The WebSocket close event * @param {number} id The shard ID that disconnected */ - this.client.emit(Events.SHARD_DISCONNECTED, event, shard.id); + this.client.emit(Events.SHARD_DISCONNECT, event, shard.id); this.debug(WSCodes[event.code], shard); return; } diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js index e345cb74..5e5f403a 100644 --- a/src/client/websocket/handlers/RESUMED.js +++ b/src/client/websocket/handlers/RESUMED.js @@ -6,9 +6,9 @@ module.exports = (client, packet, shard) => { const replayed = shard.sequence - shard.closeSequence; /** * Emitted when a shard resumes successfully. - * @event Client#shardResumed + * @event Client#shardResume * @param {number} id The shard ID that resumed * @param {number} replayedEvents The amount of replayed events */ - client.emit(Events.SHARD_RESUMED, shard.id, replayed); + client.emit(Events.SHARD_RESUME, shard.id, replayed); }; diff --git a/src/util/Constants.js b/src/util/Constants.js index a344f653..23591328 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -264,11 +264,11 @@ exports.Events = { ERROR: 'error', WARN: 'warn', DEBUG: 'debug', - SHARD_DISCONNECTED: 'shardDisconnected', + SHARD_DISCONNECT: 'shardDisconnect', SHARD_ERROR: 'shardError', SHARD_RECONNECTING: 'shardReconnecting', SHARD_READY: 'shardReady', - SHARD_RESUMED: 'shardResumed', + SHARD_RESUME: 'shardResume', INVALIDATED: 'invalidated', RAW: 'raw', }; From 45af62a621ccf41dd26d4f76dabb947abaa55c03 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 31 Aug 2019 17:18:18 -0400 Subject: [PATCH 242/428] Update typings for renamed shard events --- typings/index.d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 85d0343a..fc3f2bb1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -187,11 +187,11 @@ declare module 'discord.js' { public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: 'invalidated', listener: () => void): this; - public on(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this; + public on(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; public on(event: 'shardError', listener: (error: Error, id: number) => void): this; public on(event: 'shardReconnecting', listener: (id: number) => void): this; public on(event: 'shardReady', listener: (id: number) => void): this; - public on(event: 'shardResumed', listener: (id: number, replayed: number) => void): this; + public on(event: 'shardResume', listener: (id: number, replayed: number) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -224,11 +224,11 @@ declare module 'discord.js' { public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: 'invalidated', listener: () => void): this; - public once(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this; + public once(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; public once(event: 'shardError', listener: (error: Error, id: number) => void): this; public once(event: 'shardReconnecting', listener: (id: number) => void): this; public once(event: 'shardReady', listener: (id: number) => void): this; - public once(event: 'shardResumed', listener: (id: number, replayed: number) => void): this; + public once(event: 'shardResume', listener: (id: number, replayed: number) => void): this; public once(event: string, listener: Function): this; } @@ -473,11 +473,11 @@ declare module 'discord.js' { ERROR: 'error'; WARN: 'warn'; DEBUG: 'debug'; - SHARD_DISCONNECTED: 'shardDisconnected'; + SHARD_DISCONNECT: 'shardDisconnect'; SHARD_ERROR: 'shardError'; SHARD_RECONNECTING: 'shardReconnecting'; SHARD_READY: 'shardReady'; - SHARD_RESUMED: 'shardResumed'; + SHARD_RESUME: 'shardResume'; INVALIDATED: 'invalidated'; RAW: 'raw'; }; From 3ce60212e6c92d4bd2d7113682af4a153dbd4d78 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 1 Sep 2019 15:41:07 +0200 Subject: [PATCH 243/428] ci: run a lint cronjob every 12h --- .github/workflows/lint-cron.yml | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/lint-cron.yml diff --git a/.github/workflows/lint-cron.yml b/.github/workflows/lint-cron.yml new file mode 100644 index 00000000..077fe438 --- /dev/null +++ b/.github/workflows/lint-cron.yml @@ -0,0 +1,48 @@ +name: Lint Cronjob + +on: + schedule: + - cron: '0 */12 * * *' + +jobs: + eslint: + name: eslint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: npm install + run: npm install + - name: eslint + uses: npm run lint + + typings-lint: + name: typings-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: npm install + run: npm install + - name: lint typings + run: npm run lint:typings + + docs-lint: + name: docs-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: npm install + run: npm install + - name: lint docs + run: npm run docs:test From fe71cecc2acd2898364f42c11565980cd3ffd668 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Mon, 2 Sep 2019 13:27:34 +0200 Subject: [PATCH 244/428] ci: use the correct command --- .github/workflows/lint-cron.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-cron.yml b/.github/workflows/lint-cron.yml index 077fe438..2e101d12 100644 --- a/.github/workflows/lint-cron.yml +++ b/.github/workflows/lint-cron.yml @@ -17,7 +17,7 @@ jobs: - name: npm install run: npm install - name: eslint - uses: npm run lint + run: npm run lint typings-lint: name: typings-lint From e7a961781c2a4325ba967d87eed99b11b1f31b2c Mon Sep 17 00:00:00 2001 From: iCrawl Date: Tue, 3 Sep 2019 16:04:36 +0200 Subject: [PATCH 245/428] ci: add webpack workflow --- .github/workflows/docs.yml | 1 - .github/workflows/webpack.yml | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/webpack.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9bfbca24..13f197ad 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,7 +4,6 @@ on: push: branches: - '*' - - '!gh-action' - '!webpack' - '!docs' diff --git a/.github/workflows/webpack.yml b/.github/workflows/webpack.yml new file mode 100644 index 00000000..027bfeb3 --- /dev/null +++ b/.github/workflows/webpack.yml @@ -0,0 +1,25 @@ +name: Webpack + +on: + push: + branches: + - '*' + - '!webpack' + - '!docs' + +jobs: + deploy: + name: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: install node v12 + uses: actions/setup-node@master + with: + node-version: 12 + - name: npm install + run: npm install + - name: deploy webpack + uses: discordjs/action-webpack@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0c14616ffc4dc69a9e1b2f9f34339902fe0b0944 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Tue, 3 Sep 2019 16:08:25 +0200 Subject: [PATCH 246/428] ci: reduce the amount of yml files --- .github/workflows/{webpack.yml => deploy.yml} | 21 +++++++++++++--- .github/workflows/docs.yml | 25 ------------------- 2 files changed, 18 insertions(+), 28 deletions(-) rename .github/workflows/{webpack.yml => deploy.yml} (51%) delete mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/webpack.yml b/.github/workflows/deploy.yml similarity index 51% rename from .github/workflows/webpack.yml rename to .github/workflows/deploy.yml index 027bfeb3..f1059b2d 100644 --- a/.github/workflows/webpack.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Webpack +name: Deploy on: push: @@ -8,8 +8,23 @@ on: - '!docs' jobs: - deploy: - name: deploy + docs: + name: docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: install node v12 + uses: actions/setup-node@master + with: + node-version: 12 + - name: npm install + run: npm install + - name: docs + uses: discordjs/action-docs@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + webpack: + name: webpack runs-on: ubuntu-latest steps: - uses: actions/checkout@master diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 13f197ad..00000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Docs - -on: - push: - branches: - - '*' - - '!webpack' - - '!docs' - -jobs: - deploy: - name: deploy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: install node v12 - uses: actions/setup-node@master - with: - node-version: 12 - - name: npm install - run: npm install - - name: deploy docs - uses: discordjs/action-docs@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0a2003095d4aa045fef6e030ee5dc6d5494214cd Mon Sep 17 00:00:00 2001 From: iCrawl Date: Tue, 3 Sep 2019 16:18:56 +0200 Subject: [PATCH 247/428] ci: consistency in naming --- .github/workflows/lint.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b85aa8b6..63fde90f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,8 +23,8 @@ jobs: with: job-name: eslint - typings-lint: - name: typings-lint + typings: + name: typings runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -37,8 +37,8 @@ jobs: - name: lint typings run: npm run lint:typings - docs-lint: - name: docs-lint + docs: + name: docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 From 5d95a4b264eae333af67ecfceaad1aa63dfdea0e Mon Sep 17 00:00:00 2001 From: BannerBomb Date: Tue, 3 Sep 2019 10:24:20 -0400 Subject: [PATCH 248/428] fix: Util#splitMessage when destructured (#3456) --- src/util/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index 8e6743d9..f0079a2a 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -56,7 +56,7 @@ class Util { * @returns {string[]} */ static splitMessage(text, { maxLength = 2000, char = '\n', prepend = '', append = '' } = {}) { - text = this.resolveString(text); + text = Util.resolveString(text); if (text.length <= maxLength) return [text]; const splitText = text.split(char); if (splitText.some(chunk => chunk.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN'); From 827cab805e7c7d158a2111bd214ca3f13c613bba Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Wed, 4 Sep 2019 01:02:01 -0400 Subject: [PATCH 249/428] Clean up workflows --- .github/workflows/deploy.yml | 67 ++++++++++++++++++--------------- .github/workflows/lint-cron.yml | 48 ----------------------- .github/workflows/lint.yml | 52 ------------------------- .github/workflows/test-cron.yml | 58 ++++++++++++++++++++++++++++ .github/workflows/test.yml | 60 +++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 130 deletions(-) delete mode 100644 .github/workflows/lint-cron.yml delete mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test-cron.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f1059b2d..ceb67cf7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,40 +1,47 @@ -name: Deploy - +name: Deployment on: push: branches: - - '*' - - '!webpack' - - '!docs' - + - '*' + - '!webpack' + - '!docs' jobs: docs: - name: docs + name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: install node v12 - uses: actions/setup-node@master - with: - node-version: 12 - - name: npm install - run: npm install - - name: docs - uses: discordjs/action-docs@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository + uses: actions/checkout@master + + - name: Install Node v12 + uses: actions/setup-node@master + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Build and deploy documentation + uses: discordjs/action-docs@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + webpack: - name: webpack + name: Webpack runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: install node v12 - uses: actions/setup-node@master - with: - node-version: 12 - - name: npm install - run: npm install - - name: deploy webpack - uses: discordjs/action-webpack@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository + uses: actions/checkout@master + + - name: Install Node v12 + uses: actions/setup-node@master + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Build and deploy Webpack + uses: discordjs/action-webpack@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint-cron.yml b/.github/workflows/lint-cron.yml deleted file mode 100644 index 2e101d12..00000000 --- a/.github/workflows/lint-cron.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Lint Cronjob - -on: - schedule: - - cron: '0 */12 * * *' - -jobs: - eslint: - name: eslint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: eslint - run: npm run lint - - typings-lint: - name: typings-lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: lint typings - run: npm run lint:typings - - docs-lint: - name: docs-lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: lint docs - run: npm run docs:test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 63fde90f..00000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Lint - -on: - push: - pull_request: - -jobs: - eslint: - name: eslint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: eslint - uses: discordjs/action-eslint@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - job-name: eslint - - typings: - name: typings - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: lint typings - run: npm run lint:typings - - docs: - name: docs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: lint docs - run: npm run docs:test diff --git a/.github/workflows/test-cron.yml b/.github/workflows/test-cron.yml new file mode 100644 index 00000000..99ee1067 --- /dev/null +++ b/.github/workflows/test-cron.yml @@ -0,0 +1,58 @@ +name: Testing Cron +on: + schedule: + - cron: '0 */12 * * *' +jobs: + lint: + name: ESLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Run ESLint + run: npm run lint + + typings: + name: TSLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Run TSLint + run: npm run lint:typings + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Test documentation + run: npm run docs:test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..641a6327 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: Testing +on: [push, pull_request] +jobs: + lint: + name: ESLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Run ESLint + uses: discordjs/action-eslint@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + job-name: ESLint + + typings: + name: TSLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Run TSLint + run: npm run lint:typings + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Test documentation + run: npm run docs:test From 89a3a3a6daa7b5e25bd277b4e2ec6bccb368f1df Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Wed, 4 Sep 2019 01:04:31 -0400 Subject: [PATCH 250/428] Lowercase webpack (their branding, not mine) --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ceb67cf7..4deae773 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,7 +27,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} webpack: - name: Webpack + name: webpack runs-on: ubuntu-latest steps: - name: Checkout repository @@ -41,7 +41,7 @@ jobs: - name: Install dependencies run: npm install - - name: Build and deploy Webpack + - name: Build and deploy webpack uses: discordjs/action-webpack@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c5cbf8677e72522a366d03c9c90e313b864ff582 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Fri, 6 Sep 2019 12:03:21 +0200 Subject: [PATCH 251/428] feat(typings): reply overloads for splitmessage --- typings/index.d.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index fc3f2bb1..aaf9f524 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -981,8 +981,12 @@ declare module 'discord.js' { public fetch(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; - public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; public toJSON(): object; public toString(): string; public unpin(): Promise; From d252ddf9daa57311b8bc6435a959f2c49f901e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Sun, 8 Sep 2019 09:49:10 +0200 Subject: [PATCH 252/428] docs: Document Message#author as nullable (#3464) Fixes #3463 --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 99c006b7..5b6cc475 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -63,7 +63,7 @@ class Message extends Base { /** * The author of the message - * @type {User} + * @type {?User} */ this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null; From c71454f312152129663723ea6a817b6f9800270c Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 8 Sep 2019 11:46:11 +0200 Subject: [PATCH 253/428] chore: add more voice files to ignore for webpack --- package.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c234bfb7..626fea20 100644 --- a/package.json +++ b/package.json @@ -88,15 +88,20 @@ "src/sharding/ShardClientUtil.js": false, "src/sharding/ShardingManager.js": false, "src/client/voice/ClientVoiceManager.js": false, + "src/client/voice/VoiceBroadcast.js": false, "src/client/voice/VoiceConnection.js": false, + "src/client/voice/dispatcher/BroadcastDispatcher.js": false, + "src/client/voice/dispatcher/StreamDispatcher.js": false, "src/client/voice/networking/VoiceUDPClient.js": false, "src/client/voice/networking/VoiceWebSocket.js": false, - "src/client/voice/dispatcher/StreamDispatcher.js": false, "src/client/voice/player/AudioPlayer.js": false, + "src/client/voice/player/BasePlayer.js": false, + "src/client/voice/player/BroadcastAudioPlayer.js": false, "src/client/voice/receiver/PacketHandler.js": false, "src/client/voice/receiver/Receiver.js": false, + "src/client/voice/util/PlayInterface.js": false, "src/client/voice/util/Secretbox.js": false, - "src/client/voice/util/VolumeInterface.js": false, - "src/client/voice/VoiceBroadcast.js": false + "src/client/voice/util/Silence.js": false, + "src/client/voice/util/VolumeInterface.js": false } } From 6e616edc696d76593f0f19094b5bc38d581ea3dd Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 8 Sep 2019 11:46:47 +0200 Subject: [PATCH 254/428] chore: keep_classnames literally everywhere --- webpack.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index c95745f1..f07e0f6a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,12 +49,13 @@ module.exports = { optimization: { minimizer: [ new TerserJSPlugin({ + cache: false, terserOptions: { mangle: { keep_classnames: true }, compress: { keep_classnames: true }, + keep_classnames: true, output: { comments: false }, }, - parallel: true, }), ], }, From 4fc461c2f932709f51efd65a401d943a7efcec12 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 8 Sep 2019 11:47:46 +0200 Subject: [PATCH 255/428] fix: browser-compatability Fix #3453 --- src/client/websocket/WebSocketShard.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 9663052c..f77635b0 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -2,19 +2,24 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); -const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); +const { browser, Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); let zstd; let zlib; -try { - zstd = require('zucc'); -} catch (e) { +if (browser) { + zlib = require('pako'); +} else { try { - zlib = require('zlib-sync'); - if (!zlib.Inflate) zlib = require('pako'); - } catch (err) { - zlib = require('pako'); + zstd = require('zucc'); + if (!zstd.DecompressStream) zstd = null; + } catch (e) { + try { + zlib = require('zlib-sync'); + if (!zlib.Inflate) zlib = require('pako'); + } catch (err) { + zlib = require('pako'); + } } } From a6810e2eaaea4021d0a283c7f20ff6d643d34f4f Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Tue, 10 Sep 2019 18:55:42 +1000 Subject: [PATCH 256/428] feat(Permissions): add new method Permissions#any (#3450) * Add new method Permissions#any * Update src/util/BitField.js This is much better Co-Authored-By: bdistin * Remove unreachable code * Gotta keep the linter happy * Apply bdistin suggested change to both methods --- src/util/BitField.js | 9 +++++++++ src/util/Permissions.js | 13 +++++++++++-- typings/index.d.ts | 2 ++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/util/BitField.js b/src/util/BitField.js index 424a8443..757da7ab 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -17,6 +17,15 @@ class BitField { this.bitfield = this.constructor.resolve(bits); } + /** + * Checks whether the bitfield has a bit, or any of multiple bits. + * @param {BitFieldResolvable} bit Bit(s) to check for + * @returns {boolean} + */ + any(bit) { + return (this.bitfield & this.constructor.resolve(bit)) !== 0; + } + /** * Checks if this bitfield equals another * @param {BitFieldResolvable} bit Bit(s) to check for diff --git a/src/util/Permissions.js b/src/util/Permissions.js index ef5273ff..831f1738 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -18,6 +18,16 @@ class Permissions extends BitField { * @typedef {string|number|Permissions|PermissionResolvable[]} PermissionResolvable */ + /** + * Checks whether the bitfield has a permission, or any of multiple permissions. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {boolean} + */ + any(permission, checkAdmin = true) { + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.any(permission); + } + /** * Checks whether the bitfield has a permission, or multiple permissions. * @param {PermissionResolvable} permission Permission(s) to check for @@ -25,8 +35,7 @@ class Permissions extends BitField { * @returns {boolean} */ has(permission, checkAdmin = true) { - if (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) return true; - return super.has(permission); + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.has(permission); } } diff --git a/typings/index.d.ts b/typings/index.d.ts index aaf9f524..13405c3f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -99,6 +99,7 @@ declare module 'discord.js' { constructor(bits?: BitFieldResolvable); public bitfield: number; public add(...bits: BitFieldResolvable[]): BitField; + public any(bit: BitFieldResolvable): boolean; public equals(bit: BitFieldResolvable): boolean; public freeze(): Readonly>; public has(bit: BitFieldResolvable): boolean; @@ -1111,6 +1112,7 @@ declare module 'discord.js' { } export class Permissions extends BitField { + public any(permission: PermissionResolvable, checkAdmin?: boolean): boolean; public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; public static ALL: number; From c86a6154aa736aee6bde9fa76ebd34b24225ee79 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Tue, 10 Sep 2019 02:00:04 -0700 Subject: [PATCH 257/428] feat(VoiceState): add kick method (#3462) * feat(VoiceState): add kick method * feat(typings): add types for VoiceState#kick method --- src/structures/VoiceState.js | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index e2cb2ab9..8e2922a9 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -139,6 +139,15 @@ class VoiceState extends Base { return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); } + /** + * Kicks the member from the voice channel. + * @param {string} [reason] Reason for kicking member from the channel + * @returns {Promise} + */ + kick(reason) { + return this.setChannel(null, reason); + } + /** * Moves the member to a different channel, or kick them from the one they're in. * @param {ChannelResolvable|null} [channel] Channel to move the member to, or `null` if you want to kick them from diff --git a/typings/index.d.ts b/typings/index.d.ts index 13405c3f..22e40854 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1590,6 +1590,7 @@ declare module 'discord.js' { public setDeaf(deaf: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; + public kick(reason?: string): Promise; public setChannel(channel: ChannelResolvable | null, reason?: string): Promise; public setSelfDeaf(deaf: boolean): Promise; public setSelfMute(mute: boolean): Promise; From 4072ffb50d28a6918e1b9613f5a4a123e722cf7c Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 10 Sep 2019 15:44:00 +0200 Subject: [PATCH 258/428] typings(GuildChannel): add members getter (#3467) --- typings/index.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 22e40854..f98eb230 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -804,6 +804,7 @@ declare module 'discord.js' { public readonly deletable: boolean; public guild: Guild; public readonly manageable: boolean; + public readonly members: Collection; public name: string; public readonly parent: CategoryChannel | null; public parentID: Snowflake; @@ -1362,7 +1363,6 @@ declare module 'discord.js' { export class TextChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); - public readonly members: Collection; public messages: MessageStore; public nsfw: boolean; public rateLimitPerUser: number; @@ -1375,7 +1375,6 @@ declare module 'discord.js' { export class NewsChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); - public readonly members: Collection; public messages: MessageStore; public nsfw: boolean; public topic: string; @@ -1479,7 +1478,6 @@ declare module 'discord.js' { public readonly editable: boolean; public readonly full: boolean; public readonly joinable: boolean; - public readonly members: Collection; public readonly speakable: boolean; public userLimit: number; public join(): Promise; From 8e0f525d911ce27a8c6c34f0a6fa43107c206ddb Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 10 Sep 2019 15:44:49 +0200 Subject: [PATCH 259/428] fix(Role): throw TypeError in comparePositionTo (#3466) --- src/structures/Role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index ce9f3637..9c7ebd49 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -150,7 +150,7 @@ class Role extends Base { */ comparePositionTo(role) { role = this.guild.roles.resolve(role); - if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); + if (!role) throw new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake'); return this.constructor.comparePositions(this, role); } From 6f83e715557ce834e0e5021846836698856a9fec Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Tue, 10 Sep 2019 07:47:13 -0600 Subject: [PATCH 260/428] feat: Guild#partnered (#3444) * feat: Guild#partnered * typings: added Guild#features * fix: removed trailing space * typings: made Guild#partnered readonly --- src/structures/Guild.js | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index f24f08e6..9de57145 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -403,6 +403,15 @@ class Guild extends Base { return new Date(this.joinedTimestamp); } + /** + * If this guild is partnered + * @type {boolean} + * @readonly + */ + get partnered() { + return this.features.includes('PARTNERED'); + } + /** * If this guild is verified * @type {boolean} diff --git a/typings/index.d.ts b/typings/index.d.ts index f98eb230..af240084 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -708,6 +708,7 @@ declare module 'discord.js' { public readonly nameAcronym: string; public readonly owner: GuildMember | null; public ownerID: Snowflake; + public readonly partnered: boolean; public premiumSubscriptionCount: number | null; public premiumTier: PremiumTier; public presences: PresenceStore; From b0047c424b5417287d2ea64ed84538931935b698 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 10 Sep 2019 15:09:06 +0100 Subject: [PATCH 261/428] =?UTF-8?q?feat(Partials):=20add=20DMChannel/Messa?= =?UTF-8?q?geReaction#fetch()=20and=20Parti=E2=80=A6=20(#3261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add DMChannel#fetch() & Action#getChannel({recipients}) * ref for MessageReaction partial * typings * add PartialTypes.REACTION * accommodate for fully removed reactions * fix incorrect wording and typo * typings: MessageReaction#count is nullable * typings: mark MessageReaction#partial as readonly Co-Authored-By: Vlad Frangu * fix(User): fetch dm channel if cached one is partial * docs: add missing comma Co-Authored-By: Antonio Román --- docs/topics/partials.md | 10 ++++--- src/client/actions/Action.js | 5 ++-- src/client/actions/MessageReactionAdd.js | 7 ++--- src/stores/ReactionStore.js | 22 +++++++++++++++ src/structures/DMChannel.js | 10 ++++++- src/structures/MessageReaction.js | 34 +++++++++++++++++++----- src/structures/User.js | 2 +- src/util/Constants.js | 2 ++ typings/index.d.ts | 8 ++++-- 9 files changed, 80 insertions(+), 20 deletions(-) diff --git a/docs/topics/partials.md b/docs/topics/partials.md index 2566f3de..c5c7200d 100644 --- a/docs/topics/partials.md +++ b/docs/topics/partials.md @@ -9,8 +9,8 @@ discard the event. With partials, you're able to receive the event, with a Messa Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType): ```js -// Accept partial messages and DM channels when emitting events -new Client({ partials: ['MESSAGE', 'CHANNEL'] }); +// Accept partial messages, DM channels, and reactions when emitting events +new Client({ partials: ['MESSAGE', 'CHANNEL', 'REACTION'] }); ``` ## Usage & warnings @@ -45,6 +45,10 @@ client.on('messageReactionAdd', async (reaction, user) => { if (reaction.message.partial) await reaction.message.fetch(); // Now the message has been cached and is fully available: console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`); + // Fetches and caches the reaction itself, updating resources that were possibly defunct. + if (reaction.partial) await reaction.fetch(); + // Now the reaction is fully available and the properties will be reflected accurately: + console.log(`${reaction.count} user(s) have given the same reaction this message!`); }); ``` @@ -58,4 +62,4 @@ bot or any bot that relies on still receiving updates to resources you don't hav good example. Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should -not be cached. \ No newline at end of file +not be cached. diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 7f21318b..bc9ed267 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -36,6 +36,7 @@ class GenericAction { return data.channel || this.getPayload({ id, guild_id: data.guild_id, + recipients: [data.author || { id: data.user_id }], }, this.client.channels, id, PartialTypes.CHANNEL); } @@ -52,9 +53,9 @@ class GenericAction { const id = data.emoji.id || decodeURIComponent(data.emoji.name); return this.getPayload({ emoji: data.emoji, - count: 0, + count: message.partial ? null : 0, me: user.id === this.client.user.id, - }, message.reactions, id, PartialTypes.MESSAGE); + }, message.reactions, id, PartialTypes.REACTION); } getMember(data, guild) { diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index e7ae7e26..721d9251 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -26,11 +26,8 @@ class MessageReactionAdd extends Action { if (!message) return false; // Verify reaction - const reaction = message.reactions.add({ - emoji: data.emoji, - count: 0, - me: user.id === this.client.user.id, - }); + const reaction = this.getReaction(data, message, user); + if (!reaction) return false; reaction._add(user); /** * Emitted whenever a reaction is added to a cached message. diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index b3910ba4..29b53849 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -50,6 +50,28 @@ class ReactionStore extends DataStore { return this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions.delete() .then(() => this.message); } + + _partial(emoji) { + const id = emoji.id || emoji.name; + const existing = this.get(id); + return !existing || existing.partial; + } + + async _fetchReaction(reactionEmoji, cache) { + const id = reactionEmoji.id || reactionEmoji.name; + const existing = this.get(id); + if (!this._partial(reactionEmoji)) return existing; + const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); + if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { + reactionEmoji.reaction._patch({ count: 0 }); + this.message.reactions.remove(id); + return existing; + } + for (const reaction of data.reactions) { + if (this._partial(reaction.emoji)) this.add(reaction, cache); + } + return existing; + } } module.exports = ReactionStore; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index e172f226..6c034ec9 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -56,7 +56,15 @@ class DMChannel extends Channel { * @readonly */ get partial() { - return !this.recipient; + return this.lastMessageID === undefined; + } + + /** + * Fetch this DMChannel. + * @returns {Promise} + */ + fetch() { + return this.recipient.createDM(); } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index fe10e428..cd242595 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -27,12 +27,6 @@ class MessageReaction { */ this.me = data.me; - /** - * The number of people that have given the same reaction - * @type {number} - */ - this.count = data.count || 0; - /** * The users that have given this reaction, mapped by their ID * @type {ReactionUserStore} @@ -40,6 +34,16 @@ class MessageReaction { this.users = new ReactionUserStore(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji); + + this._patch(data); + } + + _patch(data) { + /** + * The number of people that have given the same reaction + * @type {?number} + */ + this.count = typeof data.count === 'number' ? data.count : null; } /** @@ -63,18 +67,36 @@ class MessageReaction { return this._emoji; } + /** + * Whether or not this reaction is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.count === null; + } + + /** + * Fetch this reaction. + * @returns {Promise} + */ + fetch() { + return this.message.reactions._fetchReaction(this.emoji, true); + } toJSON() { return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' }); } _add(user) { + if (this.partial) return; this.users.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me) this.me = user.id === this.message.client.user.id; } _remove(user) { + if (this.partial) return; this.users.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; diff --git a/src/structures/User.js b/src/structures/User.js index c2276c4f..4425850f 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -211,7 +211,7 @@ class User extends Base { */ async createDM() { const { dmChannel } = this; - if (dmChannel) return dmChannel; + if (dmChannel && !dmChannel.partial) return dmChannel; const data = await this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, } }); diff --git a/src/util/Constants.js b/src/util/Constants.js index 23591328..89b14c6a 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -287,6 +287,7 @@ exports.ShardEvents = { * * CHANNEL (only affects DMChannels) * * GUILD_MEMBER * * MESSAGE + * * REACTION * Partials require you to put checks in place when handling data, read the Partials topic listed in the * sidebar for more information. * @typedef {string} PartialType @@ -296,6 +297,7 @@ exports.PartialTypes = keyMirror([ 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', + 'REACTION', ]); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index af240084..28946efa 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -653,6 +653,7 @@ declare module 'discord.js' { public messages: MessageStore; public recipient: User; public readonly partial: boolean; + public fetch(): Promise; } export class Emoji extends Base { @@ -1091,11 +1092,13 @@ declare module 'discord.js' { constructor(client: Client, data: object, message: Message); private _emoji: GuildEmoji | ReactionEmoji; - public count: number; + public count: number | null; public readonly emoji: GuildEmoji | ReactionEmoji; public me: boolean; public message: Message; + public readonly partial: boolean; public users: ReactionUserStore; + public fetch(): Promise; public toJSON(): object; } @@ -2452,7 +2455,8 @@ declare module 'discord.js' { type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' - | 'MESSAGE'; + | 'MESSAGE' + | 'REACTION'; type PresenceStatus = ClientPresenceStatus | 'offline'; From 37ecf7b826db002875ff578e31bc9ac001b14882 Mon Sep 17 00:00:00 2001 From: newt Date: Tue, 10 Sep 2019 15:12:27 +0100 Subject: [PATCH 262/428] feat(constants): add verificationLevels (#3369) * add Util.parseVerification() * Made the code much cleaner. * Removed method and created constant. * Lint! * refactor(constants): capitalize VerficiationLevels and add a typedef * Changed VerificationLevels typedef to singular. Co-Authored-By: Will Nelson --- src/util/Constants.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index 89b14c6a..ab891eb8 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -467,6 +467,23 @@ exports.Colors = { NOT_QUITE_BLACK: 0x23272A, }; +/** + * The value set for the verification levels for a guild: + * * None + * * Low + * * Medium + * * (╯°□°)╯︵ ┻━┻ + * * ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ + * @typedef {string} VerificationLevel + */ +exports.VerificationLevels = [ + 'None', + 'Low', + 'Medium', + '(╯°□°)╯︵ ┻━┻', + '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', +]; + /** * An error encountered while performing an API request. Here are the potential errors: * * UNKNOWN_ACCOUNT From 8dec251d3c28f1aa273e88085bff772cc56aef53 Mon Sep 17 00:00:00 2001 From: Charalampos Fanoulis Date: Tue, 10 Sep 2019 18:14:07 +0300 Subject: [PATCH 263/428] github: add support file and redirect to it (#3302) * github: add support file and redirect to it * github: happily accept that suggestion --- .../ISSUE_TEMPLATE/question---general-support-request.md | 5 +---- .github/SUPPORT.md | 7 +++++++ 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .github/SUPPORT.md diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md index d2cef51f..d48d4940 100644 --- a/.github/ISSUE_TEMPLATE/question---general-support-request.md +++ b/.github/ISSUE_TEMPLATE/question---general-support-request.md @@ -9,8 +9,5 @@ assignees: '' Seriously, we only use this issue tracker for bugs in the library itself and feature requests for it. We don't typically answer questions or help with support issues here. -Instead, please ask in one of the support channels in our Discord server: -https://discord.gg/bRCvFy9 - -Any issues that don't directly involve a bug in the library or a feature request will likely be closed and redirected to the Discord server. +If you have a question or need support on our library, please read our [Support Document](https://github.com/discordjs/discord.js/blob/master/.github/SUPPORT.md) diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 00000000..032cbc2b --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,7 @@ +# Seeking support? + +We're sorry, we only use this issue tracker for bugs in the library itself and feature requests for it. We are not able to provide general support or answser questions on the issue tracker. + +Should you want to ask such questions, please post in one of our support channels in our Discord server: https://discord.gg/bRCvFy9 + +Any issues that don't directly involve a bug in the library or a feature request will likely be closed and redirected to the Discord server. From 278f185b645353b1f85991cde50e828178e10715 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 10 Sep 2019 10:29:44 -0500 Subject: [PATCH 264/428] =?UTF-8?q?fix(rate-limits):=20reactions=20buckets?= =?UTF-8?q?=20need=20to=20be=20shared=20with=20sub-=E2=80=A6=20(#3439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rest/APIRouter.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 715b1c7c..4d1fb5d6 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -13,13 +13,18 @@ function buildRoute(manager) { get(target, name) { if (reflectors.includes(name)) return () => route.join('/'); if (methods.includes(name)) { + const routeBucket = []; + for (let i = 0; i < route.length; i++) { + // Reactions routes and sub-routes all share the same bucket + if (route[i - 1] === 'reactions') break; + // Literal IDs should only be taken account if they are the Major ID (the Channel/Guild ID) + if (/\d{16,19}/g.test(route[i]) && !/channels|guilds/.test(route[i - 1])) routeBucket.push(':id'); + // All other parts of the route should be considered as part of the bucket identifier + else routeBucket.push(route[i]); + } return options => manager.request(name, route.join('/'), Object.assign({ versioned: manager.versioned, - route: route.map((r, i) => { - if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id'; - if (route[i - 1] === 'reactions') return ':reaction'; - return r; - }).join('/'), + route: routeBucket.join('/'), }, options)); } route.push(name); From dad0cd8e81e3f530d5e0dda1ea45359c602372ca Mon Sep 17 00:00:00 2001 From: 1Computer1 Date: Tue, 10 Sep 2019 11:44:47 -0400 Subject: [PATCH 265/428] feat: external collection package (#2934) * Use external collection package * Complete typings * Document properly base collection class * Add clone since sort is now in-place * Update for latest changes to package * Fix whitespace * Update docs link * Oops * Update Collection.js * Update index.d.ts --- package.json | 1 + src/util/Collection.js | 417 +---------------------------------------- src/util/Util.js | 2 +- typings/index.d.ts | 35 +--- 4 files changed, 12 insertions(+), 443 deletions(-) diff --git a/package.json b/package.json index 626fea20..60297394 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { + "@discordjs/collection": "^0.1.0", "abort-controller": "^3.0.0", "form-data": "^2.3.3", "node-fetch": "^2.3.0", diff --git a/src/util/Collection.js b/src/util/Collection.js index 3d01ffef..13cc0889 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -1,423 +1,22 @@ 'use strict'; +const BaseCollection = require('@discordjs/collection'); const Util = require('./Util'); /** * A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has * an ID, for significantly improved performance and ease-of-use. - * @extends {Map} + * @extends {BaseCollection} */ -class Collection extends Map { - constructor(iterable) { - super(iterable); - - /** - * Cached array for the `array()` method - will be reset to `null` whenever `set()` or `delete()` are called - * @name Collection#_array - * @type {?Array} - * @private - */ - Object.defineProperty(this, '_array', { value: null, writable: true, configurable: true }); - - /** - * Cached array for the `keyArray()` method - will be reset to `null` whenever `set()` or `delete()` are called - * @name Collection#_keyArray - * @type {?Array} - * @private - */ - Object.defineProperty(this, '_keyArray', { value: null, writable: true, configurable: true }); - } - - set(key, val) { - this._array = null; - this._keyArray = null; - return super.set(key, val); - } - - delete(key) { - this._array = null; - this._keyArray = null; - return super.delete(key); - } - - /** - * 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 change the length of the array - * itself. If you don't want this caching behavior, use `[...collection.values()]` or - * `Array.from(collection.values())` instead. - * @returns {Array} - */ - array() { - if (!this._array || this._array.length !== this.size) this._array = [...this.values()]; - return this._array; - } - - /** - * 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 change the length of the array - * itself. If you don't want this caching behavior, use `[...collection.keys()]` or - * `Array.from(collection.keys())` instead. - * @returns {Array} - */ - keyArray() { - if (!this._keyArray || this._keyArray.length !== this.size) this._keyArray = [...this.keys()]; - return this._keyArray; - } - - /** - * Obtains the first value(s) in this collection. - * @param {number} [amount] Amount of values to obtain from the beginning - * @returns {*|Array<*>} A single value if no amount is provided or an array of values, starting from the end if - * amount is negative - */ - first(amount) { - if (typeof amount === 'undefined') return this.values().next().value; - if (amount < 0) return this.last(amount * -1); - amount = Math.min(this.size, amount); - const arr = new Array(amount); - const iter = this.values(); - for (let i = 0; i < amount; i++) arr[i] = iter.next().value; - return arr; - } - - /** - * Obtains the first key(s) in this collection. - * @param {number} [amount] Amount of keys to obtain from the beginning - * @returns {*|Array<*>} A single key if no amount is provided or an array of keys, starting from the end if - * amount is negative - */ - firstKey(amount) { - if (typeof amount === 'undefined') return this.keys().next().value; - if (amount < 0) return this.lastKey(amount * -1); - amount = Math.min(this.size, amount); - const arr = new Array(amount); - const iter = this.keys(); - for (let i = 0; i < amount; i++) arr[i] = iter.next().value; - return arr; - } - - /** - * Obtains the last value(s) in this collection. This relies on {@link Collection#array}, and thus the caching - * mechanism applies here as well. - * @param {number} [amount] Amount of values to obtain from the end - * @returns {*|Array<*>} A single value if no amount is provided or an array of values, starting from the start if - * amount is negative - */ - last(amount) { - const arr = this.array(); - if (typeof amount === 'undefined') return arr[arr.length - 1]; - if (amount < 0) return this.first(amount * -1); - if (!amount) return []; - return arr.slice(-amount); - } - - /** - * Obtains the last key(s) in this collection. This relies on {@link Collection#keyArray}, and thus the caching - * mechanism applies here as well. - * @param {number} [amount] Amount of keys to obtain from the end - * @returns {*|Array<*>} A single key if no amount is provided or an array of keys, starting from the start if - * amount is negative - */ - lastKey(amount) { - const arr = this.keyArray(); - if (typeof amount === 'undefined') return arr[arr.length - 1]; - if (amount < 0) return this.firstKey(amount * -1); - if (!amount) return []; - return arr.slice(-amount); - } - - /** - * Obtains unique random value(s) from this collection. This relies on {@link Collection#array}, and thus the caching - * mechanism applies here as well. - * @param {number} [amount] Amount of values to obtain randomly - * @returns {*|Array<*>} A single value if no amount is provided or an array of values - */ - random(amount) { - let arr = this.array(); - if (typeof amount === 'undefined') return arr[Math.floor(Math.random() * arr.length)]; - if (arr.length === 0 || !amount) return []; - const rand = new Array(amount); - arr = arr.slice(); - for (let i = 0; i < amount; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; - return rand; - } - - /** - * Obtains unique random key(s) from this collection. This relies on {@link Collection#keyArray}, and thus the caching - * mechanism applies here as well. - * @param {number} [amount] Amount of keys to obtain randomly - * @returns {*|Array<*>} A single key if no amount is provided or an array - */ - randomKey(amount) { - let arr = this.keyArray(); - if (typeof amount === 'undefined') return arr[Math.floor(Math.random() * arr.length)]; - if (arr.length === 0 || !amount) return []; - const rand = new Array(amount); - arr = arr.slice(); - for (let i = 0; i < amount; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; - return rand; - } - - /** - * Searches for a single item where the given function returns a truthy value. This behaves like - * [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find). - * All collections used in Discord.js are mapped using their `id` property, and if you want to find by id you - * should use the `get` method. See - * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details. - * @param {Function} fn The function to test with (should return boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {*} - * @example collection.find(user => user.username === 'Bob'); - */ - find(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - for (const [key, val] of this) { - if (fn(val, key, this)) return val; - } - return undefined; - } - - /* eslint-disable max-len */ - /** - * Searches for the key of a single item where the given function returns a truthy value. This behaves like - * [Array.findIndex()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex), - * but returns the key rather than the positional index. - * @param {Function} fn The function to test with (should return boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {*} - * @example collection.findKey(user => user.username === 'Bob'); - */ - findKey(fn, thisArg) { - /* eslint-enable max-len */ - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - for (const [key, val] of this) { - if (fn(val, key, this)) return key; - } - return undefined; - } - - /** - * Removes entries that satisfy the provided filter function. - * @param {Function} fn Function used to test (should return a boolean) - * @param {Object} [thisArg] Value to use as `this` when executing function - * @returns {number} The number of removed entries - */ - sweep(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const previousSize = this.size; - for (const [key, val] of this) { - if (fn(val, key, this)) this.delete(key); - } - return previousSize - this.size; - } - - /** - * Identical to - * [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter), - * but returns a Collection instead of an Array. - * @param {Function} fn The function to test with (should return boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Collection} - * @example collection.filter(user => user.username === 'Bob'); - */ - filter(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = new this.constructor[Symbol.species](); - for (const [key, val] of this) { - if (fn(val, key, this)) results.set(key, val); - } - return results; - } - - /** - * Partitions the collection into two collections where the first collection - * contains the items that passed and the second contains the items that failed. - * @param {Function} fn Function used to test (should return a boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Collection[]} - * @example const [big, small] = collection.partition(guild => guild.memberCount > 250); - */ - partition(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = [new this.constructor[Symbol.species](), new this.constructor[Symbol.species]()]; - for (const [key, val] of this) { - if (fn(val, key, this)) { - results[0].set(key, val); - } else { - results[1].set(key, val); - } - } - return results; - } - - /** - * Maps each item to another value. Identical in behavior to - * [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). - * @param {Function} fn Function that produces an element of the new array, taking three arguments - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Array} - * @example collection.map(user => user.tag); - */ - map(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const arr = new Array(this.size); - let i = 0; - for (const [key, val] of this) arr[i++] = fn(val, key, this); - return arr; - } - - /** - * Checks if there exists an item that passes a test. Identical in behavior to - * [Array.some()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some). - * @param {Function} fn Function used to test (should return a boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {boolean} - * @example collection.some(user => user.discriminator === '0000'); - */ - some(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - for (const [key, val] of this) { - if (fn(val, key, this)) return true; - } - return false; - } - - /** - * Checks if all items passes a test. Identical in behavior to - * [Array.every()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every). - * @param {Function} fn Function used to test (should return a boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {boolean} - * @example collection.every(user => !user.bot); - */ - every(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - for (const [key, val] of this) { - if (!fn(val, key, this)) return false; - } - return true; - } - - /** - * Applies a function to produce a single value. Identical in behavior to - * [Array.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce). - * @param {Function} fn Function used to reduce, taking four arguments; `accumulator`, `currentValue`, `currentKey`, - * and `collection` - * @param {*} [initialValue] Starting value for the accumulator - * @returns {*} - * @example collection.reduce((acc, guild) => acc + guild.memberCount, 0); - */ - reduce(fn, initialValue) { - let accumulator; - if (typeof initialValue !== 'undefined') { - accumulator = initialValue; - for (const [key, val] of this) accumulator = fn(accumulator, val, key, this); - } else { - let first = true; - for (const [key, val] of this) { - if (first) { - accumulator = val; - first = false; - continue; - } - accumulator = fn(accumulator, val, key, this); - } - } - return accumulator; - } - - /** - * Identical to - * [Map.forEach()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach), - * but returns the collection instead of undefined. - * @param {Function} fn Function to execute for each element - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Collection} - * @example - * collection - * .each(user => console.log(user.username)) - * .filter(user => user.bot) - * .each(user => console.log(user.username)); - */ - each(fn, thisArg) { - this.forEach(fn, thisArg); - return this; - } - - /** - * Runs a function on the collection and returns the collection. - * @param {Function} fn Function to execute - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Collection} - * @example - * collection - * .tap(coll => console.log(`${coll.size} users, including bots`)) - * .filter(user => user.bot) - * .tap(coll => console.log(`${coll.size} users, excluding bots`)) - */ - tap(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - fn(this); - return this; - } - - /** - * Creates an identical shallow copy of this collection. - * @returns {Collection} - * @example const newColl = someColl.clone(); - */ - clone() { - return new this.constructor[Symbol.species](this); - } - - /** - * Combines this collection with others into a new collection. None of the source collections are modified. - * @param {...Collection} collections Collections to merge - * @returns {Collection} - * @example const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl); - */ - concat(...collections) { - const newColl = this.clone(); - for (const coll of collections) { - for (const [key, val] of coll) newColl.set(key, val); - } - return newColl; - } - - /** - * Checks if this collection shares identical key-value pairings with another. - * This is different to checking for equality using equal-signs, because - * the collections may be different objects, but contain the same data. - * @param {Collection} collection Collection to compare with - * @returns {boolean} Whether the collections have identical contents - */ - equals(collection) { - if (!collection) return false; - if (this === collection) return true; - if (this.size !== collection.size) return false; - return !this.find((value, key) => { - const testVal = collection.get(key); - return testVal !== value || (testVal === undefined && !collection.has(key)); - }); - } - - /** - * The sort() method sorts the elements of a collection and returns it. - * The sort is not necessarily stable. The default sort order is according to string Unicode code points. - * @param {Function} [compareFunction] Specifies a function that defines the sort order. - * If omitted, the collection is sorted according to each character's Unicode code point value, - * according to the string conversion of each element. - * @returns {Collection} - * @example collection.sort((userA, userB) => userA.createdTimestamp - userB.createdTimestamp); - */ - sort(compareFunction = (x, y) => +(x > y) || +(x === y) - 1) { - return new this.constructor[Symbol.species]([...this.entries()] - .sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); - } - +class Collection extends BaseCollection { toJSON() { return this.map(e => typeof e.toJSON === 'function' ? e.toJSON() : Util.flatten(e)); } } module.exports = Collection; + +/** + * @external BaseCollection + * @see {@link https://discord.js.org/#/docs/collection/} + */ diff --git a/src/util/Util.js b/src/util/Util.js index f0079a2a..6087f3c2 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -432,7 +432,7 @@ class Util { * @returns {Collection} */ static discordSort(collection) { - return collection.sort((a, b) => + return collection.sorted((a, b) => a.rawPosition - b.rawPosition || parseInt(b.id.slice(0, -10)) - parseInt(a.id.slice(0, -10)) || parseInt(b.id.slice(10)) - parseInt(a.id.slice(10)) diff --git a/typings/index.d.ts b/typings/index.d.ts index 28946efa..4a6896c1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,4 +1,5 @@ declare module 'discord.js' { + import BaseCollection from '@discord.js/collection'; import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; @@ -311,39 +312,7 @@ declare module 'discord.js' { public setUsername(username: string): Promise; } - export class Collection extends Map { - private _array: V[]; - private _keyArray: K[]; - - public array(): V[]; - public clone(): Collection; - public concat(...collections: Collection[]): Collection; - public each(fn: (value: V, key: K, collection: Collection) => void, thisArg?: any): Collection; - public equals(collection: Collection): boolean; - public every(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; - public filter(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): Collection; - public find(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): V | undefined; - public findKey(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): K | undefined; - public first(): V | undefined; - public first(count: number): V[]; - public firstKey(): K | undefined; - public firstKey(count: number): K[]; - public keyArray(): K[]; - public last(): V | undefined; - public last(count: number): V[]; - public lastKey(): K | undefined; - public lastKey(count: number): K[]; - public map(fn: (value: V, key: K, collection: Collection) => T, thisArg?: any): T[]; - public partition(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): [Collection, Collection]; - public random(): V | undefined; - public random(count: number): V[]; - public randomKey(): K | undefined; - public randomKey(count: number): K[]; - public reduce(fn: (accumulator: T, value: V, key: K, collection: Collection) => T, initialValue?: T): T; - public some(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; - public sort(compareFunction?: (a: V, b: V, c?: K, d?: K) => number): Collection; - public sweep(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): number; - public tap(fn: (collection: Collection) => void, thisArg?: any): Collection; + export class Collection extends BaseCollection { public toJSON(): object; } From 321beb73bd92c939fae701cde774c238a98b613a Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 10 Sep 2019 19:49:56 +0200 Subject: [PATCH 266/428] =?UTF-8?q?revert:=20"feat(Partials):=20add=20DMCh?= =?UTF-8?q?annel/MessageReaction#fetch()=E2=80=A6=20(#3468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b0047c424b5417287d2ea64ed84538931935b698. --- docs/topics/partials.md | 10 +++---- src/client/actions/Action.js | 5 ++-- src/client/actions/MessageReactionAdd.js | 7 +++-- src/stores/ReactionStore.js | 22 --------------- src/structures/DMChannel.js | 10 +------ src/structures/MessageReaction.js | 34 +++++------------------- src/structures/User.js | 2 +- src/util/Constants.js | 2 -- typings/index.d.ts | 8 ++---- 9 files changed, 20 insertions(+), 80 deletions(-) diff --git a/docs/topics/partials.md b/docs/topics/partials.md index c5c7200d..2566f3de 100644 --- a/docs/topics/partials.md +++ b/docs/topics/partials.md @@ -9,8 +9,8 @@ discard the event. With partials, you're able to receive the event, with a Messa Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType): ```js -// Accept partial messages, DM channels, and reactions when emitting events -new Client({ partials: ['MESSAGE', 'CHANNEL', 'REACTION'] }); +// Accept partial messages and DM channels when emitting events +new Client({ partials: ['MESSAGE', 'CHANNEL'] }); ``` ## Usage & warnings @@ -45,10 +45,6 @@ client.on('messageReactionAdd', async (reaction, user) => { if (reaction.message.partial) await reaction.message.fetch(); // Now the message has been cached and is fully available: console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`); - // Fetches and caches the reaction itself, updating resources that were possibly defunct. - if (reaction.partial) await reaction.fetch(); - // Now the reaction is fully available and the properties will be reflected accurately: - console.log(`${reaction.count} user(s) have given the same reaction this message!`); }); ``` @@ -62,4 +58,4 @@ bot or any bot that relies on still receiving updates to resources you don't hav good example. Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should -not be cached. +not be cached. \ No newline at end of file diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index bc9ed267..7f21318b 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -36,7 +36,6 @@ class GenericAction { return data.channel || this.getPayload({ id, guild_id: data.guild_id, - recipients: [data.author || { id: data.user_id }], }, this.client.channels, id, PartialTypes.CHANNEL); } @@ -53,9 +52,9 @@ class GenericAction { const id = data.emoji.id || decodeURIComponent(data.emoji.name); return this.getPayload({ emoji: data.emoji, - count: message.partial ? null : 0, + count: 0, me: user.id === this.client.user.id, - }, message.reactions, id, PartialTypes.REACTION); + }, message.reactions, id, PartialTypes.MESSAGE); } getMember(data, guild) { diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 721d9251..e7ae7e26 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -26,8 +26,11 @@ class MessageReactionAdd extends Action { if (!message) return false; // Verify reaction - const reaction = this.getReaction(data, message, user); - if (!reaction) return false; + const reaction = message.reactions.add({ + emoji: data.emoji, + count: 0, + me: user.id === this.client.user.id, + }); reaction._add(user); /** * Emitted whenever a reaction is added to a cached message. diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index 29b53849..b3910ba4 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -50,28 +50,6 @@ class ReactionStore extends DataStore { return this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions.delete() .then(() => this.message); } - - _partial(emoji) { - const id = emoji.id || emoji.name; - const existing = this.get(id); - return !existing || existing.partial; - } - - async _fetchReaction(reactionEmoji, cache) { - const id = reactionEmoji.id || reactionEmoji.name; - const existing = this.get(id); - if (!this._partial(reactionEmoji)) return existing; - const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); - if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { - reactionEmoji.reaction._patch({ count: 0 }); - this.message.reactions.remove(id); - return existing; - } - for (const reaction of data.reactions) { - if (this._partial(reaction.emoji)) this.add(reaction, cache); - } - return existing; - } } module.exports = ReactionStore; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 6c034ec9..e172f226 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -56,15 +56,7 @@ class DMChannel extends Channel { * @readonly */ get partial() { - return this.lastMessageID === undefined; - } - - /** - * Fetch this DMChannel. - * @returns {Promise} - */ - fetch() { - return this.recipient.createDM(); + return !this.recipient; } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index cd242595..fe10e428 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -27,6 +27,12 @@ class MessageReaction { */ this.me = data.me; + /** + * The number of people that have given the same reaction + * @type {number} + */ + this.count = data.count || 0; + /** * The users that have given this reaction, mapped by their ID * @type {ReactionUserStore} @@ -34,16 +40,6 @@ class MessageReaction { this.users = new ReactionUserStore(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji); - - this._patch(data); - } - - _patch(data) { - /** - * The number of people that have given the same reaction - * @type {?number} - */ - this.count = typeof data.count === 'number' ? data.count : null; } /** @@ -67,36 +63,18 @@ class MessageReaction { return this._emoji; } - /** - * Whether or not this reaction is a partial - * @type {boolean} - * @readonly - */ - get partial() { - return this.count === null; - } - - /** - * Fetch this reaction. - * @returns {Promise} - */ - fetch() { - return this.message.reactions._fetchReaction(this.emoji, true); - } toJSON() { return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' }); } _add(user) { - if (this.partial) return; this.users.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me) this.me = user.id === this.message.client.user.id; } _remove(user) { - if (this.partial) return; this.users.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; diff --git a/src/structures/User.js b/src/structures/User.js index 4425850f..c2276c4f 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -211,7 +211,7 @@ class User extends Base { */ async createDM() { const { dmChannel } = this; - if (dmChannel && !dmChannel.partial) return dmChannel; + if (dmChannel) return dmChannel; const data = await this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, } }); diff --git a/src/util/Constants.js b/src/util/Constants.js index ab891eb8..9d742e78 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -287,7 +287,6 @@ exports.ShardEvents = { * * CHANNEL (only affects DMChannels) * * GUILD_MEMBER * * MESSAGE - * * REACTION * Partials require you to put checks in place when handling data, read the Partials topic listed in the * sidebar for more information. * @typedef {string} PartialType @@ -297,7 +296,6 @@ exports.PartialTypes = keyMirror([ 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', - 'REACTION', ]); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 4a6896c1..e56769f3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -622,7 +622,6 @@ declare module 'discord.js' { public messages: MessageStore; public recipient: User; public readonly partial: boolean; - public fetch(): Promise; } export class Emoji extends Base { @@ -1061,13 +1060,11 @@ declare module 'discord.js' { constructor(client: Client, data: object, message: Message); private _emoji: GuildEmoji | ReactionEmoji; - public count: number | null; + public count: number; public readonly emoji: GuildEmoji | ReactionEmoji; public me: boolean; public message: Message; - public readonly partial: boolean; public users: ReactionUserStore; - public fetch(): Promise; public toJSON(): object; } @@ -2424,8 +2421,7 @@ declare module 'discord.js' { type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' - | 'MESSAGE' - | 'REACTION'; + | 'MESSAGE'; type PresenceStatus = ClientPresenceStatus | 'offline'; From ac44a7fc5717e4dfad52285658e2c529fcc60875 Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 10 Sep 2019 20:34:47 +0200 Subject: [PATCH 267/428] fix(typings): collections import --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index e56769f3..501d54e9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,5 @@ declare module 'discord.js' { - import BaseCollection from '@discord.js/collection'; + import BaseCollection from '@discordjs/collection'; import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; From 33ecdd4c5039bc2e2c50f8f5a24783e2df21e4f7 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Wed, 11 Sep 2019 01:58:12 +0200 Subject: [PATCH 268/428] fix(typings): collection constructor --- package.json | 2 +- typings/index.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 60297394..e89ca4fc 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { - "@discordjs/collection": "^0.1.0", + "@discordjs/collection": "^0.1.1", "abort-controller": "^3.0.0", "form-data": "^2.3.3", "node-fetch": "^2.3.0", diff --git a/typings/index.d.ts b/typings/index.d.ts index 501d54e9..c694fa26 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,5 @@ declare module 'discord.js' { - import BaseCollection from '@discordjs/collection'; + import BaseCollection, { CollectionConstructor } from '@discordjs/collection'; import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; @@ -1684,7 +1684,7 @@ declare module 'discord.js' { export class DataStore, R = any> extends Collection { constructor(client: Client, iterable: Iterable, holds: VConstructor); - public static readonly [Symbol.species]: typeof Collection; + public static readonly [Symbol.species]: typeof CollectionConstructor; public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; From ea9e1441905e67e2627a6f5e80a8d989d01376c5 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Wed, 11 Sep 2019 02:11:31 +0200 Subject: [PATCH 269/428] fix(typings): remove leftover typeof --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c694fa26..d71dde4d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1684,7 +1684,7 @@ declare module 'discord.js' { export class DataStore, R = any> extends Collection { constructor(client: Client, iterable: Iterable, holds: VConstructor); - public static readonly [Symbol.species]: typeof CollectionConstructor; + public static readonly [Symbol.species]: CollectionConstructor; public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; From d5831df7b18e3b71c9ad7057b85fc3c84aa7c609 Mon Sep 17 00:00:00 2001 From: 1Computer1 Date: Tue, 17 Sep 2019 18:53:00 -0400 Subject: [PATCH 270/428] fix: typings for this-polymorphism of collections (#3472) * Fix typings for this-polymorphism of collections * Update index.d.ts Co-authored-by: Crawl --- typings/index.d.ts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index d71dde4d..811aa78a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,5 @@ declare module 'discord.js' { - import BaseCollection, { CollectionConstructor } from '@discordjs/collection'; + import BaseCollection from '@discordjs/collection'; import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; @@ -313,6 +313,12 @@ declare module 'discord.js' { } export class Collection extends BaseCollection { + public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; + public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; + public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; + public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; + public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; + public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; public toJSON(): object; } @@ -1684,13 +1690,37 @@ declare module 'discord.js' { export class DataStore, R = any> extends Collection { constructor(client: Client, iterable: Iterable, holds: VConstructor); - public static readonly [Symbol.species]: CollectionConstructor; public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; public remove(key: K): void; public resolve(resolvable: R): V | null; public resolveID(resolvable: R): K | null; + // Don't worry about those bunch of ts-ignores here, this is intended https://github.com/microsoft/TypeScript/issues/1213 + // @ts-ignore + public filter(fn: (value: V, key: K, collection: this) => boolean): Collection; + // @ts-ignore + public filter(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): Collection; + // @ts-ignore + public filter(fn: (value: V, key: K, collection: this) => boolean, thisArg?: unknown): Collection; + // @ts-ignore + public partition(fn: (value: V, key: K, collection: this) => boolean): [Collection, Collection]; + // @ts-ignore + public partition(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): [Collection, Collection]; + // @ts-ignore + public partition(fn: (value: V, key: K, collection: this) => boolean, thisArg?: unknown): [Collection, Collection]; + public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; + public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; + public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; + public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; + public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; + public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; + // @ts-ignore + public clone(): Collection; + // @ts-ignore + public concat(...collections: Collection[]): Collection; + // @ts-ignore + public sorted(compareFunction: (firstValue: V, secondValue: V, firstKey: K, secondKey: K) => number): Collection; } export class GuildEmojiRoleStore extends OverridableDataStore { From d05334fd3cd8fb913e869654f25eb8230493de11 Mon Sep 17 00:00:00 2001 From: BadCoder1337 Date: Fri, 20 Sep 2019 00:01:08 +0300 Subject: [PATCH 271/428] typings: make TypeScript interaction with channels better (#3469) * make channel returning methods generic * channel's type inference from kind of channel * add extends * rename generic due to name convention * edit overload * Update index.d.ts * Update index.d.ts * Update index.d.ts Co-authored-by: Crawl Co-authored-by: SpaceEEC --- typings/index.d.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 811aa78a..fdf1a5b8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -789,20 +789,20 @@ declare module 'discord.js' { public readonly position: number; public rawPosition: number; public readonly viewable: boolean; - public clone(options?: GuildChannelCloneOptions): Promise; + public clone(options?: GuildChannelCloneOptions): Promise; public createInvite(options?: InviteOptions): Promise; - public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; - public edit(data: ChannelData, reason?: string): Promise; + public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; + public edit(data: ChannelData, reason?: string): Promise; public equals(channel: GuildChannel): boolean; public fetchInvites(): Promise>; - public lockPermissions(): Promise; - public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; + public lockPermissions(): Promise; + public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; - public setName(name: string, reason?: string): Promise; - public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; - public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; - public setTopic(topic: string, reason?: string): Promise; - public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; + public setName(name: string, reason?: string): Promise; + public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; + public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; + public setTopic(topic: string, reason?: string): Promise; + public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } export class StoreChannel extends GuildChannel { @@ -1738,7 +1738,10 @@ declare module 'discord.js' { export class GuildChannelStore extends DataStore { constructor(guild: Guild, iterable?: Iterable); - public create(name: string, options?: GuildCreateChannelOptions): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'voice' }): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'category' }): Promise; + public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text' }): Promise; + public create(name: string, options: GuildCreateChannelOptions): Promise; } // Hacky workaround because changing the signature of an overridden method errors From 60f89bd96f7e110617af021642a810e7fb71e694 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Mon, 23 Sep 2019 07:50:41 +1000 Subject: [PATCH 272/428] fix(stores): Add symbol.species for not-actual-stores (#3477) * fix(stores): Add symbol.species for not-actual-stores * Linting fixes --- src/stores/GuildEmojiRoleStore.js | 4 ++++ src/stores/GuildMemberRoleStore.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js index ad3793b2..6db2003e 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/stores/GuildEmojiRoleStore.js @@ -105,6 +105,10 @@ class GuildEmojiRoleStore extends Collection { valueOf() { return this._filtered; } + + static get [Symbol.species]() { + return Collection; + } } Util.mixin(GuildEmojiRoleStore, ['set']); diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 4c63bed8..047b8086 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -154,6 +154,10 @@ class GuildMemberRoleStore extends Collection { valueOf() { return this._filtered; } + + static get [Symbol.species]() { + return Collection; + } } Util.mixin(GuildMemberRoleStore, ['set']); From d280d1b03f595f1c4464ab1a8ba20cf74088ec2d Mon Sep 17 00:00:00 2001 From: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Date: Tue, 24 Sep 2019 11:36:19 -0400 Subject: [PATCH 273/428] style: change let to const in MessageMentions (#3483) --- src/structures/MessageMentions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index b4414a3b..610c8c12 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -47,7 +47,7 @@ class MessageMentions { } else { this.users = new Collection(); for (const mention of users) { - let user = message.client.users.add(mention); + const user = message.client.users.add(mention); this.users.set(user.id, user); } } From ea8b4e73559d118a826ac3b242c1a3626b4f6d29 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 26 Sep 2019 00:28:12 +0300 Subject: [PATCH 274/428] docs/typings: Rename LURKABLE to PUBLIC and update GuildFeatures type (#3484) * docs: Rename LURKABLE to PUBLIC * typings: Update GuildFeatures type --- src/structures/Guild.js | 2 +- typings/index.d.ts | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9de57145..75925663 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -154,7 +154,7 @@ class Guild extends Base { * * DISCOVERABLE * * FEATURABLE * * INVITE_SPLASH - * * LURKABLE + * * PUBLIC * * NEWS * * PARTNERED * * VANITY_URL diff --git a/typings/index.d.ts b/typings/index.d.ts index fdf1a5b8..4f03ef11 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2235,13 +2235,18 @@ declare module 'discord.js' { channel: GuildChannelResolvable | null; } - type GuildFeatures = 'INVITE_SPLASH' - | 'MORE_EMOJI' - | 'VERIFIED' - | 'VIP_REGIONS' - | 'VANITY_URL' + type GuildFeatures = 'ANIMATED_ICON' + | 'BANNER' + | 'COMMERCE' | 'DISCOVERABLE' - | 'FEATURABLE'; + | 'FEATURABLE' + | 'INVITE_SPLASH' + | 'PUBLIC' + | 'NEWS' + | 'PARTNERED' + | 'VANITY_URL' + | 'VERIFIED' + | 'VIP_REGIONS'; interface GuildMemberEditData { nick?: string; From 41c0dd44eb53c8048c40e9ab5be5530d6330fb81 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 1 Oct 2019 09:46:49 +0100 Subject: [PATCH 275/428] fix(BitField): throw when resolving invalid string constant Checked to see if the permission actually exists. --- src/util/BitField.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/BitField.js b/src/util/BitField.js index 757da7ab..f849e7b8 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -146,7 +146,7 @@ class BitField { if (typeof bit === 'number' && bit >= 0) return bit; if (bit instanceof BitField) return bit.bitfield; if (Array.isArray(bit)) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); - if (typeof bit === 'string') return this.FLAGS[bit]; + if (typeof bit === 'string' && typeof this.FLAGS[bit] !== 'undefined') return this.FLAGS[bit]; throw new RangeError('BITFIELD_INVALID'); } } From a03e439d6b5459aaef8671c34f20f193e1248208 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Tue, 1 Oct 2019 18:56:14 +1000 Subject: [PATCH 276/428] fix(GuildChannelStore): default channel type incorrectly set (#3496) --- src/stores/GuildChannelStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 55b9ebb0..552b40f0 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -102,7 +102,7 @@ class GuildChannelStore extends DataStore { data: { name, topic, - type: type ? ChannelTypes[type.toUpperCase()] : 'text', + type: type ? ChannelTypes[type.toUpperCase()] : ChannelTypes.TEXT, nsfw, bitrate, user_limit: userLimit, From a4f06bdffd12e4478ba2862bad8c178839e9fa21 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Tue, 1 Oct 2019 12:01:55 +0300 Subject: [PATCH 277/428] src: support new message fields (#3388) * src: Update channel pattern * src: Remove useless non-capture group * src: it's as though we're starting fresh * src: Bring this up to date for reals now * src: typings and a bug fix * src: Add crossposted channels to message mentions * src: Requested changes and add typings * src: Move Object.keys outside loop * typings: Fix enum being exported when it shouldn't * src: Consistency with roles and users * docs: Correct docstring for MessageFlags#flags * docs: Correct docstring for MessageMentions#crosspostedChannels * docs: Suggestions Co-authored-by: SpaceEEC * src: Reset flags to 0 if no flags are received on MESSAGE_UPDATE --- src/index.js | 1 + src/structures/Message.js | 38 +++++++++++++++++++++--- src/structures/MessageMentions.js | 36 ++++++++++++++++++++++- src/util/Constants.js | 2 ++ src/util/MessageFlags.js | 25 ++++++++++++++++ typings/index.d.ts | 49 +++++++++++++++++++++++++++---- 6 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 src/util/MessageFlags.js diff --git a/src/index.js b/src/index.js index 916056fe..77c56336 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ module.exports = { DataStore: require('./stores/DataStore'), DiscordAPIError: require('./rest/DiscordAPIError'), HTTPError: require('./rest/HTTPError'), + MessageFlags: require('./util/MessageFlags'), Permissions: require('./util/Permissions'), Speaking: require('./util/Speaking'), Snowflake: require('./util/Snowflake'), diff --git a/src/structures/Message.js b/src/structures/Message.js index 5b6cc475..8a9df07e 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -13,6 +13,7 @@ const Permissions = require('../util/Permissions'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); const APIMessage = require('./APIMessage'); +const MessageFlags = require('../util/MessageFlags'); /** * Represents a message on Discord. @@ -81,7 +82,9 @@ class Message extends Base { /** * A random number or string used for checking message delivery - * @type {string} + * This is only received after the message was sent successfully, and + * lost if re-fetched + * @type {?string} */ this.nonce = data.nonce; @@ -89,7 +92,7 @@ class Message extends Base { * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) * @type {boolean} */ - this.system = data.type === 6; + this.system = data.type !== 0; /** * A list of embeds in the message - e.g. YouTube Player @@ -137,7 +140,7 @@ class Message extends Base { * All valid mentions that the message contains * @type {MessageMentions} */ - this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone); + this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone, data.mention_channels); /** * ID of the webhook that sent the message, if applicable @@ -172,6 +175,30 @@ class Message extends Base { } else if (data.member && this.guild && this.author) { this.guild.members.add(Object.assign(data.member, { user: this.author })); } + + /** + * Flags that are applied to the message + * @type {Readonly} + */ + this.flags = new MessageFlags(data.flags).freeze(); + + /** + * Reference data sent in a crossposted message. + * @typedef {Object} MessageReference + * @property {string} channelID ID of the channel the message was crossposted from + * @property {?string} guildID ID of the guild the message was crossposted from + * @property {?string} messageID ID of the message that was crossposted + */ + + /** + * Message reference data + * @type {?MessageReference} + */ + this.reference = data.message_reference ? { + channelID: data.message_reference.channel_id, + guildID: data.message_reference.guild_id, + messageID: data.message_reference.message_id, + } : null; } /** @@ -214,8 +241,11 @@ class Message extends Base { this, 'mentions' in data ? data.mentions : this.mentions.users, 'mentions_roles' in data ? data.mentions_roles : this.mentions.roles, - 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone + 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone, + 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels ); + + this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); } /** diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 610c8c12..9b17685f 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -3,12 +3,13 @@ const Collection = require('../util/Collection'); const Util = require('../util/Util'); const GuildMember = require('./GuildMember'); +const { ChannelTypes } = require('../util/Constants'); /** * Keeps track of mentions in a {@link Message}. */ class MessageMentions { - constructor(message, users, roles, everyone) { + constructor(message, users, roles, everyone, crosspostedChannels) { /** * The client the message is from * @type {Client} @@ -86,6 +87,39 @@ class MessageMentions { * @private */ this._channels = null; + + /** + * Crossposted channel data. + * @typedef {Object} CrosspostedChannel + * @property {string} channelID ID of the mentioned channel + * @property {string} guildID ID of the guild that has the channel + * @property {string} type Type of the channel + * @property {string} name The name of the channel + */ + + if (crosspostedChannels) { + if (crosspostedChannels instanceof Collection) { + /** + * A collection of crossposted channels + * @type {Collection} + */ + this.crosspostedChannels = new Collection(crosspostedChannels); + } else { + this.crosspostedChannels = new Collection(); + const channelTypes = Object.keys(ChannelTypes); + for (const d of crosspostedChannels) { + const type = channelTypes[d.type]; + this.crosspostedChannels.set(d.id, { + channelID: d.id, + guildID: d.guild_id, + type: type ? type.toLowerCase() : 'unknown', + name: d.name, + }); + } + } + } else { + this.crosspostedChannels = new Collection(); + } } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 9d742e78..7bb3bb50 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -386,6 +386,7 @@ exports.WSEvents = keyMirror([ * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 + * * CHANNEL_FOLLOW_ADD * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -401,6 +402,7 @@ exports.MessageTypes = [ 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1', 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', + 'CHANNEL_FOLLOW_ADD', ]; /** diff --git a/src/util/MessageFlags.js b/src/util/MessageFlags.js new file mode 100644 index 00000000..b1c86b7d --- /dev/null +++ b/src/util/MessageFlags.js @@ -0,0 +1,25 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link Message#flags} bitfield. + * @extends {BitField} + */ +class MessageFlags extends BitField {} + +/** + * Numeric message flags. All available properties: + * * `CROSSPOSTED` + * * `IS_CROSSPOST` + * * `SUPPRESS_EMBEDS` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/resources/channel#message-object-message-flags} + */ +MessageFlags.FLAGS = { + CROSSPOSTED: 1 << 0, + IS_CROSSPOST: 1 << 1, + SUPPRESS_EMBEDS: 1 << 2, +}; + +module.exports = MessageFlags; diff --git a/typings/index.d.ts b/typings/index.d.ts index 4f03ef11..cb08e2ff 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,3 +1,14 @@ +declare enum ChannelType { + text, + dm, + voice, + group, + category, + news, + store, + unknown +} + declare module 'discord.js' { import BaseCollection from '@discordjs/collection'; import { EventEmitter } from 'events'; @@ -125,7 +136,7 @@ declare module 'discord.js' { public readonly createdTimestamp: number; public deleted: boolean; public id: Snowflake; - public type: 'dm' | 'text' | 'voice' | 'category' | 'news' | 'store' | 'unknown'; + public type: keyof typeof ChannelType; public delete(reason?: string): Promise; public fetch(): Promise; public toString(): string; @@ -914,12 +925,17 @@ declare module 'discord.js' { public toString(): string; } + export class MessageFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + export class Message extends Base { constructor(client: Client, data: object, channel: TextChannel | DMChannel); private _edits: Message[]; private patch(data: object): void; - public activity: GroupActivity | null; + public activity: MessageActivity | null; public application: ClientApplication | null; public attachments: Collection; public author: User | null; @@ -939,7 +955,7 @@ declare module 'discord.js' { public id: Snowflake; public readonly member: GuildMember | null; public mentions: MessageMentions; - public nonce: string; + public nonce: string | null; public readonly partial: boolean; public readonly pinnable: boolean; public pinned: boolean; @@ -949,6 +965,8 @@ declare module 'discord.js' { public type: MessageType; public readonly url: string; public webhookID: Snowflake | null; + public flags: Readonly; + public reference: MessageReference | null; public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; public delete(options?: { timeout?: number, reason?: string }): Promise; @@ -1054,6 +1072,7 @@ declare module 'discord.js' { public readonly members: Collection | null; public roles: Collection; public users: Collection; + public crosspostedChannels: Collection; public toJSON(): object; public static CHANNELS_PATTERN: RegExp; @@ -1886,6 +1905,10 @@ declare module 'discord.js' { | 'LISTENING' | 'WATCHING'; + type MessageFlagsString = 'CROSSPOSTED' + | 'IS_CROSSPOST' + | 'SUPPRESS_EMBEDS'; + interface APIErrror { UNKNOWN_ACCOUNT: number; UNKNOWN_APPLICATION: number; @@ -2128,11 +2151,17 @@ declare module 'discord.js' { name?: string; } - interface GroupActivity { + interface MessageActivity { partyID: string; type: number; } + interface MessageReference { + channelID: string; + guildID: string; + messageID: string | null; + } + type GuildAuditLogsAction = keyof GuildAuditLogsActions; interface GuildAuditLogsActions { @@ -2196,7 +2225,7 @@ declare module 'discord.js' { interface GuildCreateChannelOptions { permissionOverwrites?: OverwriteResolvable[] | Collection; topic?: string; - type?: 'text' | 'voice' | 'category'; + type?: Exclude; nsfw?: boolean; parent?: ChannelResolvable; bitrate?: number; @@ -2373,7 +2402,8 @@ declare module 'discord.js' { | 'USER_PREMIUM_GUILD_SUBSCRIPTION' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2' - | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3'; + | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3' + | 'CHANNEL_FOLLOW_ADD'; interface OverwriteData { allow?: PermissionResolvable; @@ -2611,5 +2641,12 @@ declare module 'discord.js' { type CloseEvent = { wasClean: boolean; code: number; reason: string; target: WebSocket; }; type ErrorEvent = { error: any, message: string, type: string, target: WebSocket; }; + interface CrosspostedChannel { + channelID: Snowflake; + guildID: Snowflake; + type: keyof typeof ChannelType; + name: string; + } + //#endregion } From e936f071c04557e7349ea193de3a17e1dc963562 Mon Sep 17 00:00:00 2001 From: Souji Date: Wed, 2 Oct 2019 14:26:42 +0200 Subject: [PATCH 278/428] fix(typings): GuildChannel#parentID is nullable (#3508) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index cb08e2ff..0e3e95b3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -794,7 +794,7 @@ declare module 'discord.js' { public readonly members: Collection; public name: string; public readonly parent: CategoryChannel | null; - public parentID: Snowflake; + public parentID: Snowflake | null; public permissionOverwrites: Collection; public readonly permissionsLocked: boolean | null; public readonly position: number; From 345869374845cf4fc4463fd0ac3eea9b582ee1d1 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Fri, 4 Oct 2019 10:19:46 +0100 Subject: [PATCH 279/428] typings: mark GuildMember#nickname as nullable (#3516) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0e3e95b3..1bbb8038 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -851,7 +851,7 @@ declare module 'discord.js' { public joinedTimestamp: number | null; public readonly kickable: boolean; public readonly manageable: boolean; - public nickname: string; + public nickname: string | null; public readonly partial: boolean; public readonly permissions: Readonly; public readonly premiumSince: Date | null; From 48856c08155858ccb5db0f40960846e7f1cee4f9 Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 4 Oct 2019 16:44:04 +0200 Subject: [PATCH 280/428] fix: set messages deleted when their channel is deleted (#3519) --- src/client/actions/ChannelDelete.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index 9fc0e6d9..fdfc3870 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -2,6 +2,7 @@ const Action = require('./Action'); const { Events } = require('../../util/Constants'); +const DMChannel = require('../../structures/DMChannel'); class ChannelDeleteAction extends Action { constructor(client) { @@ -16,6 +17,11 @@ class ChannelDeleteAction extends Action { if (channel) { client.channels.remove(channel.id); channel.deleted = true; + if (channel.messages && !(channel instanceof DMChannel)) { + for (const message of channel.messages.values()) { + message.deleted = true; + } + } /** * Emitted whenever a channel is deleted. * @event Client#channelDelete From a8f06f251f1af87650711d4c163ed32d909d4e18 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Fri, 4 Oct 2019 15:44:35 +0100 Subject: [PATCH 281/428] feat(VoiceState): add VoiceState#streaming (#3521) * feat: add VoiceState#streaming * typings: add VoiceState#streaming --- src/structures/VoiceState.js | 5 +++++ typings/index.d.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 8e2922a9..c60f6ac6 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -53,6 +53,11 @@ class VoiceState extends Base { * @type {?string} */ this.sessionID = data.session_id; + /** + * Whether this member is streaming using "Go Live" + * @type {boolean} + */ + this.streaming = data.self_stream || false; /** * The ID of the voice channel that this member is in * @type {?Snowflake} diff --git a/typings/index.d.ts b/typings/index.d.ts index 1bbb8038..456deee7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1579,6 +1579,7 @@ declare module 'discord.js' { public serverDeaf?: boolean; public serverMute?: boolean; public sessionID?: string; + public streaming: boolean; public readonly speaking: boolean | null; public setDeaf(deaf: boolean, reason?: string): Promise; From a60f8b3d49ffc98061ae4ee51be2092410b81f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Fri, 11 Oct 2019 11:01:16 +0200 Subject: [PATCH 282/428] src(constants): add missing APIErrors (#3531) * src: Updated APIErrors * typings: Updated constants --- src/util/Constants.js | 14 ++++++++++++++ typings/index.d.ts | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index 7bb3bb50..da2d2e30 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -508,7 +508,10 @@ exports.VerificationLevels = [ * * MAXIMUM_PINS * * MAXIMUM_ROLES * * MAXIMUM_REACTIONS + * * MAXIMUM_CHANNELS + * * MAXIMUM_INVITES * * UNAUTHORIZED + * * USER_BANNED * * MISSING_ACCESS * * INVALID_ACCOUNT_TYPE * * CANNOT_EXECUTE_ON_DM @@ -528,9 +531,13 @@ exports.VerificationLevels = [ * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL * * INVALID_OR_TAKEN_INVITE_CODE * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE + * * INVALID_OAUTH_TOKEN * * BULK_DELETE_MESSAGE_TOO_OLD + * * INVALID_FORM_BODY * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT + * * INVALID_API_VERSION * * REACTION_BLOCKED + * * RESOURCE_OVERLOADED * @typedef {string} APIError */ exports.APIErrors = { @@ -556,7 +563,10 @@ exports.APIErrors = { MAXIMUM_PINS: 30003, MAXIMUM_ROLES: 30005, MAXIMUM_REACTIONS: 30010, + MAXIMUM_CHANNELS: 30013, + MAXIMUM_INVITES: 30016, UNAUTHORIZED: 40001, + USER_BANNED: 40007, MISSING_ACCESS: 50001, INVALID_ACCOUNT_TYPE: 50002, CANNOT_EXECUTE_ON_DM: 50003, @@ -576,9 +586,13 @@ exports.APIErrors = { CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, INVALID_OR_TAKEN_INVITE_CODE: 50020, CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, + INVALID_OAUTH_TOKEN: 50025, BULK_DELETE_MESSAGE_TOO_OLD: 50034, + INVALID_FORM_BODY: 50035, INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, + INVALID_API_VERSION: 50041, REACTION_BLOCKED: 90001, + RESOURCE_OVERLOADED: 130000, }; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 456deee7..df1b5f2d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -557,7 +557,10 @@ declare module 'discord.js' { MAXIMUM_PINS: 30003; MAXIMUM_ROLES: 30005; MAXIMUM_REACTIONS: 30010; + MAXIMUM_CHANNELS: 30013; + MAXIMUM_INVITES: 30016; UNAUTHORIZED: 40001; + USER_BANNED: 40007; MISSING_ACCESS: 50001; INVALID_ACCOUNT_TYPE: 50002; CANNOT_EXECUTE_ON_DM: 50003; @@ -576,9 +579,13 @@ declare module 'discord.js' { INVALID_BULK_DELETE_QUANTITY: 50016; CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019; CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021; + INVALID_OAUTH_TOKEN: 50025; BULK_DELETE_MESSAGE_TOO_OLD: 50034; + INVALID_FORM_BODY: 50035; INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036; + INVALID_API_VERSION: 50041; REACTION_BLOCKED: 90001; + RESOURCE_OVERLOADED: 130000; }; VoiceStatus: { CONNECTED: 0; From 79133b4d5e55132b04687c04ba247416aed615e6 Mon Sep 17 00:00:00 2001 From: Tenpi <37512637+Tenpi@users.noreply.github.com> Date: Fri, 11 Oct 2019 18:52:00 -0400 Subject: [PATCH 283/428] at(typings): partial Types (#3493) * test * test 2 * update * update * replaced double quotes * Made message.guild and message.member nullable * replaced double quotes again (oops) * missing semicolons * removed fetch from Omit * Added Partialize generic type * Created interfaces (prettier intellisense) * joinedAt/joinedTimestamp are nullable and fixed conflict --- typings/index.d.ts | 86 +++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index df1b5f2d..bc96ed23 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -170,33 +170,33 @@ declare module 'discord.js' { public sweepMessages(lifetime?: number): number; public toJSON(): object; - public on(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; - public on(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; - public on(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; + public on(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel | PartialChannel) => void): this; + public on(event: 'channelPinsUpdate', listener: (channel: Channel | PartialChannel, time: Date) => void): this; + public on(event: 'channelUpdate', listener: (oldChannel: Channel | PartialChannel, newChannel: Channel | PartialChannel) => void): this; public on(event: 'debug' | 'warn', listener: (info: string) => void): this; public on(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public on(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public on(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public on(event: 'error', listener: (error: Error) => void): this; - public on(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User) => void): this; + public on(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User | PartialUser) => void): this; public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; - public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; - public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; - public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember | PartialGuildMember) => void): this; + public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; + public on(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; + public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; - public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; - public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; - public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; - public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; + public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; + public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User | PartialUser) => void): this; + public on(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; - public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; - public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; + public on(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: 'invalidated', listener: () => void): this; @@ -207,33 +207,33 @@ declare module 'discord.js' { public on(event: 'shardResume', listener: (id: number, replayed: number) => void): this; public on(event: string, listener: Function): this; - public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; - public once(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; - public once(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; + public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel | PartialChannel) => void): this; + public once(event: 'channelPinsUpdate', listener: (channel: Channel | PartialChannel, time: Date) => void): this; + public once(event: 'channelUpdate', listener: (oldChannel: Channel | PartialChannel, newChannel: Channel | PartialChannel) => void): this; public once(event: 'debug' | 'warn', listener: (info: string) => void): this; public once(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public once(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public once(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public once(event: 'error', listener: (error: Error) => void): this; - public once(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User) => void): this; + public once(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User | PartialUser) => void): this; public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; - public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; - public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; - public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember | PartialGuildMember) => void): this; + public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; + public once(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; + public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; - public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; - public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; - public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; - public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; + public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; + public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User | PartialUser) => void): this; + public once(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; - public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; - public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; + public once(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: 'invalidated', listener: () => void): this; @@ -476,7 +476,7 @@ declare module 'discord.js' { RESUMED: 'resumed'; }; PartialTypes: { - [K in PartialType]: K; + [K in PartialTypes]: K; }; WSEvents: { [K in WSEventType]: K; @@ -645,7 +645,7 @@ declare module 'discord.js' { constructor(client: Client, data?: object); public messages: MessageStore; public recipient: User; - public readonly partial: boolean; + public readonly partial: false; } export class Emoji extends Base { @@ -859,7 +859,7 @@ declare module 'discord.js' { public readonly kickable: boolean; public readonly manageable: boolean; public nickname: string | null; - public readonly partial: boolean; + public readonly partial: false; public readonly permissions: Readonly; public readonly premiumSince: Date | null; public premiumSinceTimestamp: number | null; @@ -945,7 +945,7 @@ declare module 'discord.js' { public activity: MessageActivity | null; public application: ClientApplication | null; public attachments: Collection; - public author: User | null; + public author: User; public channel: TextChannel | DMChannel; public readonly cleanContent: string; public content: string; @@ -963,7 +963,7 @@ declare module 'discord.js' { public readonly member: GuildMember | null; public mentions: MessageMentions; public nonce: string | null; - public readonly partial: boolean; + public readonly partial: false; public readonly pinnable: boolean; public pinned: boolean; public reactions: ReactionStore; @@ -1396,7 +1396,7 @@ declare module 'discord.js' { public readonly dmChannel: DMChannel; public id: Snowflake; public locale: string; - public readonly partial: boolean; + public readonly partial: false; public readonly presence: Presence; public readonly tag: string; public username: string; @@ -2499,6 +2499,19 @@ declare module 'discord.js' { | 'GUILD_MEMBER' | 'MESSAGE'; + type Partialize = { + id: string; + partial: true; + fetch(): Promise; + } & { + [K in keyof Omit]: T[K] | null; + }; + + interface PartialMessage extends Partialize {} + interface PartialChannel extends Partialize {} + interface PartialGuildMember extends Partialize {} + interface PartialUser extends Partialize {} + type PresenceStatus = ClientPresenceStatus | 'offline'; type PresenceStatusData = ClientPresenceStatus | 'invisible'; @@ -2605,11 +2618,6 @@ declare module 'discord.js' { compress?: boolean; } - type PartialType = 'USER' - | 'CHANNEL' - | 'GUILD_MEMBER' - | 'MESSAGE'; - type WSEventType = 'READY' | 'RESUMED' | 'GUILD_CREATE' From c3228b426370a6cc5d48cb41e28bbe9f731e8473 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 11 Oct 2019 17:54:02 -0500 Subject: [PATCH 284/428] fix(docslink): partialtypes (#3510) --- docs/topics/partials.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/partials.md b/docs/topics/partials.md index 2566f3de..f1f05fe2 100644 --- a/docs/topics/partials.md +++ b/docs/topics/partials.md @@ -6,7 +6,7 @@ discard the event. With partials, you're able to receive the event, with a Messa ## Opting in -Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType): +Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](/#/docs/main/master/typedef/PartialType): ```js // Accept partial messages and DM channels when emitting events @@ -58,4 +58,4 @@ bot or any bot that relies on still receiving updates to resources you don't hav good example. Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should -not be cached. \ No newline at end of file +not be cached. From ca1bd61f4f061a9e15f9f636b7c8386417770683 Mon Sep 17 00:00:00 2001 From: Alexander Kashev Date: Fri, 18 Oct 2019 11:30:49 +0200 Subject: [PATCH 285/428] typings(Emoji): remove deletable, add deleted, mark nullable props (#3542) --- typings/index.d.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index bc96ed23..52a9a39e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -651,13 +651,13 @@ declare module 'discord.js' { export class Emoji extends Base { constructor(client: Client, emoji: object); public animated: boolean; - public readonly createdAt: Date; - public readonly createdTimestamp: number; - public readonly deletable: boolean; - public id: Snowflake; + public readonly createdAt: Date | null; + public readonly createdTimestamp: number | null; + public deleted: boolean; + public id: Snowflake | null; public name: string; public readonly identifier: string; - public readonly url: string; + public readonly url: string | null; public toJSON(): object; public toString(): string; } From a61cfc3004e84738e3155dd275800d92cb1a96d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Fri, 18 Oct 2019 11:32:19 +0200 Subject: [PATCH 286/428] docs: VoiceStateUpdate always sends an instance of VoiceState (#3537) * docs: VoiceStateUpdate always sends the old * typings: Update definition for voiceStateUpdate event --- src/client/actions/VoiceStateUpdate.js | 2 +- typings/index.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 14e1aa3f..386bf778 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -33,7 +33,7 @@ class VoiceStateUpdate extends Action { /** * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate - * @param {?VoiceState} oldState The voice state before the update + * @param {VoiceState} oldState The voice state before the update * @param {VoiceState} newState The voice state after the update */ client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); diff --git a/typings/index.d.ts b/typings/index.d.ts index 52a9a39e..6ff29546 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -197,7 +197,7 @@ declare module 'discord.js' { public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; public on(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; - public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; + public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: 'invalidated', listener: () => void): this; public on(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; @@ -234,7 +234,7 @@ declare module 'discord.js' { public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; public once(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; - public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: 'invalidated', listener: () => void): this; public once(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; From 9e0705cbc39f1be4b03a3cad5e2d216b7d1be4e9 Mon Sep 17 00:00:00 2001 From: rei2hu Date: Fri, 18 Oct 2019 04:55:35 -0500 Subject: [PATCH 287/428] fix(Message): check for edited_timestamp in data when patching message (#3535) * check for data.edited_timestamp * actually i should do it like this for consistency * indentation --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 8a9df07e..b09f1d34 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -219,7 +219,7 @@ class Message extends Base { const clone = this._clone(); this._edits.unshift(clone); - this.editedTimestamp = new Date(data.edited_timestamp).getTime(); + if ('edited_timestamp' in data) this.editedTimestamp = new Date(data.edited_timestamp).getTime(); if ('content' in data) this.content = data.content; if ('pinned' in data) this.pinned = data.pinned; if ('tts' in data) this.tts = data.tts; From 16db92ede8dc164346eca8482ea008cb21032c7b Mon Sep 17 00:00:00 2001 From: Alexander Kashev Date: Fri, 18 Oct 2019 13:06:34 +0200 Subject: [PATCH 288/428] typings(GuildEmoji): restore deletable, remove inherited property deleted (#3543) * Typings: restore deletable on GuildEmoji * Remove inherited property "deleted" --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 6ff29546..cfcd9661 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -833,7 +833,7 @@ declare module 'discord.js' { private _roles: string[]; public available: boolean; - public deleted: boolean; + public readonly deletable: boolean; public guild: Guild; public managed: boolean; public requiresColons: boolean; From 9bcb6a04ba43f7d6eebd48c81b84c227cd94a094 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Sun, 27 Oct 2019 11:58:38 -0400 Subject: [PATCH 289/428] fix(VoiceConnection): clear timeouts using Client#clearTimeout (#3553) * Update VoiceConnection.js * fix last instance --- src/client/voice/VoiceConnection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b56177c6..57fac3ac 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -282,7 +282,7 @@ class VoiceConnection extends EventEmitter { * @private */ authenticateFailed(reason) { - clearTimeout(this.connectTimeout); + this.client.clearTimeout(this.connectTimeout); this.emit('debug', `Authenticate failed - ${reason}`); if (this.status === VoiceStatus.AUTHENTICATING) { /** @@ -348,7 +348,7 @@ class VoiceConnection extends EventEmitter { disconnect() { this.emit('closing'); this.emit('debug', 'disconnect() triggered'); - clearTimeout(this.connectTimeout); + this.client.clearTimeout(this.connectTimeout); const conn = this.voiceManager.connections.get(this.channel.guild.id); if (conn === this) this.voiceManager.connections.delete(this.channel.guild.id); this.sendVoiceStateUpdate({ @@ -454,7 +454,7 @@ class VoiceConnection extends EventEmitter { this.status = VoiceStatus.CONNECTED; const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); dispatcher.on('finish', () => { - clearTimeout(this.connectTimeout); + this.client.clearTimeout(this.connectTimeout); this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, From 3a9eb5b92928631edc79e09894ba6bbad90516f7 Mon Sep 17 00:00:00 2001 From: Marwin M Date: Tue, 29 Oct 2019 13:22:21 +0100 Subject: [PATCH 290/428] Fix Opus voice streams (#3555) This fixes a wrong assumption about incoming discord voice packets revealed during a recent discord change that broke incoming opus voice streams --- src/client/voice/receiver/PacketHandler.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 7189f112..8ea9576e 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -63,7 +63,9 @@ class PacketHandler extends EventEmitter { if (byte === 0) continue; offset += 1 + (0b1111 & (byte >> 4)); } - while (packet[offset] === 0) offset++; + // Skip over undocumented Discord byte + offset++; + packet = packet.slice(offset); } From 3c634b2a262787ab42b0c3c2242d2616874a1c29 Mon Sep 17 00:00:00 2001 From: Jeroen Claassens Date: Mon, 4 Nov 2019 11:25:14 +0100 Subject: [PATCH 291/428] chore: mark optional peerDependencies as optional (#3511) --- package.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/package.json b/package.json index e89ca4fc..cda32552 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,29 @@ "zlib-sync": "^0.1.4", "zucc": "^0.1.0" }, + "peerDependenciesMeta": { + "@discordjs/uws": { + "optional": true + }, + "bufferutil": { + "optional": true + }, + "erlpack": { + "optional": true + }, + "libsodium-wrappers": { + "optional": true + }, + "sodium": { + "optional": true + }, + "zlib-sync": { + "optional": true + }, + "zucc": { + "optional": true + } + }, "devDependencies": { "@types/node": "^10.12.24", "@types/ws": "^6.0.1", From 2e20e8092bcd14fc992d51ba24e29e11b8ef0dc9 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 4 Nov 2019 11:25:43 +0100 Subject: [PATCH 292/428] fix(*Collector): account for a max listener count of 0 (#3504) --- src/structures/MessageCollector.js | 4 ++-- src/structures/ReactionCollector.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 44156cc0..5d45be08 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -39,7 +39,7 @@ class MessageCollector extends Collector { for (const message of messages.values()) this.handleDispose(message); }).bind(this); - this.client.setMaxListeners(this.client.getMaxListeners() + 1); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); this.client.on(Events.MESSAGE_CREATE, this.handleCollect); this.client.on(Events.MESSAGE_DELETE, this.handleDispose); this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); @@ -48,7 +48,7 @@ class MessageCollector extends Collector { this.client.removeListener(Events.MESSAGE_CREATE, this.handleCollect); this.client.removeListener(Events.MESSAGE_DELETE, this.handleDispose); this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); - this.client.setMaxListeners(this.client.getMaxListeners() - 1); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); }); } diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index b4826100..8e84fb67 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -44,7 +44,7 @@ class ReactionCollector extends Collector { this.empty = this.empty.bind(this); - this.client.setMaxListeners(this.client.getMaxListeners() + 1); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); this.client.on(Events.MESSAGE_REACTION_ADD, this.handleCollect); this.client.on(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); this.client.on(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); @@ -53,7 +53,7 @@ class ReactionCollector extends Collector { this.client.removeListener(Events.MESSAGE_REACTION_ADD, this.handleCollect); this.client.removeListener(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); this.client.removeListener(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); - this.client.setMaxListeners(this.client.getMaxListeners() - 1); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); }); this.on('collect', (reaction, user) => { From e26697f07d00955e14d6756ef8a1230cc07f1b81 Mon Sep 17 00:00:00 2001 From: Albus Dumbledore Date: Mon, 4 Nov 2019 15:59:19 +0530 Subject: [PATCH 293/428] docs(readme): table of contents (#3539) --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 12ee3a2b..71efbfdd 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,17 @@

+## Table of contents +- [About](#about) +- [Installation](#installation) + - [Audio engines](#audio-engines) + - [Optional packages](#optional-packages) + - [Example Usage](#example-usage) + - [Links](#links) + - [Extensions](#extensions) + - [Contributing](#contributing) + - [Help](#help) + ## About discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the [Discord API](https://discordapp.com/developers/docs/intro). From 99466a99ede23572009ac0e63f29595bb30f7865 Mon Sep 17 00:00:00 2001 From: Purpzie <25022704+Purpzie@users.noreply.github.com> Date: Mon, 4 Nov 2019 04:33:42 -0600 Subject: [PATCH 294/428] typings(Util): use StringResolvable (fixes old pull) (#3556) Fixes my extremely old pull #3212 that didn't actually update the typing (Didn't know at the time.) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index cfcd9661..199c5b52 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1447,7 +1447,7 @@ declare module 'discord.js' { route: object, reason?: string ): Promise<{ id: Snowflake; position: number }[]>; - public static splitMessage(text: string, options?: SplitOptions): string[]; + public static splitMessage(text: StringResolvable, options?: SplitOptions): string[]; public static str2ab(str: string): ArrayBuffer; } From cc466fa4b9e096175f48899435c3865b2d12b351 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Mon, 4 Nov 2019 10:35:14 +0000 Subject: [PATCH 295/428] docs: NewsChannel and StoreChannel (#3557) * Added news & store * Update GuildChannel.js * Added in News and Store --- src/structures/Channel.js | 2 ++ src/structures/GuildChannel.js | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/structures/Channel.js b/src/structures/Channel.js index ab6adb81..9eade660 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -19,6 +19,8 @@ class Channel extends Base { * * `text` - a guild text channel * * `voice` - a guild voice channel * * `category` - a guild category channel + * * `news` - a guild news channel + * * `store` - a guild store channel * * `unknown` - a generic channel of unknown type, could be Channel or GuildChannel * @type {string} */ diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 8ffdcfe8..fa1f84d4 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -10,7 +10,12 @@ const Collection = require('../util/Collection'); const { Error, TypeError } = require('../errors'); /** - * Represents a guild channel (i.g. a {@link TextChannel}, {@link VoiceChannel} or {@link CategoryChannel}). + * Represents a guild channel from any of the following: + * - {@link TextChannel} + * - {@link VoiceChannel} + * - {@link CategoryChannel} + * - {@link NewsChannel} + * - {@link StoreChannel} * @extends {Channel} */ class GuildChannel extends Channel { From 38d370fb18fdbf079b1c91793c0550e77667eccd Mon Sep 17 00:00:00 2001 From: matthewfripp <50251454+matthewfripp@users.noreply.github.com> Date: Mon, 4 Nov 2019 13:44:36 +0000 Subject: [PATCH 296/428] feat(MessageAttachment): add spoiler property (#3561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(MessageAttachment): add spoiler property * typings * Implement suggestions * Make readonly Co-Authored-By: Antonio Román --- src/structures/MessageAttachment.js | 8 ++++++++ typings/index.d.ts | 1 + 2 files changed, 9 insertions(+) diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index a4c05303..1af5d1ed 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -81,6 +81,14 @@ class MessageAttachment { this.width = typeof data.width !== 'undefined' ? data.width : null; } + /** + * Whether or not this attachment has been marked as a spoiler + * @type {boolean} + */ + get spoiler() { + return Util.basename(this.url).startsWith('SPOILER_'); + } + toJSON() { return Util.flatten(this); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 199c5b52..da78cc7e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1006,6 +1006,7 @@ declare module 'discord.js' { public size: number; public url: string; public width: number | null; + public readonly spoiler: boolean; public setFile(attachment: BufferResolvable | Stream, name?: string): this; public setName(name: string): this; public toJSON(): object; From 9a31e6e53a09cfa868bf3bbf45224821a182bc52 Mon Sep 17 00:00:00 2001 From: Carter <2209705@jeffcoschools.us> Date: Sat, 9 Nov 2019 12:52:06 -0700 Subject: [PATCH 297/428] docs(README): travis badge => github actions badge (#3569) * travis badge => github actions badge * this is why you don't copy paste :^) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71efbfdd..6c12826c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
Discord server NPM version NPM downloads - Build status + Build status Dependencies Patreon

From 1352bff2fd969092b48368ebc1f05a080572f827 Mon Sep 17 00:00:00 2001 From: Edward Wang <37031713+ewang20027@users.noreply.github.com> Date: Sat, 9 Nov 2019 11:52:55 -0800 Subject: [PATCH 298/428] docs(README): link to guide page instead of source (#3566) * Fixed the Update Guide Link Original link pointed to https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md, which is invalid. I'm not sure if the link I put (https://github.com/discordjs/guide/blob/master/guide/additional-info/changes-in-v12.md) is the correct one, but I will assume it is. * Used link to the DJS guide. --- docs/general/welcome.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index da1e02e8..d13e623b 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -77,7 +77,7 @@ client.login('token'); * [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) * [Documentation](https://discord.js.org/#/docs/main/master/general/welcome) * [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable - See also the WIP [Update Guide](https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md) also including updated and removed items in the library. + See also the WIP [Update Guide](https://discordjs.guide/additional-info/changes-in-v12.html) also including updated and removed items in the library. * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/discordjs/discord.js) From 1bcc0c2e1d7a8824529e63bee304ad1cb54ddfe3 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Tue, 19 Nov 2019 13:51:26 -0700 Subject: [PATCH 299/428] feat(GuildAuditLogs): add new event types (#3584) * adds more audit-log entries this adds additional audit-log types from https://discordapp/discord-api-docs/pull/1191 * typings for new audit-log entries * typings for new audit-log entries * fix action numbers --- src/structures/GuildAuditLogs.js | 18 ++++++++++++++++++ typings/index.d.ts | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 92421919..53494fa4 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -53,6 +53,9 @@ const Targets = { * * MEMBER_BAN_REMOVE: 23 * * MEMBER_UPDATE: 24 * * MEMBER_ROLE_UPDATE: 25 + * * MEMBER_MOVE: 26 + * * MEMBER_DISCONNECT: 27 + * * BOT_ADD: 28, * * ROLE_CREATE: 30 * * ROLE_UPDATE: 31 * * ROLE_DELETE: 32 @@ -66,6 +69,12 @@ const Targets = { * * EMOJI_UPDATE: 61 * * EMOJI_DELETE: 62 * * MESSAGE_DELETE: 72 + * * MESSAGE_BULK_DELETE: 73 + * * MESSAGE_PIN: 74 + * * MESSAGE_UNPIN: 75 + * * INTEGRATION_CREATE: 80 + * * INTEGRATION_UPDATE: 81 + * * INTEGRATION_DELETE: 82 * @typedef {?number|string} AuditLogAction */ @@ -89,6 +98,9 @@ const Actions = { MEMBER_BAN_REMOVE: 23, MEMBER_UPDATE: 24, MEMBER_ROLE_UPDATE: 25, + MEMBER_MOVE: 26, + MEMBER_DISCONNECT: 27, + BOT_ADD: 28, ROLE_CREATE: 30, ROLE_UPDATE: 31, ROLE_DELETE: 32, @@ -102,6 +114,12 @@ const Actions = { EMOJI_UPDATE: 61, EMOJI_DELETE: 62, MESSAGE_DELETE: 72, + MESSAGE_BULK_DELETE: 73, + MESSAGE_PIN: 74, + MESSAGE_UNPIN: 75, + INTEGRATION_CREATE: 80, + INTEGRATION_UPDATE: 81, + INTEGRATION_DELETE: 82, }; diff --git a/typings/index.d.ts b/typings/index.d.ts index da78cc7e..1da0d9d2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2188,6 +2188,9 @@ declare module 'discord.js' { MEMBER_BAN_REMOVE?: number; MEMBER_UPDATE?: number; MEMBER_ROLE_UPDATE?: number; + MEMBER_MOVE?: number; + MEMBER_DISCONNECT?: number; + BOT_ADD?: number; ROLE_CREATE?: number; ROLE_UPDATE?: number; ROLE_DELETE?: number; @@ -2201,6 +2204,12 @@ declare module 'discord.js' { EMOJI_UPDATE?: number; EMOJI_DELETE?: number; MESSAGE_DELETE?: number; + MESSAGE_BULK_DELETE?: number; + MESSAGE_PIN?: number; + MESSAGE_UNPIN?: number; + INTEGRATION_CREATE?: number; + INTEGRATION_UPDATE?: number; + INTEGRATION_DELETE?: number; } type GuildAuditLogsActionType = 'CREATE' From d39f17925d1bc9a5f25adbc2f52f7807c4e8b163 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Tue, 19 Nov 2019 15:54:35 -0500 Subject: [PATCH 300/428] fix(Invite): fix valueOf returning undefined (#3582) * Update Invite.js * Fix typings * Fix ESLint errors --- src/structures/Invite.js | 4 ++++ typings/index.d.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index d5e9f895..4d048b3f 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -183,6 +183,10 @@ class Invite extends Base { guild: 'guildID', }); } + + valueOf() { + return this.code; + } } module.exports = Invite; diff --git a/typings/index.d.ts b/typings/index.d.ts index 1da0d9d2..3cae781a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -878,6 +878,7 @@ declare module 'discord.js' { public setNickname(nickname: string, reason?: string): Promise; public toJSON(): object; public toString(): string; + public valueOf(): string; } export class Integration extends Base { From cbde819b6a21e008625c20ca575d44f4e450bd2f Mon Sep 17 00:00:00 2001 From: Jyguy Date: Tue, 19 Nov 2019 18:21:47 -0500 Subject: [PATCH 301/428] typings(GuildAuditLogsFetchOptions): specify concrete type of 'type' property (#3586) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 3cae781a..20851d7a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2222,7 +2222,7 @@ declare module 'discord.js' { before?: Snowflake | GuildAuditLogsEntry; limit?: number; user?: UserResolvable; - type?: string | number; + type?: GuildAuditLogsAction | number; } type GuildAuditLogsTarget = keyof GuildAuditLogsTargets; From 100360705ae29216f14129bc7a37f3870ec58fc6 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Wed, 20 Nov 2019 12:11:23 -0500 Subject: [PATCH 302/428] fix(APIRouter): use proper symbol for util.inspect (#3589) --- src/rest/APIRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 4d1fb5d6..efb71fa3 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -4,7 +4,7 @@ const noop = () => {}; // eslint-disable-line no-empty-function const methods = ['get', 'post', 'delete', 'patch', 'put']; const reflectors = [ 'toString', 'valueOf', 'inspect', 'constructor', - Symbol.toPrimitive, Symbol.for('util.inspect.custom'), + Symbol.toPrimitive, Symbol.for('nodejs.util.inspect.custom'), ]; function buildRoute(manager) { From 1b1289b35e2f8cd21341351549e6ac52fd08cb34 Mon Sep 17 00:00:00 2001 From: BannerBomb Date: Mon, 25 Nov 2019 10:17:30 -0500 Subject: [PATCH 303/428] misc(index): export Store- and NewsChannel (#3594) * Added Store and NewsChannel to exports Added the StoreChannel and NewsChannel structures to the module exports. * keeping the list in alphabetical order I moved the StoreChannel and NewsChannel exports that I added in the last commit in their right position to keep things alphabetized. --- src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index 77c56336..2d776e7c 100644 --- a/src/index.js +++ b/src/index.js @@ -79,6 +79,7 @@ module.exports = { MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), MessageReaction: require('./structures/MessageReaction'), + NewsChannel: require('./structures/NewsChannel'), PermissionOverwrites: require('./structures/PermissionOverwrites'), Presence: require('./structures/Presence').Presence, ClientPresence: require('./structures/ClientPresence'), @@ -86,6 +87,7 @@ module.exports = { ReactionEmoji: require('./structures/ReactionEmoji'), RichPresenceAssets: require('./structures/Presence').RichPresenceAssets, Role: require('./structures/Role'), + StoreChannel: require('./structures/StoreChannel'), Team: require('./structures/Team'), TeamMember: require('./structures/TeamMember'), TextChannel: require('./structures/TextChannel'), From 2ca74d6b63392c09a388c58f9b04945e884646c6 Mon Sep 17 00:00:00 2001 From: Jeroen Claassens Date: Thu, 5 Dec 2019 13:13:42 +0100 Subject: [PATCH 304/428] feat(Activity): support for CUSTOM_STATUS activity type (#3353) * feat: support for custom status in activity * nit(typings): order properties --- src/structures/Presence.js | 22 ++++++++++++++++++++++ src/util/Constants.js | 2 ++ typings/index.d.ts | 6 +++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 8c7d22b0..2eccf7cb 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -3,6 +3,7 @@ const Util = require('../util/Util'); const ActivityFlags = require('../util/ActivityFlags'); const { ActivityTypes } = require('../util/Constants'); +const Emoji = require('./Emoji'); /** * Activity sent in a message. @@ -205,6 +206,18 @@ class Activity { * @type {Readonly} */ this.flags = new ActivityFlags(data.flags).freeze(); + + /** + * Emoji for a custom activity + * @type {?Emoji} + */ + this.emoji = data.emoji ? new Emoji(presence.client, data.emoji) : null; + + /** + * Creation date of the activity + * @type {number} + */ + this.createdTimestamp = new Date(data.created_at).getTime(); } /** @@ -221,6 +234,15 @@ class Activity { ); } + /** + * The time the activity was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + /** * When concatenated with a string, this automatically returns the activities' name instead of the Activity object. * @returns {string} diff --git a/src/util/Constants.js b/src/util/Constants.js index da2d2e30..12ddfbf8 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -411,6 +411,7 @@ exports.MessageTypes = [ * * STREAMING * * LISTENING * * WATCHING + * * CUSTOM_STATUS * @typedef {string} ActivityType */ exports.ActivityTypes = [ @@ -418,6 +419,7 @@ exports.ActivityTypes = [ 'STREAMING', 'LISTENING', 'WATCHING', + 'CUSTOM_STATUS', ]; exports.ChannelTypes = { diff --git a/typings/index.d.ts b/typings/index.d.ts index 20851d7a..abab6b95 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -25,7 +25,10 @@ declare module 'discord.js' { constructor(presence: Presence, data?: object); public applicationID: Snowflake | null; public assets: RichPresenceAssets | null; + public readonly createdAt: Date; + public createdTimestamp: number; public details: string | null; + public emoji: Emoji | null; public name: string; public party: { id: string | null; @@ -1913,7 +1916,8 @@ declare module 'discord.js' { type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' - | 'WATCHING'; + | 'WATCHING' + | 'CUSTOM_STATUS'; type MessageFlagsString = 'CROSSPOSTED' | 'IS_CROSSPOST' From bb8333a4f9d3171f41a72e3bd5d5c6118fe22025 Mon Sep 17 00:00:00 2001 From: Clemens E <43886026+Clemens-E@users.noreply.github.com> Date: Fri, 6 Dec 2019 12:56:29 +0100 Subject: [PATCH 305/428] Handle voice errors outside of authenticated event (#3520) --- src/client/voice/ClientVoiceManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 78413693..cf752662 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -93,12 +93,13 @@ class ClientVoiceManager { reject(reason); }); + connection.on('error', reject); + connection.once('authenticated', () => { connection.once('ready', () => { resolve(connection); connection.removeListener('error', reject); }); - connection.on('error', reject); connection.once('disconnect', () => this.connections.delete(channel.guild.id)); }); }); From 4585d965b430ed0661ac8d3e32b738f27b4c9d42 Mon Sep 17 00:00:00 2001 From: sillyfrog <816454+sillyfrog@users.noreply.github.com> Date: Fri, 6 Dec 2019 21:59:57 +1000 Subject: [PATCH 306/428] Start/Stop speaking events on UDP packets (#3578) * Start/Stop speaking using incomming UDP packets * Fix ESLint errors * Updates for styling consistency Co-Authored-By: Gryffon Bellish * Minor improvements * Acutally use previousTimeout * Use BaseClient setTimeout and refresh() * Update README to match node version for refresh() * Update comment to match startSpeaking * Correctly report Priority bit * Fix ESlint errors --- README.md | 2 +- src/client/voice/VoiceConnection.js | 11 ++++--- src/client/voice/networking/VoiceWebSocket.js | 4 +-- src/client/voice/receiver/PacketHandler.js | 31 ++++++++++++++++--- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6c12826c..d667ab1f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 10.0.0 or newer is required.** +**Node.js 10.2.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 57fac3ac..540dd8c7 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -422,7 +422,7 @@ class VoiceConnection extends EventEmitter { udp.on('error', err => this.emit('error', err)); ws.on('ready', this.onReady.bind(this)); ws.on('sessionDescription', this.onSessionDescription.bind(this)); - ws.on('speaking', this.onSpeaking.bind(this)); + ws.on('startSpeaking', this.onStartSpeaking.bind(this)); this.sockets.ws.connect(); } @@ -465,16 +465,19 @@ class VoiceConnection extends EventEmitter { }); } + onStartSpeaking({ user_id, ssrc, speaking }) { + this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking }); + } + /** * Invoked when a speaking event is received. * @param {Object} data The received data * @private */ - onSpeaking({ user_id, ssrc, speaking }) { + onSpeaking({ user_id, speaking }) { speaking = new Speaking(speaking).freeze(); const guild = this.channel.guild; const user = this.client.users.get(user_id); - this.ssrcMap.set(+ssrc, user_id); const old = this._speaking.get(user_id); this._speaking.set(user_id, speaking); /** @@ -504,7 +507,7 @@ class VoiceConnection extends EventEmitter { } } - play() {} // eslint-disable-line no-empty-function + play() { } // eslint-disable-line no-empty-function } PlayInterface.applyToClass(VoiceConnection); diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 6ddabe62..ac6405e1 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -201,9 +201,9 @@ class VoiceWebSocket extends EventEmitter { /** * Emitted whenever a speaking packet is received. * @param {Object} data - * @event VoiceWebSocket#speaking + * @event VoiceWebSocket#startSpeaking */ - this.emit('speaking', packet.d); + this.emit('startSpeaking', packet.d); break; default: /** diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 8ea9576e..d468abab 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -3,6 +3,10 @@ const secretbox = require('../util/Secretbox'); const EventEmitter = require('events'); +// The delay between packets when a user is considered to have stopped speaking +// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200 +const DISCORD_SPEAKING_DELAY = 250; + class Readable extends require('stream').Readable { _read() {} } // eslint-disable-line no-empty-function class PacketHandler extends EventEmitter { @@ -11,6 +15,7 @@ class PacketHandler extends EventEmitter { this.nonce = Buffer.alloc(24); this.receiver = receiver; this.streams = new Map(); + this.speakingTimeouts = new Map(); } get connection() { @@ -72,13 +77,29 @@ class PacketHandler extends EventEmitter { return packet; } - userFromSSRC(ssrc) { return this.connection.client.users.get(this.connection.ssrcMap.get(ssrc)); } - push(buffer) { const ssrc = buffer.readUInt32BE(8); - const user = this.userFromSSRC(ssrc); - if (!user) return; - let stream = this.streams.get(user.id); + const userStat = this.connection.ssrcMap.get(ssrc); + if (!userStat) return; + + let speakingTimeout = this.speakingTimeouts.get(ssrc); + if (typeof speakingTimeout === 'undefined') { + this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking }); + speakingTimeout = this.receiver.connection.client.setTimeout(() => { + try { + this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: 0 }); + this.receiver.connection.client.clearTimeout(speakingTimeout); + this.speakingTimeouts.delete(ssrc); + } catch (ex) { + // Connection already closed, ignore + } + }, DISCORD_SPEAKING_DELAY); + this.speakingTimeouts.set(ssrc, speakingTimeout); + } else { + speakingTimeout.refresh(); + } + + let stream = this.streams.get(userStat.userID); if (!stream) return; stream = stream.stream; const opusPacket = this.parseBuffer(buffer); From 123713305ad5a6aa1e5205a53713494009740aef Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+duckthecuck@users.noreply.github.com> Date: Sun, 8 Dec 2019 13:52:03 -0500 Subject: [PATCH 307/428] docs(ReactionStore): resolveID takes a reaction, not role (#3617) * Docs: ReactionStore.resolveID should take Reaction, not role * Make param lowercase Co-Authored-By: SpaceEEC --- src/stores/ReactionStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index b3910ba4..5092e861 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -38,7 +38,7 @@ class ReactionStore extends DataStore { * @method resolveID * @memberof ReactionStore * @instance - * @param {MessageReactionResolvable} role The role resolvable to resolve + * @param {MessageReactionResolvable} reaction The MessageReaction to resolve * @returns {?Snowflake} */ From 330d5db5868ad61bbdae5ba0b2090d10d76ae9c9 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Sun, 15 Dec 2019 12:20:15 -0700 Subject: [PATCH 308/428] feat(Webhook): addition of Webhook#avatarURL function (#3625) * feat: addition of Webhook#avatarURL * typings: added Webhook#avatarURL * fix: trailing space * docs: fixed jsdoc function description * fix: typo --- src/structures/Webhook.js | 10 ++++++++++ typings/index.d.ts | 1 + 2 files changed, 11 insertions(+) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 2ed6137b..da7dcd12 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -220,6 +220,16 @@ class Webhook { return this.client.options.http.api + this.client.api.webhooks(this.id, this.token); } + /** + * A link to the webhook's avatar. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + avatarURL({ format, size } = {}) { + if (!this.avatar) return null; + return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size); + } + static applyToClass(structure) { for (const prop of [ 'send', diff --git a/typings/index.d.ts b/typings/index.d.ts index abab6b95..cb2ccda0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1620,6 +1620,7 @@ declare module 'discord.js' { export class Webhook extends WebhookMixin() { constructor(client: Client, data?: object); public avatar: string; + public avatarURL(options?: AvatarOptions): string | null; public channelID: Snowflake; public guildID: Snowflake; public name: string; From 43782839ec51519deb27977573fb44e516c35ec2 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sun, 15 Dec 2019 19:23:06 +0000 Subject: [PATCH 309/428] feat: add new MessageFlags.FLAGS & User#system (#3603) * feat: add new FLAGS * feat: add system property * typings: add User#system & new MessageFlagsStrings --- src/structures/User.js | 7 +++++++ src/util/MessageFlags.js | 4 ++++ typings/index.d.ts | 5 ++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/structures/User.js b/src/structures/User.js index c2276c4f..b5358be7 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -59,6 +59,13 @@ class User extends Base { if (typeof data.bot !== 'undefined') this.bot = Boolean(data.bot); + /** + * Whether the user is an Official Discord System user (part of the urgent message system) + * @type {?boolean} + * @name User#system + */ + if (typeof data.system !== 'undefined') this.system = Boolean(data.system); + /** * The locale of the user's client (ISO 639-1) * @type {?string} diff --git a/src/util/MessageFlags.js b/src/util/MessageFlags.js index b1c86b7d..4863e671 100644 --- a/src/util/MessageFlags.js +++ b/src/util/MessageFlags.js @@ -13,6 +13,8 @@ class MessageFlags extends BitField {} * * `CROSSPOSTED` * * `IS_CROSSPOST` * * `SUPPRESS_EMBEDS` + * * `SOURCE_MESSAGE_DELETED` + * * `URGENT` * @type {Object} * @see {@link https://discordapp.com/developers/docs/resources/channel#message-object-message-flags} */ @@ -20,6 +22,8 @@ MessageFlags.FLAGS = { CROSSPOSTED: 1 << 0, IS_CROSSPOST: 1 << 1, SUPPRESS_EMBEDS: 1 << 2, + SOURCE_MESSAGE_DELETED: 1 << 3, + URGENT: 1 << 4, }; module.exports = MessageFlags; diff --git a/typings/index.d.ts b/typings/index.d.ts index cb2ccda0..b0774df2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1403,6 +1403,7 @@ declare module 'discord.js' { public locale: string; public readonly partial: false; public readonly presence: Presence; + public system?: boolean; public readonly tag: string; public username: string; public avatarURL(options?: AvatarOptions): string | null; @@ -1922,7 +1923,9 @@ declare module 'discord.js' { type MessageFlagsString = 'CROSSPOSTED' | 'IS_CROSSPOST' - | 'SUPPRESS_EMBEDS'; + | 'SUPPRESS_EMBEDS' + | 'SOURCE_MESSAGE_DELETED' + | 'URGENT'; interface APIErrror { UNKNOWN_ACCOUNT: number; From f56b442e83e4016015c9123d4308dbf9209eea76 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Sun, 15 Dec 2019 14:26:09 -0500 Subject: [PATCH 310/428] typings(Bitfield): use IterableIterator instead of Iterator (#3599) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index b0774df2..9eb0b453 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -124,7 +124,7 @@ declare module 'discord.js' { public toArray(): S[]; public toJSON(): number; public valueOf(): number; - public [Symbol.iterator](): Iterator; + public [Symbol.iterator](): IterableIterator; public static FLAGS: object; public static resolve(bit?: BitFieldResolvable): number; } From 5519d6fbaa61abccc27b742f97ad725bf4259265 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 15 Dec 2019 21:45:27 +0200 Subject: [PATCH 311/428] src: sharding cleanup and checkReady rewrite (#3393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * src: Step 1 of who knows how many * src: Remove accidentally committed test file * src: Remove useless added property in package.json * docs: Trailing spaces, come back >.> * src: Buhbye uws, we will miss you..not! * src: Move 'auto' shard selection from totalShardCount to shards * src: tweak * src: Filter out floats from shard IDs You want half of a shard or what? * src: Misc cleanup and bugfix for GUILD_BAN_ADD * src: Rewrite checkReady * src: Misse this while merging master into my branch * typings: Bring these up to date * typings: Forgot allReady event * src: Don't checkReady if the shard isn't waiting for guilds * src: Fix a possible bug for when the ws dies and the session becomes -1 * src: Hopefully fix last edge case that could case a shard to infinitely boot loop * src: Rename totalShardCount to shardCount * src: Small bugfix * src: Correct error message for shardCount being imvalid Co-Authored-By: bdistin * src: Small tweaks * src: If this doesn't fix the issues I'm gonna throw a brick at my PC * src: I swear, STOP BREAKING * src: *groans at a certain snake* * src: Use undefined instead of null on destroy in close event Setting it to null sets the close code to null, which causes a WebSocket error to be thrown. The error is thrown from WebSocket, although there is no connection alive. Fun times! * src: @SpaceEEC's requested changes * src: Remove zucc from discord.js Discord is removing support for it, sooo... Bye bye * src: Missed this * src: Apply @kyranet's suggestions Co-Authored-By: Antonio Román * src: @kyranet's suggestions * src: Remove pako, update debug messages - Pako is officially gone from both enviroments Install zlib-sync on node.js if you want it - Improve a few debug messages some more - Discover that internal sharding works in browsers but please don't do that --- .eslintrc.json | 2 +- README.md | 5 +- docs/general/welcome.md | 11 +- package.json | 11 +- src/WebSocket.js | 13 +- src/client/Client.js | 37 +-- src/client/websocket/WebSocketManager.js | 104 ++++---- src/client/websocket/WebSocketShard.js | 244 +++++++++++------- .../websocket/handlers/GUILD_BAN_ADD.js | 2 +- src/client/websocket/handlers/GUILD_CREATE.js | 16 +- src/client/websocket/handlers/READY.js | 2 - src/sharding/Shard.js | 2 +- src/sharding/ShardClientUtil.js | 2 +- src/util/Constants.js | 15 +- typings/index.d.ts | 22 +- 15 files changed, 280 insertions(+), 208 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index a4f08d68..54950034 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": "eslint:recommended", "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2019 }, "env": { "es6": true, diff --git a/README.md b/README.md index d667ab1f..2003f60d 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,12 @@ For production bots, using node-opus should be considered a necessity, especiall ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) -- [zucc](https://www.npmjs.com/package/zucc) for significantly faster WebSocket data inflation (`npm install zucc`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) -- [uws](https://www.npmjs.com/package/@discordjs/uws) for a much faster WebSocket connection (`npm install @discordjs/uws`) -- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`) +- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`) +- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`) ## Example usage ```js diff --git a/docs/general/welcome.md b/docs/general/welcome.md index d13e623b..52cb3135 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -24,8 +24,8 @@ v12 is still very much a work-in-progress, as we're aiming to make it the best i Only use it if you are fond of living life on the bleeding edge. ## About -discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to interact with the -[Discord API](https://discordapp.com/developers/docs/intro) very easily. +discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the +[Discord API](https://discordapp.com/developers/docs/intro). - Object-oriented - Predictable abstractions @@ -46,14 +46,13 @@ Using opusscript is only recommended for development environments where node-opu For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`) -- [zucc](https://www.npmjs.com/package/zucc) for significantly faster WebSocket data inflation (`npm install zucc`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) -- [uws](https://www.npmjs.com/package/@discordjs/uws) for a much faster WebSocket connection (`npm install @discordjs/uws`) -- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`) +- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`) +- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`) ## Example usage ```js diff --git a/package.json b/package.json index cda32552..c8decaa3 100644 --- a/package.json +++ b/package.json @@ -39,20 +39,18 @@ "abort-controller": "^3.0.0", "form-data": "^2.3.3", "node-fetch": "^2.3.0", - "pako": "^1.0.8", "prism-media": "^1.0.0", "setimmediate": "^1.0.5", "tweetnacl": "^1.0.1", - "ws": "^6.1.3" + "ws": "^7.2.0" }, "peerDependencies": { - "@discordjs/uws": "^11.149.1", "bufferutil": "^4.0.1", "erlpack": "discordapp/erlpack", "libsodium-wrappers": "^0.7.4", "sodium": "^3.0.2", - "zlib-sync": "^0.1.4", - "zucc": "^0.1.0" + "utf-8-validate": "^5.0.2", + "zlib-sync": "^0.1.6" }, "peerDependenciesMeta": { "@discordjs/uws": { @@ -97,8 +95,6 @@ "browser": { "https": false, "ws": false, - "uws": false, - "@discordjs/uws": false, "erlpack": false, "prism-media": false, "opusscript": false, @@ -107,7 +103,6 @@ "sodium": false, "worker_threads": false, "zlib-sync": false, - "zucc": false, "src/sharding/Shard.js": false, "src/sharding/ShardClientUtil.js": false, "src/sharding/ShardingManager.js": false, diff --git a/src/WebSocket.js b/src/WebSocket.js index 8c61dfb4..90dd51bb 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -1,10 +1,13 @@ 'use strict'; const { browser } = require('./util/Constants'); + +let erlpack; + try { - var erlpack = require('erlpack'); + erlpack = require('erlpack'); if (!erlpack.pack) erlpack = null; -} catch (err) {} // eslint-disable-line no-empty +} catch {} // eslint-disable-line no-empty let TextDecoder; @@ -13,11 +16,7 @@ if (browser) { exports.WebSocket = window.WebSocket; // eslint-disable-line no-undef } else { TextDecoder = require('util').TextDecoder; - try { - exports.WebSocket = require('@discordjs/uws'); - } catch (err) { - exports.WebSocket = require('ws'); - } + exports.WebSocket = require('ws'); } const ab = new TextDecoder(); diff --git a/src/client/Client.js b/src/client/Client.js index 3be8e311..a1ab6672 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -36,7 +36,7 @@ class Client extends BaseClient { try { // Test if worker threads module is present and used data = require('worker_threads').workerData || data; - } catch (_) { + } catch { // Do nothing } @@ -46,25 +46,25 @@ class Client extends BaseClient { } } - if (this.options.totalShardCount === DefaultOptions.totalShardCount) { - if ('TOTAL_SHARD_COUNT' in data) { - this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); + if (this.options.shardCount === DefaultOptions.shardCount) { + if ('SHARD_COUNT' in data) { + this.options.shardCount = Number(data.SHARD_COUNT); } else if (Array.isArray(this.options.shards)) { - this.options.totalShardCount = this.options.shards.length; - } else { - this.options.totalShardCount = this.options.shardCount; + this.options.shardCount = this.options.shards.length; } } - if (typeof this.options.shards === 'undefined' && typeof this.options.shardCount === 'number') { + const typeofShards = typeof this.options.shards; + + if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') { this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); } - if (typeof this.options.shards === 'number') this.options.shards = [this.options.shards]; + if (typeofShards === 'number') this.options.shards = [this.options.shards]; - if (typeof this.options.shards !== 'undefined') { + if (Array.isArray(this.options.shards)) { this.options.shards = [...new Set( - this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity) + this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)) )]; } @@ -198,7 +198,9 @@ class Client extends BaseClient { async login(token = this.token) { if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); - this.emit(Events.DEBUG, `Provided token: ${token}`); + this.emit(Events.DEBUG, + `Provided token: ${token.split('.').map((val, i) => i > 1 ? val.replace(/./g, '*') : val).join('.')}` + ); if (this.options.presence) { this.options.ws.presence = await this.presence._parse(this.options.presence); @@ -363,14 +365,15 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { // eslint-disable-line complexity - if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); + if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1'); } - if (options.shards && !Array.isArray(options.shards)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); + if (options.shards && + !(options.shards === 'auto' || Array.isArray(options.shards)) + ) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shards', '\'auto\', a number or array of numbers'); } if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS'); - if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1'); if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); } diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 8668d2de..71cbf93c 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -19,6 +19,7 @@ const BeforeReadyWhitelist = [ ]; const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011]; +const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007]; /** * The WebSocket manager for this client. @@ -47,9 +48,9 @@ class WebSocketManager extends EventEmitter { /** * The amount of shards this manager handles * @private - * @type {number|string} + * @type {number} */ - this.totalShards = this.client.options.shardCount; + this.totalShards = this.client.options.shards.length; /** * A collection of all shards this manager handles @@ -143,25 +144,25 @@ class WebSocketManager extends EventEmitter { const { total, remaining, reset_after } = sessionStartLimit; this.debug(`Fetched Gateway Information - URL: ${gatewayURL} - Recommended Shards: ${recommendedShards}`); + URL: ${gatewayURL} + Recommended Shards: ${recommendedShards}`); this.debug(`Session Limit Information - Total: ${total} - Remaining: ${remaining}`); + Total: ${total} + Remaining: ${remaining}`); this.gateway = `${gatewayURL}/`; - if (this.totalShards === 'auto') { + const { shards } = this.client.options; + + if (shards === 'auto') { this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); - this.totalShards = this.client.options.shardCount = this.client.options.totalShardCount = recommendedShards; - if (typeof this.client.options.shards === 'undefined' || !this.client.options.shards.length) { + this.totalShards = this.client.options.shardCount = recommendedShards; + if (shards === 'auto' || !this.client.options.shards.length) { this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); } } - const { shards } = this.client.options; - if (Array.isArray(shards)) { this.totalShards = shards.length; this.debug(`Spawning shards: ${shards.join(', ')}`); @@ -190,15 +191,17 @@ class WebSocketManager extends EventEmitter { this.shardQueue.delete(shard); if (!shard.eventsAttached) { - shard.on(ShardEvents.READY, () => { + shard.on(ShardEvents.ALL_READY, unavailableGuilds => { /** * Emitted when a shard turns ready. * @event Client#shardReady * @param {number} id The shard ID that turned ready + * @param {?Set} unavailableGuilds Set of unavailable guild IDs, if any */ - this.client.emit(Events.SHARD_READY, shard.id); + this.client.emit(Events.SHARD_READY, shard.id, unavailableGuilds); if (!this.shardQueue.size) this.reconnecting = false; + this.checkShardsReady(); }); shard.on(ShardEvents.CLOSE, event => { @@ -214,8 +217,8 @@ class WebSocketManager extends EventEmitter { return; } - if (event.code === 1000 || event.code === 4006) { - // Any event code in this range cannot be resumed. + if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) { + // These event codes cannot be resumed shard.sessionID = undefined; } @@ -226,27 +229,23 @@ class WebSocketManager extends EventEmitter { */ this.client.emit(Events.SHARD_RECONNECTING, shard.id); + this.shardQueue.add(shard); + if (shard.sessionID) { this.debug(`Session ID is present, attempting an immediate reconnect...`, shard); - shard.connect().catch(() => null); - return; + this.reconnect(true); + } else { + shard.destroy(undefined, true); + this.reconnect(); } - - shard.destroy(); - - this.shardQueue.add(shard); - this.reconnect(); }); shard.on(ShardEvents.INVALID_SESSION, () => { this.client.emit(Events.SHARD_RECONNECTING, shard.id); - - this.shardQueue.add(shard); - this.reconnect(); }); shard.on(ShardEvents.DESTROYED, () => { - this.debug('Shard was destroyed but no WebSocket connection existed... Reconnecting...', shard); + this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); this.client.emit(Events.SHARD_RECONNECTING, shard.id); @@ -264,7 +263,7 @@ class WebSocketManager extends EventEmitter { } catch (error) { if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) { throw new DJSError(WSCodes[error.code]); - // Undefined if session is invalid, error event (or uws' event mimicking it) for regular closes + // Undefined if session is invalid, error event for regular closes } else if (!error || error.code) { this.debug('Failed to connect to the gateway, requeueing...', shard); this.shardQueue.add(shard); @@ -285,14 +284,15 @@ class WebSocketManager extends EventEmitter { /** * Handles reconnects for this manager. + * @param {boolean} [skipLimit=false] IF this reconnect should skip checking the session limit * @private * @returns {Promise} */ - async reconnect() { + async reconnect(skipLimit = false) { if (this.reconnecting || this.status !== Status.READY) return false; this.reconnecting = true; try { - await this._handleSessionLimit(); + if (!skipLimit) await this._handleSessionLimit(); await this.createShards(); } catch (error) { this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); @@ -340,7 +340,7 @@ class WebSocketManager extends EventEmitter { this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); this.destroyed = true; this.shardQueue.clear(); - for (const shard of this.shards.values()) shard.destroy(); + for (const shard of this.shards.values()) shard.destroy(1000, true); } /** @@ -356,8 +356,8 @@ class WebSocketManager extends EventEmitter { remaining = session_start_limit.remaining; resetAfter = session_start_limit.reset_after; this.debug(`Session Limit Information - Total: ${session_start_limit.total} - Remaining: ${remaining}`); + Total: ${session_start_limit.total} + Remaining: ${remaining}`); } if (!remaining) { this.debug(`Exceeded identify threshold. Will attempt a connection in ${resetAfter}ms`); @@ -396,45 +396,37 @@ class WebSocketManager extends EventEmitter { /** * Checks whether the client is ready to be marked as ready. - * @returns {boolean} * @private */ - checkReady() { + async checkShardsReady() { + if (this.status === Status.READY) return; if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) { - return false; + return; } - const unavailableGuilds = this.client.guilds.reduce((acc, guild) => guild.available ? acc : acc + 1, 0); + this.status = Status.NEARLY; - // TODO: Rethink implementation for this - if (unavailableGuilds === 0) { - this.status = Status.NEARLY; - if (!this.client.options.fetchAllMembers) return this.triggerReady(); - // Fetch all members before marking self as ready - const promises = this.client.guilds.map(g => g.members.fetch()); - Promise.all(promises) - .then(() => this.triggerReady()) - .catch(e => { - this.debug(`Failed to fetch all members before ready! ${e}\n${e.stack}`); - this.triggerReady(); + if (this.client.options.fetchAllMembers) { + try { + const promises = this.client.guilds.map(guild => { + if (guild.available) return guild.members.fetch(); + // Return empty promise if guild is unavailable + return Promise.resolve(); }); - } else { - this.debug(`There are ${unavailableGuilds} unavailable guilds. Waiting for their GUILD_CREATE packets`); + await Promise.all(promises); + } catch (err) { + this.debug(`Failed to fetch all members before ready! ${err}\n${err.stack}`); + } } - return true; + this.triggerClientReady(); } /** * Causes the client to be marked as ready and emits the ready event. * @private */ - triggerReady() { - if (this.status === Status.READY) { - this.debug('Tried to mark self as ready, but already ready'); - return; - } - + triggerClientReady() { this.status = Status.READY; this.client.readyAt = new Date(); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index f77635b0..8d9afd31 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -4,23 +4,15 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { browser, Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); -let zstd; +const STATUS_KEYS = Object.keys(Status); +const CONNECTION_STATE = Object.keys(WebSocket.WebSocket); + let zlib; -if (browser) { - zlib = require('pako'); -} else { +if (!browser) { try { - zstd = require('zucc'); - if (!zstd.DecompressStream) zstd = null; - } catch (e) { - try { - zlib = require('zlib-sync'); - if (!zlib.Inflate) zlib = require('pako'); - } catch (err) { - zlib = require('pako'); - } - } + zlib = require('zlib-sync'); + } catch {} // eslint-disable-line no-empty } /** @@ -70,10 +62,10 @@ class WebSocketShard extends EventEmitter { this.sessionID = undefined; /** - * The previous 3 heartbeat pings of the shard (most recent first) - * @type {number[]} + * The previous heartbeat ping of the shard + * @type {number} */ - this.pings = []; + this.ping = -1; /** * The last time a ping was sent (a timestamp) @@ -128,7 +120,7 @@ class WebSocketShard extends EventEmitter { * @type {?NodeJS.Timer} * @private */ - Object.defineProperty(this, 'helloTimeout', { value: null, writable: true }); + Object.defineProperty(this, 'helloTimeout', { value: undefined, writable: true }); /** * If the manager attached its event handlers on the shard @@ -136,16 +128,27 @@ class WebSocketShard extends EventEmitter { * @private */ Object.defineProperty(this, 'eventsAttached', { value: false, writable: true }); - } - /** - * Average heartbeat ping of the websocket, obtained by averaging the WebSocketShard#pings property - * @type {number} - * @readonly - */ - get ping() { - const sum = this.pings.reduce((a, b) => a + b, 0); - return sum / this.pings.length; + /** + * A set of guild IDs this shard expects to receive + * @type {?Set} + * @private + */ + Object.defineProperty(this, 'expectedGuilds', { value: undefined, writable: true }); + + /** + * The ready timeout + * @type {?NodeJS.Timer} + * @private + */ + Object.defineProperty(this, 'readyTimeout', { value: undefined, writable: true }); + + /** + * Time when the WebSocket connection was opened + * @type {number} + * @private + */ + Object.defineProperty(this, 'connectedAt', { value: 0, writable: true }); } /** @@ -166,36 +169,35 @@ class WebSocketShard extends EventEmitter { connect() { const { gateway, client } = this.manager; - if (this.status === Status.READY && this.connection && this.connection.readyState === WebSocket.OPEN) { + if (this.connection && this.connection.readyState === WebSocket.OPEN && this.status === Status.READY) { return Promise.resolve(); } return new Promise((resolve, reject) => { - const onReady = () => { + const cleanup = () => { this.off(ShardEvents.CLOSE, onClose); + this.off(ShardEvents.READY, onReady); this.off(ShardEvents.RESUMED, onResumed); this.off(ShardEvents.INVALID_SESSION, onInvalid); + }; + + const onReady = () => { + cleanup(); resolve(); }; const onResumed = () => { - this.off(ShardEvents.CLOSE, onClose); - this.off(ShardEvents.READY, onReady); - this.off(ShardEvents.INVALID_SESSION, onInvalid); + cleanup(); resolve(); }; const onClose = event => { - this.off(ShardEvents.READY, onReady); - this.off(ShardEvents.RESUMED, onResumed); - this.off(ShardEvents.INVALID_SESSION, onInvalid); + cleanup(); reject(event); }; const onInvalid = () => { - this.off(ShardEvents.READY, onReady); - this.off(ShardEvents.RESUMED, onResumed); - this.off(ShardEvents.CLOSE, onClose); + cleanup(); // eslint-disable-next-line prefer-promise-reject-errors reject(); }; @@ -206,29 +208,35 @@ class WebSocketShard extends EventEmitter { this.once(ShardEvents.INVALID_SESSION, onInvalid); if (this.connection && this.connection.readyState === WebSocket.OPEN) { - this.identifyNew(); + this.debug('Connection found, attempting an immediate identify.'); + this.identify(); return; } - if (zstd) { - this.inflate = new zstd.DecompressStream(); - } else { + const wsQuery = { v: client.options.ws.version }; + + if (zlib) { this.inflate = new zlib.Inflate({ chunkSize: 65535, flush: zlib.Z_SYNC_FLUSH, to: WebSocket.encoding === 'json' ? 'string' : '', }); + wsQuery.compress = 'zlib-stream'; } - this.debug(`Trying to connect to ${gateway}, version ${client.options.ws.version}`); + this.debug( + `[CONNECT] + Gateway: ${gateway} + Version: ${client.options.ws.version} + Encoding: ${WebSocket.encoding} + Compression: ${zlib ? 'zlib-stream' : 'none'}`); this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; this.setHelloTimeout(); - const ws = this.connection = WebSocket.create(gateway, { - v: client.options.ws.version, - compress: zstd ? 'zstd-stream' : 'zlib-stream', - }); + this.connectedAt = Date.now(); + + const ws = this.connection = WebSocket.create(gateway, wsQuery); ws.onopen = this.onOpen.bind(this); ws.onmessage = this.onMessage.bind(this); ws.onerror = this.onError.bind(this); @@ -241,7 +249,7 @@ class WebSocketShard extends EventEmitter { * @private */ onOpen() { - this.debug('Opened a connection to the gateway successfully.'); + this.debug(`[CONNECTED] ${this.connection.url} in ${Date.now() - this.connectedAt}ms`); this.status = Status.NEARLY; } @@ -252,10 +260,8 @@ class WebSocketShard extends EventEmitter { */ onMessage({ data }) { let raw; - if (zstd) { - raw = this.inflate.decompress(new Uint8Array(data).buffer); - } else { - if (data instanceof ArrayBuffer) data = new Uint8Array(data); + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + if (zlib) { const l = data.length; const flush = l >= 4 && data[l - 4] === 0x00 && @@ -266,6 +272,8 @@ class WebSocketShard extends EventEmitter { this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH); if (!flush) return; raw = this.inflate.result; + } else { + raw = data; } let packet; try { @@ -281,19 +289,13 @@ class WebSocketShard extends EventEmitter { /** * Called whenever an error occurs with the WebSocket. - * @param {ErrorEvent|Object} event The error that occurred + * @param {ErrorEvent} event The error that occurred * @private */ onError(event) { const error = event && event.error ? event.error : event; if (!error) return; - if (error.message === 'uWs client connection error') { - this.debug('Received a uWs error. Closing the connection and reconnecting...'); - this.connection.close(4000); - return; - } - /** * Emitted whenever a shard's WebSocket encounters a connection error. * @event Client#shardError @@ -324,13 +326,13 @@ class WebSocketShard extends EventEmitter { * @private */ onClose(event) { - this.closeSequence = this.sequence; + if (this.sequence !== -1) this.closeSequence = this.sequence; this.sequence = -1; - this.debug(`WebSocket was closed. - Event Code: ${event.code} - Clean: ${event.wasClean} - Reason: ${event.reason || 'No reason received'}`); + this.debug(`[CLOSE] + Event Code: ${event.code} + Clean: ${event.wasClean} + Reason: ${event.reason || 'No reason received'}`); this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); @@ -360,16 +362,18 @@ class WebSocketShard extends EventEmitter { switch (packet.t) { case WSEvents.READY: /** - * Emitted when the shard becomes ready + * Emitted when the shard receives the READY payload and is now waiting for guilds * @event WebSocketShard#ready */ this.emit(ShardEvents.READY); this.sessionID = packet.d.session_id; - this.status = Status.READY; - this.debug(`READY | Session ${this.sessionID}.`); + this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id)); + this.status = Status.WAITING_FOR_GUILDS; + this.debug(`[READY] Session ${this.sessionID}.`); this.lastHeartbeatAcked = true; - this.sendHeartbeat(); + this.sendHeartbeat('ReadyHeartbeat'); + this.checkReady(); break; case WSEvents.RESUMED: { /** @@ -380,9 +384,10 @@ class WebSocketShard extends EventEmitter { this.status = Status.READY; const replayed = packet.s - this.closeSequence; - this.debug(`RESUMED | Session ${this.sessionID} | Replayed ${replayed} events.`); + this.debug(`[RESUMED] Session ${this.sessionID} | Replayed ${replayed} events.`); this.lastHeartbeatAcked = true; - this.sendHeartbeat(); + this.sendHeartbeat('ResumeHeartbeat'); + break; } } @@ -398,7 +403,7 @@ class WebSocketShard extends EventEmitter { this.connection.close(1001); break; case OPCodes.INVALID_SESSION: - this.debug(`Session invalidated. Resumable: ${packet.d}.`); + this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`); // If we can resume the session, do so immediately if (packet.d) { this.identifyResume(); @@ -417,13 +422,56 @@ class WebSocketShard extends EventEmitter { this.ackHeartbeat(); break; case OPCodes.HEARTBEAT: - this.sendHeartbeat(); + this.sendHeartbeat('HeartbeatRequest'); break; default: this.manager.handlePacket(packet, this); + if (this.status === Status.WAITING_FOR_GUILDS && packet.t === WSEvents.GUILD_CREATE) { + this.expectedGuilds.delete(packet.d.id); + this.checkReady(); + } } } + /** + * Checks if the shard can be marked as ready + * @private + */ + checkReady() { + // Step 0. Clear the ready timeout, if it exists + if (this.readyTimeout) { + this.manager.client.clearTimeout(this.readyTimeout); + this.readyTimeout = undefined; + } + // Step 1. If we don't have any other guilds pending, we are ready + if (!this.expectedGuilds.size) { + this.debug('Shard received all its guilds. Marking as fully ready.'); + this.status = Status.READY; + + /** + * Emitted when the shard is fully ready. + * This event is emitted if: + * * all guilds were received by this shard + * * the ready timeout expired, and some guilds are unavailable + * @event WebSocketShard#allReady + * @param {?Set} unavailableGuilds Set of unavailable guilds, if any + */ + this.emit(ShardEvents.ALL_READY); + return; + } + // Step 2. Create a 15s timeout that will mark the shard as ready if there are still unavailable guilds + this.readyTimeout = this.manager.client.setTimeout(() => { + this.debug(`Shard did not receive any more guild packets in 15 seconds. + Unavailable guild count: ${this.expectedGuilds.size}`); + + this.readyTimeout = undefined; + + this.status = Status.READY; + + this.emit(ShardEvents.ALL_READY, this.expectedGuilds); + }, 15000); + } + /** * Sets the HELLO packet timeout. * @param {number} [time] If set to -1, it will clear the hello timeout timeout @@ -434,7 +482,7 @@ class WebSocketShard extends EventEmitter { if (this.helloTimeout) { this.debug('Clearing the HELLO timeout.'); this.manager.client.clearTimeout(this.helloTimeout); - this.helloTimeout = null; + this.helloTimeout = undefined; } return; } @@ -455,26 +503,39 @@ class WebSocketShard extends EventEmitter { if (this.heartbeatInterval) { this.debug('Clearing the heartbeat interval.'); this.manager.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; + this.heartbeatInterval = undefined; } return; } this.debug(`Setting a heartbeat interval for ${time}ms.`); + // Sanity checks + if (this.heartbeatInterval) this.manager.client.clearInterval(this.heartbeatInterval); this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); } /** * Sends a heartbeat to the WebSocket. * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect + * @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent + * @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully. * @private */ - sendHeartbeat() { - if (!this.lastHeartbeatAcked) { - this.debug("Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting."); + sendHeartbeat(tag = 'HeartbeatTimer', + ignoreHeartbeatAck = [Status.WAITING_FOR_GUILDS, Status.IDENTIFYING, Status.RESUMING].includes(this.status)) { + if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) { + this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`); + } else if (!this.lastHeartbeatAcked) { + this.debug( + `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. + Status : ${STATUS_KEYS[this.status]} + Sequence : ${this.sequence} + Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}` + ); this.destroy(4009); return; } - this.debug('Sending a heartbeat.'); + + this.debug(`[${tag}] Sending a heartbeat.`); this.lastHeartbeatAcked = false; this.lastPingTimestamp = Date.now(); this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }, true); @@ -488,8 +549,7 @@ class WebSocketShard extends EventEmitter { this.lastHeartbeatAcked = true; const latency = Date.now() - this.lastPingTimestamp; this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`); - this.pings.unshift(latency); - if (this.pings.length > 3) this.pings.length = 3; + this.ping = latency; } /** @@ -508,18 +568,20 @@ class WebSocketShard extends EventEmitter { identifyNew() { const { client } = this.manager; if (!client.token) { - this.debug('No token available to identify a new session.'); + this.debug('[IDENTIFY] No token available to identify a new session.'); return; } + this.status = Status.IDENTIFYING; + // Clone the identify payload and assign the token and shard info const d = { ...client.options.ws, token: client.token, - shard: [this.id, Number(client.options.totalShardCount)], + shard: [this.id, Number(client.options.shardCount)], }; - this.debug(`Identifying as a new session. Shard ${this.id}/${client.options.totalShardCount}`); + this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount}`); this.send({ op: OPCodes.IDENTIFY, d }, true); } @@ -529,12 +591,14 @@ class WebSocketShard extends EventEmitter { */ identifyResume() { if (!this.sessionID) { - this.debug('Warning: attempted to resume but no session ID was present; identifying as a new session.'); + this.debug('[RESUME] No session ID was present; identifying as a new session.'); this.identifyNew(); return; } - this.debug(`Attempting to resume session ${this.sessionID} at sequence ${this.closeSequence}`); + this.status = Status.RESUMING; + + this.debug(`[RESUME] Session ${this.sessionID}, sequence ${this.closeSequence}`); const d = { token: this.manager.client.token, @@ -600,15 +664,17 @@ class WebSocketShard extends EventEmitter { /** * Destroys this shard and closes its WebSocket connection. * @param {number} [closeCode=1000] The close code to use + * @param {boolean} [cleanup=false] If the shard should attempt a reconnect * @private */ - destroy(closeCode = 1000) { + destroy(closeCode = 1000, cleanup = false) { this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); + // Close the WebSocket connection, if any - if (this.connection && this.connection.readyState !== WebSocket.CLOSED) { + if (this.connection && this.connection.readyState === WebSocket.OPEN) { this.connection.close(closeCode); - } else { + } else if (!cleanup) { /** * Emitted when a shard is destroyed, but no WebSocket connection was present. * @private @@ -616,9 +682,11 @@ class WebSocketShard extends EventEmitter { */ this.emit(ShardEvents.DESTROYED); } + this.connection = null; // Set the shard status this.status = Status.DISCONNECTED; + if (this.sequence !== -1) this.closeSequence = this.sequence; // Reset the sequence this.sequence = -1; // Reset the ratelimit data diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js index 4fa89edc..cbb60e13 100644 --- a/src/client/websocket/handlers/GUILD_BAN_ADD.js +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -4,7 +4,7 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { const guild = client.guilds.get(data.guild_id); - const user = client.users.get(data.user.id); + const user = client.users.add(data.user); /** * Emitted whenever a member is banned from a guild. diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index 9f13c658..33cc0c23 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -8,20 +8,28 @@ module.exports = async (client, { d: data }, shard) => { if (!guild.available && !data.unavailable) { // A newly available guild guild._patch(data); - client.ws.checkReady(); + // If the client was ready before and we had unavailable guilds, fetch them + if (client.ws.status === Status.READY && client.options.fetchAllMembers) { + await guild.members.fetch().catch(err => + client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`) + ); + } } } else { // A new guild data.shardID = shard.id; guild = client.guilds.add(data); - const emitEvent = client.ws.status === Status.READY; - if (emitEvent) { + if (client.ws.status === Status.READY) { /** * Emitted whenever the client joins a guild. * @event Client#guildCreate * @param {Guild} guild The created guild */ - if (client.options.fetchAllMembers) await guild.members.fetch(); + if (client.options.fetchAllMembers) { + await guild.members.fetch().catch(err => + client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`) + ); + } client.emit(Events.GUILD_CREATE, guild); } } diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 039f8f23..1612cfa0 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -16,6 +16,4 @@ module.exports = (client, { d: data }, shard) => { guild.shardID = shard.id; client.guilds.add(guild); } - - client.ws.checkReady(); }; diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 6533b4f8..04d224db 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -55,7 +55,7 @@ class Shard extends EventEmitter { this.env = Object.assign({}, process.env, { SHARDING_MANAGER: true, SHARDS: this.id, - TOTAL_SHARD_COUNT: this.manager.totalShards, + SHARD_COUNT: this.manager.totalShards, DISCORD_TOKEN: this.manager.token, }); diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 4d0b4a43..8ed1b978 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -60,7 +60,7 @@ class ShardClientUtil { * @readonly */ get count() { - return this.client.options.totalShardCount; + return this.client.options.shardCount; } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 12ddfbf8..06f12f36 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -7,9 +7,10 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number|number[]} [shards] ID of the shard to run, or an array of shard IDs - * @property {number} [shardCount=1] Total number of shards that will be spawned by this Client - * @property {number} [totalShardCount=1] The total amount of shards used by all processes of this bot + * @property {number|number[]|string} [shards] ID of the shard to run, or an array of shard IDs. If not specified, + * the client will spawn {@link ClientOptions#shardCount} shards. If set to `auto`, it will fetch the + * recommended amount of shards from Discord and spawn that amount + * @property {number} [shardCount=1] The total amount of shards used by all processes of this bot * (e.g. recommended shard count, shard count of the ShardingManager) * @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel * (-1 or Infinity for unlimited - don't do this without message sweeping, otherwise memory usage will climb @@ -42,7 +43,6 @@ const browser = exports.browser = typeof window !== 'undefined'; */ exports.DefaultOptions = { shardCount: 1, - totalShardCount: 1, messageCacheMaxSize: 200, messageCacheLifetime: 0, messageSweepInterval: 0, @@ -163,6 +163,9 @@ exports.Endpoints = { * * IDLE: 3 * * NEARLY: 4 * * DISCONNECTED: 5 + * * WAITING_FOR_GUILDS: 6 + * * IDENTIFYING: 7 + * * RESUMING: 8 * @typedef {number} Status */ exports.Status = { @@ -172,6 +175,9 @@ exports.Status = { IDLE: 3, NEARLY: 4, DISCONNECTED: 5, + WAITING_FOR_GUILDS: 6, + IDENTIFYING: 7, + RESUMING: 8, }; /** @@ -279,6 +285,7 @@ exports.ShardEvents = { INVALID_SESSION: 'invalidSession', READY: 'ready', RESUMED: 'resumed', + ALL_READY: 'allReady', }; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 9eb0b453..27898c18 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1650,6 +1650,7 @@ declare module 'discord.js' { public on(event: WSEventType, listener: (data: any, shardID: number) => void): this; public once(event: WSEventType, listener: (data: any, shardID: number) => void): this; + private debug(message: string, shard?: WebSocketShard): void; private connect(): Promise; private createShards(): Promise; @@ -1657,9 +1658,9 @@ declare module 'discord.js' { private broadcast(packet: object): void; private destroy(): void; private _handleSessionLimit(remaining?: number, resetAfter?: number): Promise; - private handlePacket(packet?: object, shard?: WebSocketShard): Promise; - private checkReady(): boolean; - private triggerReady(): void; + private handlePacket(packet?: object, shard?: WebSocketShard): boolean; + private checkShardsReady(): Promise; + private triggerClientReady(): void; } export class WebSocketShard extends EventEmitter { @@ -1671,14 +1672,15 @@ declare module 'discord.js' { private lastHeartbeatAcked: boolean; private ratelimit: { queue: object[]; total: number; remaining: number; time: 60e3; timer: NodeJS.Timeout | null; }; private connection: WebSocket | null; - private helloTimeout: NodeJS.Timeout | null; + private helloTimeout: NodeJS.Timeout | undefined; private eventsAttached: boolean; + private expectedGuilds: Set | undefined; + private readyTimeout: NodeJS.Timeout | undefined; public manager: WebSocketManager; public id: number; public status: Status; - public pings: [number, number, number]; - public readonly ping: number; + public ping: number; private debug(message: string): void; private connect(): Promise; @@ -1687,6 +1689,7 @@ declare module 'discord.js' { private onError(error: ErrorEvent | object): void; private onClose(event: CloseEvent): void; private onPacket(packet: object): void; + private checkReady(): void; private setHelloTimeout(time?: number): void; private setHeartbeatTimer(time: number): void; private sendHeartbeat(): void; @@ -1703,12 +1706,14 @@ declare module 'discord.js' { public on(event: 'resumed', listener: () => void): this; public on(event: 'close', listener: (event: CloseEvent) => void): this; public on(event: 'invalidSession', listener: () => void): this; + public on(event: 'allReady', listener: (unavailableGuilds?: Set) => void): this; public on(event: string, listener: Function): this; public once(event: 'ready', listener: () => void): this; public once(event: 'resumed', listener: () => void): this; public once(event: 'close', listener: (event: CloseEvent) => void): this; public once(event: 'invalidSession', listener: () => void): this; + public once(event: 'allReady', listener: (unavailableGuilds?: Set) => void): this; public once(event: string, listener: Function): this; } @@ -2054,9 +2059,8 @@ declare module 'discord.js' { } interface ClientOptions { - shards?: number | number[]; - shardCount?: number | 'auto'; - totalShardCount?: number; + shards?: number | number[] | 'auto'; + shardCount?: number; messageCacheMaxSize?: number; messageCacheLifetime?: number; messageSweepInterval?: number; From 7d74e7e4196b42e45b57c6c20461664468bd8e43 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 20 Dec 2019 11:58:46 +0100 Subject: [PATCH 312/428] typings(Extendable): add missing channels (#3581) --- typings/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 27898c18..c8400d18 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2148,6 +2148,8 @@ declare module 'discord.js' { TextChannel: typeof TextChannel; VoiceChannel: typeof VoiceChannel; CategoryChannel: typeof CategoryChannel; + NewsChannel: typeof NewsChannel; + StoreChannel: typeof StoreChannel; GuildMember: typeof GuildMember; Guild: typeof Guild; Message: typeof Message; From 99e8d3c540ae2c54effafb945acdd22b5241e58d Mon Sep 17 00:00:00 2001 From: Sugden Date: Sat, 21 Dec 2019 19:32:24 +0000 Subject: [PATCH 313/428] cleanup: remove acknowledge method from TextChannel & DMChannel (#3635) * Update TextChannel.js * remove acknowledge method --- src/structures/DMChannel.js | 1 - src/structures/TextChannel.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index e172f226..02dca0d1 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -83,7 +83,6 @@ class DMChannel extends Channel { createMessageCollector() {} awaitMessages() {} // Doesn't work on DM channels; bulkDelete() {} - acknowledge() {} _cacheMessage() {} } diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 55c3b0d2..66212e70 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -138,7 +138,6 @@ class TextChannel extends GuildChannel { createMessageCollector() {} awaitMessages() {} bulkDelete() {} - acknowledge() {} } TextBasedChannel.applyToClass(TextChannel, true); From e13b3f550dd550b5c3fb8ae211a456d330a7768f Mon Sep 17 00:00:00 2001 From: Charlie Date: Sat, 21 Dec 2019 21:12:35 +0100 Subject: [PATCH 314/428] typings: TextChannel.topic & NewsChannel.topic should be nullable (#3628) * Fix GuildChannel#topic to be optional * Update typings/index.d.ts Implement the suggested change from optional to null return Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * Update typings/index.d.ts Implement the suggested change from optional to null return Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> Co-authored-by: izexi <43889168+izexi@users.noreply.github.com> --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c8400d18..d26160fe 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1373,7 +1373,7 @@ declare module 'discord.js' { public messages: MessageStore; public nsfw: boolean; public rateLimitPerUser: number; - public topic: string; + public topic: string | null; public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; @@ -1384,7 +1384,7 @@ declare module 'discord.js' { constructor(guild: Guild, data?: object); public messages: MessageStore; public nsfw: boolean; - public topic: string; + public topic: string | null; public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; public fetchWebhooks(): Promise>; From f578cce9ac75114c51f7a4538974817a3836d3fd Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Sat, 21 Dec 2019 12:27:14 -0800 Subject: [PATCH 315/428] feat(Guild): add systemChannelFlags (#3559) * Add systemChannelFlags bitfield to Guild * Implement @vladfrangu's suggestions * fix: apply suggestions, reverse order of flags, reword docs * docs: add SystemCHannelFlagsResolvable typedef Co-authored-by: SpaceEEC --- src/index.js | 1 + src/structures/Guild.js | 21 +++++++++++++++++++++ src/util/SystemChannelFlags.js | 33 +++++++++++++++++++++++++++++++++ typings/index.d.ts | 13 +++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/util/SystemChannelFlags.js diff --git a/src/index.js b/src/index.js index 2d776e7c..cbc0d5a7 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,7 @@ module.exports = { Snowflake: require('./util/Snowflake'), SnowflakeUtil: require('./util/Snowflake'), Structures: require('./util/Structures'), + SystemChannelFlags: require('./util/SystemChannelFlags'), Util: Util, util: Util, version: require('../package.json').version, diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 75925663..987fa38f 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -10,6 +10,7 @@ const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); const Snowflake = require('../util/Snowflake'); +const SystemChannelFlags = require('../util/SystemChannelFlags'); const GuildMemberStore = require('../stores/GuildMemberStore'); const RoleStore = require('../stores/RoleStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); @@ -275,6 +276,12 @@ class Guild extends Base { this.defaultMessageNotifications = DefaultMessageNotifications[data.default_message_notifications] || data.default_message_notifications; + /** + * The value set for the guild's system channel flags + * @type {Readonly} + */ + this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze(); + /** * The maximum amount of members the guild can have * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter @@ -773,6 +780,7 @@ class Guild extends Base { * @property {Base64Resolvable} [splash] The splash screen of the guild * @property {Base64Resolvable} [banner] The banner of the guild * @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications + * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild */ /** @@ -813,6 +821,9 @@ class Guild extends Base { DefaultMessageNotifications.indexOf(data.defaultMessageNotifications) : Number(data.defaultMessageNotifications); } + if (typeof data.systemChannelFlags !== 'undefined') { + _data.systemChannelFlags = SystemChannelFlags.resolve(data.systemChannelFlags); + } return this.client.api.guilds(this.id).patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } @@ -839,6 +850,16 @@ class Guild extends Base { } /* eslint-enable max-len */ + /** + * Edits the flags of the default message notifications of the guild. + * @param {SystemChannelFlagsResolvable} systemChannelFlags The new flags for the default message notifications + * @param {string} [reason] Reason for changing the flags of the default message notifications + * @returns {Promise} + */ + setSystemChannelFlags(systemChannelFlags, reason) { + return this.edit({ systemChannelFlags }, reason); + } + /** * Edits the name of the guild. * @param {string} name The new name of the guild diff --git a/src/util/SystemChannelFlags.js b/src/util/SystemChannelFlags.js new file mode 100644 index 00000000..14e8fd4f --- /dev/null +++ b/src/util/SystemChannelFlags.js @@ -0,0 +1,33 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link Guild#systemChannelFlags} bitfield. + * Note that all event message types are enabled by default, + * and by setting their corresponding flags you are disabling them + * @extends {BitField} + */ +class SystemChannelFlags extends BitField { + /** + * Data that can be resolved to give a sytem channel flag bitfield. This can be: + * * A string (see {@link SystemChannelFlags.FLAGS}) + * * A sytem channel flag + * * An instance of SystemChannelFlags + * * An Array of SystemChannelFlagsResolvable + * @typedef {string|number|SystemChannelFlags|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable + */ +} + +/** + * Numeric system channel flags. All available properties: + * * `WELCOME_MESSAGE_DISABLED` + * * `BOOST_MESSAGE_DISABLED` + * @type {Object} + */ +SystemChannelFlags.FLAGS = { + WELCOME_MESSAGE_DISABLED: 1 << 0, + BOOST_MESSAGE_DISABLED: 1 << 1, +}; + +module.exports = SystemChannelFlags; diff --git a/typings/index.d.ts b/typings/index.d.ts index d26160fe..7ab448e7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -714,6 +714,7 @@ declare module 'discord.js' { public shardID: number; public splash: string | null; public readonly systemChannel: TextChannel | null; + public systemChannelFlags: Readonly; public systemChannelID: Snowflake | null; public vanityURLCode: string | null; public verificationLevel: number; @@ -755,6 +756,7 @@ declare module 'discord.js' { public setRolePositions(rolePositions: RolePosition[]): Promise; public setSplash(splash: Base64Resolvable | null, reason?: string): Promise; public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; + public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; public splashURL(options?: AvatarOptions): string | null; public toJSON(): object; @@ -1368,6 +1370,11 @@ declare module 'discord.js' { static extend(structure: string, extender: (baseClass: typeof Function) => T): T; } + export class SystemChannelFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + export class TextChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); public messages: MessageStore; @@ -2285,6 +2292,7 @@ declare module 'discord.js' { defaultMessageNotifications?: DefaultMessageNotifications | number; afkChannel?: ChannelResolvable; systemChannel?: ChannelResolvable; + systemChannelFlags?: SystemChannelFlags; afkTimeout?: number; icon?: Base64Resolvable; owner?: GuildMemberResolvable; @@ -2613,6 +2621,11 @@ declare module 'discord.js' { type StringResolvable = string | string[] | any; + type SystemChannelFlagsString = 'WELCOME_MESSAGE_DISABLED' + | 'BOOST_MESSAGE_DISABLED'; + + type SystemChannelFlagsResolvable = BitFieldResolvable; + type TargetUser = number; type UserResolvable = User | Snowflake | Message | GuildMember; From b4f00bfb6be6e6db25ca33122bd6e4318f081bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Sat, 21 Dec 2019 21:28:09 +0100 Subject: [PATCH 316/428] feat: widen GuildResolvable to include more structures (#3512) * feat: Widen GuildResolvable to include GuildChannel and GuildMember * docs: Documented the new overloads Co-Authored-By: Gryffon Bellish Co-authored-by: Gryffon Bellish --- src/stores/GuildStore.js | 19 ++++++++++++++++++- typings/index.d.ts | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index 90e4358d..eb7090b6 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -4,6 +4,9 @@ const DataStore = require('./DataStore'); const DataResolver = require('../util/DataResolver'); const { Events } = require('../util/Constants'); const Guild = require('../structures/Guild'); +const GuildChannel = require('../structures/GuildChannel'); +const GuildMember = require('../structures/GuildMember'); +const Role = require('../structures/Role'); /** * Stores guilds. @@ -17,8 +20,10 @@ class GuildStore extends DataStore { /** * Data that resolves to give a Guild object. This can be: * * A Guild object + * * A GuildChannel object + * * A Role object * * A Snowflake - * @typedef {Guild|Snowflake} GuildResolvable + * @typedef {Guild|GuildChannel|GuildMember|Role|Snowflake} GuildResolvable */ /** @@ -29,6 +34,12 @@ class GuildStore extends DataStore { * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Guild} */ + resolve(guild) { + if (guild instanceof GuildChannel || + guild instanceof GuildMember || + guild instanceof Role) return super.resolve(guild.guild); + return super.resolve(guild); + } /** * Resolves a GuildResolvable to a Guild ID string. @@ -38,6 +49,12 @@ class GuildStore extends DataStore { * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Snowflake} */ + resolveID(guild) { + if (guild instanceof GuildChannel || + guild instanceof GuildMember || + guild instanceof Role) return super.resolveID(guild.guild.id); + return super.resolveID(guild); + } /** * Creates a guild. diff --git a/typings/index.d.ts b/typings/index.d.ts index 7ab448e7..348a7ba9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2328,7 +2328,7 @@ declare module 'discord.js' { type GuildMemberResolvable = GuildMember | UserResolvable; - type GuildResolvable = Guild | Snowflake; + type GuildResolvable = Guild | GuildChannel | GuildMember | Role | Snowflake; interface GuildPruneMembersOptions { count?: boolean; From 710101c5805935d7804fc8e53fa130541f248c54 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 22 Dec 2019 11:31:26 +0200 Subject: [PATCH 317/428] src(WebSocket): fix race condition (#3636) A race condition caused Client#user to be null in the ready event if the client handled 0 guilds. --- src/client/websocket/WebSocketShard.js | 1 - src/client/websocket/handlers/READY.js | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 8d9afd31..5171d7e5 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -373,7 +373,6 @@ class WebSocketShard extends EventEmitter { this.debug(`[READY] Session ${this.sessionID}.`); this.lastHeartbeatAcked = true; this.sendHeartbeat('ReadyHeartbeat'); - this.checkReady(); break; case WSEvents.RESUMED: { /** diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 1612cfa0..00257502 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -16,4 +16,6 @@ module.exports = (client, { d: data }, shard) => { guild.shardID = shard.id; client.guilds.add(guild); } + + shard.checkReady(); }; From 45b89710008d207da22000dcb633c0c0236db17e Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Mon, 23 Dec 2019 17:01:07 -0500 Subject: [PATCH 318/428] deps: mark utf-8-validate as optional, remove mentions of uws and zucc (#3638) * Mark utf-8-validate as optional * remove uws and zucc --- package.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c8decaa3..6522e2f4 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,6 @@ "zlib-sync": "^0.1.6" }, "peerDependenciesMeta": { - "@discordjs/uws": { - "optional": true - }, "bufferutil": { "optional": true }, @@ -68,10 +65,10 @@ "sodium": { "optional": true }, - "zlib-sync": { + "utf-8-validate": { "optional": true }, - "zucc": { + "zlib-sync": { "optional": true } }, From d4333f5bbe0aff3557ac51b7acb8ec62eb927a47 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Tue, 24 Dec 2019 21:28:09 -0500 Subject: [PATCH 319/428] chore: node version in package.json (#3643) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6522e2f4..a52ade25 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "webpack-cli": "^3.2.3" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" }, "browser": { "https": false, From 50ed3293a57aeb0f9c094f00d49ed9874dc5d87a Mon Sep 17 00:00:00 2001 From: NightScript Date: Tue, 24 Dec 2019 18:29:19 -0800 Subject: [PATCH 320/428] chore: issue config refactor (#3640) * Create config.yml Instead of making an entire new page with just text talking about the discord server (which they could ignore, as most people don't read), just link people directly to the discord server * Delete question---general-support-request.md --- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ .../question---general-support-request.md | 13 ------------- 2 files changed, 5 insertions(+), 13 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/question---general-support-request.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..2584693c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: discord.js discord server + url: https://discord.gg/bRCvFy9 + about: Please use this Discord Server to ask questions and get support. We don't typically answer questions here and they will likely be closed and redirected to the Discord server. diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md deleted file mode 100644 index d48d4940..00000000 --- a/.github/ISSUE_TEMPLATE/question---general-support-request.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Question / General support request -about: Ask for help in Discord instead - https://discord.gg/bRCvFy9 -title: '' -labels: question (please use Discord instead) -assignees: '' - ---- - -Seriously, we only use this issue tracker for bugs in the library itself and feature requests for it. -We don't typically answer questions or help with support issues here. - -If you have a question or need support on our library, please read our [Support Document](https://github.com/discordjs/discord.js/blob/master/.github/SUPPORT.md) From fc27ce1a1533a3c412bcdd9b1ad25ef40b11f544 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 27 Dec 2019 19:26:41 +0100 Subject: [PATCH 321/428] typings(Bitfield): add hasParams to toArray, fix serialize's type (#3579) * typings(Bitfield): add hasParams to toArray, fix serialize's type * fix: apply suggested changes * chore: remove incorrect whitespace * fix: make params optional * nit: pluralize bit in Permissions#missing * nit: group non-static methods together --- typings/index.d.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 348a7ba9..ed088226 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -118,10 +118,10 @@ declare module 'discord.js' { public equals(bit: BitFieldResolvable): boolean; public freeze(): Readonly>; public has(bit: BitFieldResolvable): boolean; - public missing(bits: BitFieldResolvable, ...hasParams: any[]): S[]; + public missing(bits: BitFieldResolvable, ...hasParam: readonly unknown[]): S[]; public remove(...bits: BitFieldResolvable[]): BitField; - public serialize(...hasParams: BitFieldResolvable[]): Record; - public toArray(): S[]; + public serialize(...hasParam: readonly unknown[]): Record; + public toArray(...hasParam: readonly unknown[]): S[]; public toJSON(): number; public valueOf(): number; public [Symbol.iterator](): IterableIterator; @@ -1124,6 +1124,9 @@ declare module 'discord.js' { export class Permissions extends BitField { public any(permission: PermissionResolvable, checkAdmin?: boolean): boolean; public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; + public missing(bits: BitFieldResolvable, checkAdmin?: boolean): PermissionString[]; + public serialize(checkAdmin?: boolean): Record; + public toArray(checkAdmin?: boolean): PermissionString[]; public static ALL: number; public static DEFAULT: number; From ea76a5663937afe78b58076eefb6ab1ed2fc1795 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 27 Dec 2019 19:27:34 +0100 Subject: [PATCH 322/428] feat(Webhook): add type property and created* getters (#3585) * feat(Webhook): add created* getters * feat(Webhook): add type property * typings(WebhookFields): use primitive string for url getter Co-Authored-By: Gryffon Bellish * fix(Webhook): token can be null Co-authored-by: Gryffon Bellish --- src/structures/Webhook.js | 32 ++++++++++++++++++++++++++++++-- src/util/Constants.js | 13 +++++++++++++ typings/index.d.ts | 10 ++++++++-- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index da7dcd12..b4c228e9 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,6 +1,8 @@ 'use strict'; +const { WebhookTypes } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); +const Snowflake = require('../util/Snowflake'); const Channel = require('./Channel'); const APIMessage = require('./APIMessage'); @@ -29,9 +31,9 @@ class Webhook { /** * The token for the webhook * @name Webhook#token - * @type {string} + * @type {?string} */ - Object.defineProperty(this, 'token', { value: data.token, writable: true, configurable: true }); + Object.defineProperty(this, 'token', { value: data.token || null, writable: true, configurable: true }); /** * The avatar for the webhook @@ -45,6 +47,12 @@ class Webhook { */ this.id = data.id; + /** + * The type of the webhook + * @type {WebhookTypes} + */ + this.type = WebhookTypes[data.type]; + /** * The guild the webhook belongs to * @type {Snowflake} @@ -210,6 +218,23 @@ class Webhook { delete(reason) { return this.client.api.webhooks(this.id, this.token).delete({ reason }); } + /** + * The timestamp the webhook was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the webhook was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } /** * The url of this webhook @@ -236,6 +261,9 @@ class Webhook { 'sendSlackMessage', 'edit', 'delete', + 'createdTimestamp', + 'createdAt', + 'url', ]) { Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(Webhook.prototype, prop)); diff --git a/src/util/Constants.js b/src/util/Constants.js index 06f12f36..4c2f6059 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -628,6 +628,19 @@ exports.MembershipStates = [ 'ACCEPTED', ]; +/** + * The value set for a webhook's type: + * * Incoming + * * Channel Follower + * @typedef {string} WebhookTypes + */ +exports.WebhookTypes = [ + // They start at 1 + null, + 'Incoming', + 'Channel Follower', +]; + function keyMirror(arr) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; diff --git a/typings/index.d.ts b/typings/index.d.ts index ed088226..de3f707a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1636,11 +1636,13 @@ declare module 'discord.js' { public guildID: Snowflake; public name: string; public owner: User | object | null; - public readonly url: string; + public token: string | null; + public type: WebhookTypes; } export class WebhookClient extends WebhookMixin(BaseClient) { constructor(id: string, token: string, options?: ClientOptions); + public token: string; } export class WebSocketManager extends EventEmitter { @@ -1909,7 +1911,9 @@ declare module 'discord.js' { interface WebhookFields { readonly client: Client; id: Snowflake; - token: string; + readonly createdAt: Date; + readonly createdTimestamp: number; + readonly url: string; delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; send(content?: StringResolvable, options?: WebhookMessageOptions & { split?: false } | MessageAdditions): Promise; @@ -2654,6 +2658,8 @@ declare module 'discord.js' { split?: boolean | SplitOptions; } + type WebhookTypes = 'Incoming' | 'Channel Follower'; + interface WebSocketOptions { large_threshold?: number; compress?: boolean; From e660ea90cc2553c53dbc702ccb49de26916a9eec Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 27 Dec 2019 19:27:48 +0100 Subject: [PATCH 323/428] fix(Webhook): edit channel when editing avatar (#3588) --- src/structures/Webhook.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index b4c228e9..9b6b43c8 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -194,20 +194,20 @@ class Webhook { * @param {string} [reason] Reason for editing this webhook * @returns {Promise} */ - edit({ name = this.name, avatar, channel }, reason) { + async edit({ name = this.name, avatar, channel }, reason) { if (avatar && (typeof avatar === 'string' && !avatar.startsWith('data:'))) { - return DataResolver.resolveImage(avatar).then(image => this.edit({ name, avatar: image }, reason)); + avatar = await DataResolver.resolveImage(avatar); } if (channel) channel = channel instanceof Channel ? channel.id : channel; - return this.client.api.webhooks(this.id, channel ? undefined : this.token).patch({ + const data = await this.client.api.webhooks(this.id, channel ? undefined : this.token).patch({ data: { name, avatar, channel_id: channel }, reason, - }).then(data => { - this.name = data.name; - this.avatar = data.avatar; - this.channelID = data.channel_id; - return this; }); + + this.name = data.name; + this.avatar = data.avatar; + this.channelID = data.channel_id; + return this; } /** From 97eac663b3e1292cc399c56eb701069301f4637d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 27 Dec 2019 19:28:04 +0100 Subject: [PATCH 324/428] feat(MessageMentions): cache mentioned members (#3601) --- src/structures/MessageMentions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 9b17685f..29a029bf 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -48,6 +48,9 @@ class MessageMentions { } else { this.users = new Collection(); for (const mention of users) { + if (mention.member && message.guild) { + message.guild.members.add(Object.assign(mention.member, { user: mention })); + } const user = message.client.users.add(mention); this.users.set(user.id, user); } From c734979ad4dc4be6999b3b1eef73d55b439c0ae4 Mon Sep 17 00:00:00 2001 From: Cadence Fish Date: Sat, 4 Jan 2020 01:53:27 +1300 Subject: [PATCH 325/428] typings(ShardingManager): add options.shardList (#3657) --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index de3f707a..b731c05e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1285,6 +1285,7 @@ declare module 'discord.js' { export class ShardingManager extends EventEmitter { constructor(file: string, options?: { totalShards?: number | 'auto'; + shardList?: number[] | 'auto'; mode?: ShardingManagerMode; respawn?: boolean; shardArgs?: string[]; From 155b682f6c9b0be7525141c054257657539b29ae Mon Sep 17 00:00:00 2001 From: Jyguy Date: Sat, 4 Jan 2020 17:23:01 -0500 Subject: [PATCH 326/428] typings(GuildEmoji): make url not-nullable (#3656) * typings(GuildEmoji): make url not-nullable * make GuildEmoji.url readonly --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index b731c05e..63942fff 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -843,6 +843,7 @@ declare module 'discord.js' { public managed: boolean; public requiresColons: boolean; public roles: GuildEmojiRoleStore; + public readonly url: string; public delete(reason?: string): Promise; public edit(data: GuildEmojiEditData, reason?: string): Promise; public equals(other: GuildEmoji | object): boolean; From bf31b28ad95ce98840aac898209715b2c7b450ad Mon Sep 17 00:00:00 2001 From: tipakA <31581159+tipakA@users.noreply.github.com> Date: Sun, 5 Jan 2020 00:50:38 +0100 Subject: [PATCH 327/428] feat(RichPresenceAssets): add Twitch preview link for largeImageURL (#3655) --- src/structures/Presence.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 2eccf7cb..8c207989 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -312,6 +312,8 @@ class RichPresenceAssets { if (!this.largeImage) return null; if (/^spotify:/.test(this.largeImage)) { return `https://i.scdn.co/image/${this.largeImage.slice(8)}`; + } else if (/^twitch:/.test(this.largeImage)) { + return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${this.largeImage.slice(7)}.png`; } return this.activity.presence.client.rest.cdn .AppAsset(this.activity.applicationID, this.largeImage, { format, size }); From 6af0da10436b13b29033a1da0a7ea96598651029 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sun, 5 Jan 2020 15:45:49 +0000 Subject: [PATCH 328/428] feat(Partials): add DMChannel/MessageReaction#fetch() and PartialTypes.REACTION (#3474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add DMChannel#fetch() & Action#getChannel({recipients}) * ref for MessageReaction partial * typings * add PartialTypes.REACTION * accommodate for fully removed reactions * fix incorrect wording and typo * typings: MessageReaction#count is nullable * typings: mark MessageReaction#partial as readonly Co-Authored-By: Vlad Frangu * fix(User): fetch dm channel if cached one is partial * docs: add missing comma Co-Authored-By: Antonio Román * fix: accomodate for new reactions * fix: updating existing/new count on _patch * docs: typo * for consistency Co-authored-by: Vlad Frangu Co-authored-by: SpaceEEC Co-authored-by: Antonio Román --- docs/topics/partials.md | 8 ++++-- src/client/actions/Action.js | 5 ++-- src/client/actions/MessageReactionAdd.js | 5 +++- src/stores/ReactionStore.js | 22 +++++++++++++++ src/structures/DMChannel.js | 10 ++++++- src/structures/MessageReaction.js | 35 ++++++++++++++++++++---- src/structures/User.js | 2 +- src/util/Constants.js | 2 ++ typings/index.d.ts | 8 ++++-- 9 files changed, 82 insertions(+), 15 deletions(-) diff --git a/docs/topics/partials.md b/docs/topics/partials.md index f1f05fe2..c930d0fd 100644 --- a/docs/topics/partials.md +++ b/docs/topics/partials.md @@ -9,8 +9,8 @@ discard the event. With partials, you're able to receive the event, with a Messa Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](/#/docs/main/master/typedef/PartialType): ```js -// Accept partial messages and DM channels when emitting events -new Client({ partials: ['MESSAGE', 'CHANNEL'] }); +// Accept partial messages, DM channels, and reactions when emitting events +new Client({ partials: ['MESSAGE', 'CHANNEL', 'REACTION'] }); ``` ## Usage & warnings @@ -45,6 +45,10 @@ client.on('messageReactionAdd', async (reaction, user) => { if (reaction.message.partial) await reaction.message.fetch(); // Now the message has been cached and is fully available: console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`); + // Fetches and caches the reaction itself, updating resources that were possibly defunct. + if (reaction.partial) await reaction.fetch(); + // Now the reaction is fully available and the properties will be reflected accurately: + console.log(`${reaction.count} user(s) have given the same reaction to this message!`); }); ``` diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 7f21318b..bc9ed267 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -36,6 +36,7 @@ class GenericAction { return data.channel || this.getPayload({ id, guild_id: data.guild_id, + recipients: [data.author || { id: data.user_id }], }, this.client.channels, id, PartialTypes.CHANNEL); } @@ -52,9 +53,9 @@ class GenericAction { const id = data.emoji.id || decodeURIComponent(data.emoji.name); return this.getPayload({ emoji: data.emoji, - count: 0, + count: message.partial ? null : 0, me: user.id === this.client.user.id, - }, message.reactions, id, PartialTypes.MESSAGE); + }, message.reactions, id, PartialTypes.REACTION); } getMember(data, guild) { diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index e7ae7e26..22287dbd 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -2,6 +2,7 @@ const Action = require('./Action'); const { Events } = require('../../util/Constants'); +const { PartialTypes } = require('../../util/Constants'); /* { user_id: 'id', @@ -26,11 +27,13 @@ class MessageReactionAdd extends Action { if (!message) return false; // Verify reaction + if (message.partial && !this.client.options.partials.includes(PartialTypes.REACTION)) return false; const reaction = message.reactions.add({ emoji: data.emoji, - count: 0, + count: message.partial ? null : 0, me: user.id === this.client.user.id, }); + if (!reaction) return false; reaction._add(user); /** * Emitted whenever a reaction is added to a cached message. diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index 5092e861..1b1fb603 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -50,6 +50,28 @@ class ReactionStore extends DataStore { return this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions.delete() .then(() => this.message); } + + _partial(emoji) { + const id = emoji.id || emoji.name; + const existing = this.get(id); + return !existing || existing.partial; + } + + async _fetchReaction(reactionEmoji, cache) { + const id = reactionEmoji.id || reactionEmoji.name; + const existing = this.get(id); + if (!this._partial(reactionEmoji)) return existing; + const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); + if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { + reactionEmoji.reaction._patch({ count: 0 }); + this.message.reactions.remove(id); + return existing; + } + for (const reaction of data.reactions) { + if (this._partial(reaction.emoji)) this.add(reaction, cache); + } + return existing; + } } module.exports = ReactionStore; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 02dca0d1..006a9fab 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -56,7 +56,15 @@ class DMChannel extends Channel { * @readonly */ get partial() { - return !this.recipient; + return typeof this.lastMessageID === 'undefined'; + } + + /** + * Fetch this DMChannel. + * @returns {Promise} + */ + fetch() { + return this.recipient.createDM(); } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index fe10e428..f32f0843 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -27,12 +27,6 @@ class MessageReaction { */ this.me = data.me; - /** - * The number of people that have given the same reaction - * @type {number} - */ - this.count = data.count || 0; - /** * The users that have given this reaction, mapped by their ID * @type {ReactionUserStore} @@ -40,6 +34,17 @@ class MessageReaction { this.users = new ReactionUserStore(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji); + + this._patch(data); + } + + _patch(data) { + /** + * The number of people that have given the same reaction + * @type {?number} + */ + // eslint-disable-next-line eqeqeq + if (this.count == undefined) this.count = data.count; } /** @@ -63,18 +68,36 @@ class MessageReaction { return this._emoji; } + /** + * Whether or not this reaction is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.count === null; + } + + /** + * Fetch this reaction. + * @returns {Promise} + */ + fetch() { + return this.message.reactions._fetchReaction(this.emoji, true); + } toJSON() { return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' }); } _add(user) { + if (this.partial) return; this.users.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me) this.me = user.id === this.message.client.user.id; } _remove(user) { + if (this.partial) return; this.users.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; diff --git a/src/structures/User.js b/src/structures/User.js index b5358be7..a4b1d3d9 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -218,7 +218,7 @@ class User extends Base { */ async createDM() { const { dmChannel } = this; - if (dmChannel) return dmChannel; + if (dmChannel && !dmChannel.partial) return dmChannel; const data = await this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, } }); diff --git a/src/util/Constants.js b/src/util/Constants.js index 4c2f6059..6ef68127 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -294,6 +294,7 @@ exports.ShardEvents = { * * CHANNEL (only affects DMChannels) * * GUILD_MEMBER * * MESSAGE + * * REACTION * Partials require you to put checks in place when handling data, read the Partials topic listed in the * sidebar for more information. * @typedef {string} PartialType @@ -303,6 +304,7 @@ exports.PartialTypes = keyMirror([ 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', + 'REACTION', ]); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 63942fff..b44df577 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -649,6 +649,7 @@ declare module 'discord.js' { public messages: MessageStore; public recipient: User; public readonly partial: false; + public fetch(): Promise; } export class Emoji extends Base { @@ -1100,11 +1101,13 @@ declare module 'discord.js' { constructor(client: Client, data: object, message: Message); private _emoji: GuildEmoji | ReactionEmoji; - public count: number; + public count: number | null; public readonly emoji: GuildEmoji | ReactionEmoji; public me: boolean; public message: Message; + public readonly partial: boolean; public users: ReactionUserStore; + public fetch(): Promise; public toJSON(): object; } @@ -2539,7 +2542,8 @@ declare module 'discord.js' { type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' - | 'MESSAGE'; + | 'MESSAGE' + | 'REACTION'; type Partialize = { id: string; From d2ef02906c5618d883b1adfb348fd248a4db616f Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Sun, 5 Jan 2020 11:45:16 -0500 Subject: [PATCH 329/428] cleanup(DataResolver): stats can't be falsy (#3651) --- src/util/DataResolver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index 91933eb9..b0218916 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -96,7 +96,7 @@ class DataResolver { const file = browser ? resource : path.resolve(resource); fs.stat(file, (err, stats) => { if (err) return reject(err); - if (!stats || !stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file)); + if (!stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file)); fs.readFile(file, (err2, data) => { if (err2) reject(err2); else resolve(data); From a53d86579b8150de5388eef5d07520b26d3f7d04 Mon Sep 17 00:00:00 2001 From: Saya <36309350+Deivu@users.noreply.github.com> Date: Fri, 10 Jan 2020 06:14:55 +0800 Subject: [PATCH 330/428] typings(BaseClient): remove delay parameter from setImmediate (#3667) There is no delay parameter on setImmediate in Node.JS docs: https://nodejs.org/docs/latest-v12.x/api/timers.html#timers_setimmediate_callback_args --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index b44df577..15cbeee7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -102,7 +102,7 @@ declare module 'discord.js' { public destroy(): void; public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; - public setImmediate(fn: Function, delay: number, ...args: any[]): NodeJS.Immediate; + public setImmediate(fn: Function, ...args: any[]): NodeJS.Immediate; public toJSON(...props: { [key: string]: boolean | string }[]): object; } From f74ae12d6ac8070ff67a2ba9ff6cd5e3e0cbcc42 Mon Sep 17 00:00:00 2001 From: didinele Date: Sat, 11 Jan 2020 21:42:01 +0200 Subject: [PATCH 331/428] fix(typings): remove VoiceChannel#connection (#3676) --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 15cbeee7..1cd568f4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1497,7 +1497,6 @@ declare module 'discord.js' { export class VoiceChannel extends GuildChannel { constructor(guild: Guild, data?: object); public bitrate: number; - public readonly connection: VoiceConnection; public readonly editable: boolean; public readonly full: boolean; public readonly joinable: boolean; From b5825c33b0f07b57d174a7d0bf157b19dc99e73c Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 13 Jan 2020 14:58:40 +0100 Subject: [PATCH 332/428] feat(Speaking): add PRIORITY_SPEAKING bit (#3680) --- src/util/Speaking.js | 2 ++ typings/index.d.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/Speaking.js b/src/util/Speaking.js index c706d995..18c1ae6a 100644 --- a/src/util/Speaking.js +++ b/src/util/Speaking.js @@ -13,12 +13,14 @@ class Speaking extends BitField {} * Numeric speaking flags. All available properties: * * `SPEAKING` * * `SOUNDSHARE` + * * `PRIORITY_SPEAKING` * @type {Object} * @see {@link https://discordapp.com/developers/docs/topics/voice-connections#speaking} */ Speaking.FLAGS = { SPEAKING: 1 << 0, SOUNDSHARE: 1 << 1, + PRIORITY_SPEAKING: 1 << 2, }; module.exports = Speaking; diff --git a/typings/index.d.ts b/typings/index.d.ts index 1cd568f4..832bfae4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2627,7 +2627,7 @@ declare module 'discord.js' { highWaterMark?: number; } - type SpeakingString = 'SPEAKING' | 'SOUNDSHARE'; + type SpeakingString = 'SPEAKING' | 'SOUNDSHARE' | 'PRIORITY_SPEAKING'; type StreamType = 'unknown' | 'converted' | 'opus' | 'ogg/opus' | 'webm/opus'; From 59205a21522a0052128270e5bc1106f6806b8cef Mon Sep 17 00:00:00 2001 From: Souji Date: Mon, 13 Jan 2020 15:02:31 +0100 Subject: [PATCH 333/428] fix: provide count on bulk deletion (#3682) * GuildAuditLogsEntry should provide count as extra in case of MESSAGE_BULK_DELETE * inner class: GuildAuditLogsEntry in GuildAuditLogs.js --- src/structures/GuildAuditLogs.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 53494fa4..d1185bdd 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -323,6 +323,10 @@ class GuildAuditLogsEntry { count: data.options.count, channel: guild.channels.get(data.options.channel_id), }; + } else if (data.action_type === Actions.MESSAGE_BULK_DELETE) { + this.extra = { + count: data.options.count, + }; } else { switch (data.options.type) { case 'member': From 11f9118551b511b5c133eacb2550729e5b696434 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 13 Jan 2020 15:07:54 +0100 Subject: [PATCH 334/428] fix(BitField): remove for..in in favor of Object.entries (#3650) * fix(BitField): remove for..in in favor of Object.keys * refactor: do not re-resolve bits Co-Authored-By: bdistin Co-authored-by: bdistin --- src/util/BitField.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/BitField.js b/src/util/BitField.js index f849e7b8..8de151d0 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -103,7 +103,7 @@ class BitField { */ serialize(...hasParams) { const serialized = {}; - for (const perm in this.constructor.FLAGS) serialized[perm] = this.has(perm, ...hasParams); + for (const [flag, bit] of Object.entries(this.constructor.FLAGS)) serialized[flag] = this.has(bit, ...hasParams); return serialized; } From 400cb563580b6383019325069d40a2be529f4d07 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 13 Jan 2020 15:12:18 +0100 Subject: [PATCH 335/428] fix(ShardingManager): assert shardList to be spawned, not totalShards (#3649) --- src/sharding/ShardingManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index f0298054..7126148d 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -235,7 +235,7 @@ class ShardingManager extends EventEmitter { */ fetchClientValues(prop) { if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS')); - if (this.shards.size !== this.totalShards) return Promise.reject(new Error('SHARDING_IN_PROCESS')); + if (this.shards.size !== this.shardList.length) return Promise.reject(new Error('SHARDING_IN_PROCESS')); const promises = []; for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop)); return Promise.all(promises); From 8014ddcd1c651749fc64e8dfc0b0863422d57203 Mon Sep 17 00:00:00 2001 From: Tenpi <37512637+Tenpi@users.noreply.github.com> Date: Mon, 13 Jan 2020 09:32:29 -0500 Subject: [PATCH 336/428] feat: dynamic property for ImageURLOptions (#3530) * Added dynamic property to ImageURLOptions * fixes * order * typings fix * made dynamic false by default * add curly spaces --- src/structures/Guild.js | 4 ++-- src/structures/User.js | 4 ++-- src/util/Constants.js | 12 +++++++----- typings/index.d.ts | 22 +++++++++++----------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 987fa38f..6b22cd25 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -433,9 +433,9 @@ class Guild extends Base { * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - iconURL({ format, size } = {}) { + iconURL({ format, size, dynamic } = {}) { if (!this.icon) return null; - return this.client.rest.cdn.Icon(this.id, this.icon, format, size); + return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic); } /** diff --git a/src/structures/User.js b/src/structures/User.js index a4b1d3d9..7ae67451 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -140,9 +140,9 @@ class User extends Base { * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - avatarURL({ format, size } = {}) { + avatarURL({ format, size, dynamic } = {}) { if (!this.avatar) return null; - return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size); + return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size, dynamic); } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 6ef68127..a2e86d4a 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -119,7 +119,9 @@ function makeImageUrl(root, { format = 'webp', size } = {}) { * Options for Image URLs. * @typedef {Object} ImageURLOptions * @property {string} [format] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, - * it will be `gif` for animated avatars or otherwise `webp` + * defaults to `webp`. + * @property {boolean} [dynamic] If true, the format will dynamically change to `gif` for + * animated avatars; the default is false. * @property {number} [size] One of `16`, `32`, `64`, `128`, `256`, `512`, `1024`, `2048` */ @@ -129,14 +131,14 @@ exports.Endpoints = { Emoji: (emojiID, format = 'png') => `${root}/emojis/${emojiID}.${format}`, Asset: name => `${root}/assets/${name}`, DefaultAvatar: discriminator => `${root}/embed/avatars/${discriminator}.png`, - Avatar: (userID, hash, format = 'default', size) => { - if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; + Avatar: (userID, hash, format = 'webp', size, dynamic = false) => { + if (dynamic) format = hash.startsWith('a_') ? 'gif' : format; return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); }, Banner: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }), - Icon: (guildID, hash, format = 'default', size) => { - if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; + Icon: (guildID, hash, format = 'webp', size, dynamic = false) => { + if (dynamic) format = hash.startsWith('a_') ? 'gif' : format; return makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }); }, AppIcon: (clientID, hash, { format = 'webp', size } = {}) => diff --git a/typings/index.d.ts b/typings/index.d.ts index 832bfae4..c5eb84f9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -272,9 +272,9 @@ declare module 'discord.js' { public name: string; public owner: User | Team | null; public rpcOrigins: string[]; - public coverImage(options?: AvatarOptions): string; + public coverImage(options?: ImageURLOptions): string; public fetchAssets(): Promise; - public iconURL(options?: AvatarOptions): string; + public iconURL(options?: ImageURLOptions): string; public toJSON(): object; public toString(): string; } @@ -291,7 +291,7 @@ declare module 'discord.js' { public readonly createdAt: Date; public readonly createdTimestamp: number; - public iconURL(options?: AvatarOptions): string; + public iconURL(options?: ImageURLOptions): string; public toJSON(): object; public toString(): string; } @@ -726,7 +726,7 @@ declare module 'discord.js' { public widgetChannelID: Snowflake | null; public widgetEnabled: boolean | null; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; - public bannerURL(options?: AvatarOptions): string | null; + public bannerURL(options?: ImageURLOptions): string | null; public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; @@ -740,7 +740,7 @@ declare module 'discord.js' { public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; - public iconURL(options?: AvatarOptions): string | null; + public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; public leave(): Promise; public member(user: UserResolvable): GuildMember | null; public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; @@ -759,7 +759,7 @@ declare module 'discord.js' { public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; - public splashURL(options?: AvatarOptions): string | null; + public splashURL(options?: ImageURLOptions): string | null; public toJSON(): object; public toString(): string; } @@ -1189,8 +1189,8 @@ declare module 'discord.js' { public largeText: string | null; public smallImage: Snowflake | null; public smallText: string | null; - public largeImageURL(options: AvatarOptions): string | null; - public smallImageURL(options: AvatarOptions): string | null; + public largeImageURL(options: ImageURLOptions): string | null; + public smallImageURL(options: ImageURLOptions): string | null; } export class Role extends Base { @@ -1421,10 +1421,10 @@ declare module 'discord.js' { public system?: boolean; public readonly tag: string; public username: string; - public avatarURL(options?: AvatarOptions): string | null; + public avatarURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; public createDM(): Promise; public deleteDM(): Promise; - public displayAvatarURL(options?: AvatarOptions): string; + public displayAvatarURL(options?: ImageURLOptions & { dynamic?: boolean }): string; public equals(user: User): boolean; public fetch(): Promise; public toString(): string; @@ -2011,7 +2011,7 @@ declare module 'discord.js' { new?: any; } - interface AvatarOptions { + interface ImageURLOptions { format?: ImageExt; size?: ImageSize; } From 62afafdbe9674801d8f8c52c0fe14c3d1bc90adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Mon, 13 Jan 2020 16:48:49 +0100 Subject: [PATCH 337/428] typings: Fixed build error (#3689) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c5eb84f9..c217cdcb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1635,7 +1635,7 @@ declare module 'discord.js' { export class Webhook extends WebhookMixin() { constructor(client: Client, data?: object); public avatar: string; - public avatarURL(options?: AvatarOptions): string | null; + public avatarURL(options?: ImageURLOptions): string | null; public channelID: Snowflake; public guildID: Snowflake; public name: string; From 53a1f8fcd4e333e60c32f9f51dc4062708eec207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Mon, 13 Jan 2020 17:16:22 +0100 Subject: [PATCH 338/428] refactor: Remove `util` alias export (#3691) --- src/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.js b/src/index.js index cbc0d5a7..896919c4 100644 --- a/src/index.js +++ b/src/index.js @@ -28,7 +28,6 @@ module.exports = { Structures: require('./util/Structures'), SystemChannelFlags: require('./util/SystemChannelFlags'), Util: Util, - util: Util, version: require('../package.json').version, // Stores From 45cd58b68ce61afcf44793e610f06ada44fb8d1f Mon Sep 17 00:00:00 2001 From: Ayyan Lewis Date: Mon, 13 Jan 2020 21:01:16 +0400 Subject: [PATCH 339/428] types(VoiceBroadcast): add subscribers property (#3677) * types(VoiceBroadcast): add subscribers property * types(VoiceBroadcast): change player property to private Co-Authored-By: Amish Shah Co-authored-by: Amish Shah --- typings/index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c217cdcb..3d55cb97 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1475,7 +1475,8 @@ declare module 'discord.js' { class VoiceBroadcast extends EventEmitter { constructor(client: Client); public client: Client; - public dispatchers: StreamDispatcher[]; + public subscribers: StreamDispatcher[]; + private player: BroadcastAudioPlayer; public readonly dispatcher: BroadcastDispatcher; public play(input: string | Readable, options?: StreamOptions): BroadcastDispatcher; From c23cc7a42ef7a301b04fde19e58782a903b6d301 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 13 Jan 2020 20:53:07 +0200 Subject: [PATCH 340/428] src: Cleanup event listeners on WebSocket connections (#3681) * src: Cleanup event listeners on WebSocket connections Should prevent #3641 from happening, as well as double connections on a shard * typings: Forgot to add the method --- src/client/websocket/WebSocketManager.js | 2 ++ src/client/websocket/WebSocketShard.js | 31 ++++++++++++++++++++---- typings/index.d.ts | 1 + 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 71cbf93c..becc570c 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -245,6 +245,8 @@ class WebSocketManager extends EventEmitter { }); shard.on(ShardEvents.DESTROYED, () => { + shard._cleanupConnection(); + this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); this.client.emit(Events.SHARD_RECONNECTING, shard.id); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 5171d7e5..1b7d5495 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -208,11 +208,18 @@ class WebSocketShard extends EventEmitter { this.once(ShardEvents.INVALID_SESSION, onInvalid); if (this.connection && this.connection.readyState === WebSocket.OPEN) { - this.debug('Connection found, attempting an immediate identify.'); + this.debug('An open connection was found, attempting an immediate identify.'); this.identify(); return; } + if (this.connection) { + this.debug(`A connection was found. Cleaning up before continuing. + State: ${CONNECTION_STATE[this.connection.readyState]}`); + this._cleanupConnection(); + this.connection.close(1000); + } + const wsQuery = { v: client.options.ws.version }; if (zlib) { @@ -526,9 +533,9 @@ class WebSocketShard extends EventEmitter { } else if (!this.lastHeartbeatAcked) { this.debug( `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. - Status : ${STATUS_KEYS[this.status]} - Sequence : ${this.sequence} - Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}` + Status : ${STATUS_KEYS[this.status]} + Sequence : ${this.sequence} + Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}` ); this.destroy(4009); return; @@ -629,7 +636,8 @@ class WebSocketShard extends EventEmitter { */ _send(data) { if (!this.connection || this.connection.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`); + this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available! Resetting the shard...`); + this.destroy(4000); return; } @@ -667,6 +675,8 @@ class WebSocketShard extends EventEmitter { * @private */ destroy(closeCode = 1000, cleanup = false) { + this.debug(`Destroying with close code ${closeCode}, attempting a reconnect: ${!cleanup}`); + this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); @@ -696,6 +706,17 @@ class WebSocketShard extends EventEmitter { this.ratelimit.timer = null; } } + + /** + * Cleans up the WebSocket connection listeners. + * @private + */ + _cleanupConnection() { + this.connection.onopen = + this.connection.onclose = + this.connection.onerror = + this.connection.onmessage = null; + } } module.exports = WebSocketShard; diff --git a/typings/index.d.ts b/typings/index.d.ts index 3d55cb97..d4160b59 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1717,6 +1717,7 @@ declare module 'discord.js' { private _send(data: object): void; private processQueue(): void; private destroy(closeCode: number): void; + private _cleanupConnection(): void; public send(data: object): void; public on(event: 'ready', listener: () => void): this; From ee0b7c155a1767ed42e2756e53d56368e8b69929 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 13 Jan 2020 21:28:29 +0100 Subject: [PATCH 341/428] feat(Presence): add support for multiple activities (#3661) * feat(Presence): add support for multiple activites * typings(Presence): fix spelling of 'activities' Co-Authored-By: Amish Shah Co-authored-by: Amish Shah --- src/structures/Presence.js | 7 +++---- typings/index.d.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 8c207989..1ad6229a 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -85,12 +85,11 @@ class Presence { */ this.status = data.status || this.status || 'offline'; - const activity = data.game || data.activity; /** - * The activity of this presence - * @type {?Activity} + * The activities of this presence + * @type {Activity[]} */ - this.activity = activity ? new Activity(this, activity) : null; + this.activities = data.activities ? data.activities.map(activity => new Activity(this, activity)) : []; /** * The devices this presence is on diff --git a/typings/index.d.ts b/typings/index.d.ts index d4160b59..9e64598b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1140,7 +1140,7 @@ declare module 'discord.js' { export class Presence { constructor(client: Client, data?: object); - public activity: Activity | null; + public activities: Activity[]; public clientStatus: ClientPresenceStatusData | null; public flags: Readonly; public guild: Guild | null; From 629c57f890fc5124384ae83f74c9190c1db2dd4c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 13 Jan 2020 22:29:05 +0000 Subject: [PATCH 342/428] fix: regression (changing voice servers) --- src/client/voice/VoiceConnection.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 540dd8c7..b4b1e9da 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -452,8 +452,7 @@ class VoiceConnection extends EventEmitter { onSessionDescription(data) { Object.assign(this.authentication, data); this.status = VoiceStatus.CONNECTED; - const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); - dispatcher.on('finish', () => { + const ready = () => { this.client.clearTimeout(this.connectTimeout); this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); /** @@ -462,7 +461,14 @@ class VoiceConnection extends EventEmitter { * @event VoiceConnection#ready */ this.emit('ready'); - }); + }; + if (this.dispatcher) { + ready(); + } else { + // This serves to provide support for voice receive, sending audio is required to receive it. + const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); + dispatcher.once('finish', ready); + } } onStartSpeaking({ user_id, ssrc, speaking }) { From 75fe1faf2f95775ae29883bfcf6f27651a1b53b2 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Mon, 13 Jan 2020 17:45:58 -0500 Subject: [PATCH 343/428] Remove BroadcastAudioPlayer from typings (#3692) --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 9e64598b..3e6aec40 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1476,7 +1476,6 @@ declare module 'discord.js' { constructor(client: Client); public client: Client; public subscribers: StreamDispatcher[]; - private player: BroadcastAudioPlayer; public readonly dispatcher: BroadcastDispatcher; public play(input: string | Readable, options?: StreamOptions): BroadcastDispatcher; From 7f99be739aa7f11b6323a08d9435acb784aeb011 Mon Sep 17 00:00:00 2001 From: Souji Date: Tue, 14 Jan 2020 11:28:19 +0100 Subject: [PATCH 344/428] docs(MessageMentions): add sort order notice (#3693) * mention order returned from API * not left to right in text --- src/structures/MessageMentions.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 29a029bf..81bd791c 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -42,6 +42,7 @@ class MessageMentions { if (users instanceof Collection) { /** * Any users that were mentioned + * Order as received from the API, not left to right by occurence in the message content * @type {Collection} */ this.users = new Collection(users); @@ -63,6 +64,7 @@ class MessageMentions { if (roles instanceof Collection) { /** * Any roles that were mentioned + * Order as received from the API, not left to right by occurence in the message content * @type {Collection} */ this.roles = new Collection(roles); @@ -104,6 +106,7 @@ class MessageMentions { if (crosspostedChannels instanceof Collection) { /** * A collection of crossposted channels + * Order as received from the API, not left to right by occurence in the message content * @type {Collection} */ this.crosspostedChannels = new Collection(crosspostedChannels); @@ -127,6 +130,7 @@ class MessageMentions { /** * Any members that were mentioned (only in {@link TextChannel}s) + * Order as received from the API, not left to right by occurence in the message content * @type {?Collection} * @readonly */ @@ -143,6 +147,7 @@ class MessageMentions { /** * Any channels that were mentioned + * Order as received from the API, not left to right by occurence in the message content * @type {Collection} * @readonly */ From d77229f423a2f2263a09a6af9b670d0ee48752ef Mon Sep 17 00:00:00 2001 From: Helmasaur Date: Thu, 16 Jan 2020 12:57:20 +0100 Subject: [PATCH 345/428] chore: ffmpeg package in the voice doc (#3697) ffmpeg package changed from "ffmpeg-binaries" to "ffmpeg-static" --- docs/topics/voice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index 22f5c93d..cf336eb5 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -4,7 +4,7 @@ Voice in discord.js can be used for many things, such as music bots, recording o In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a `VoiceConnection`, where you can start streaming and receiving audio. To get started, make sure you have: -* FFmpeg - `npm install ffmpeg-binaries` +* FFmpeg - `npm install ffmpeg-static` * an opus encoder, choose one from below: * `npm install node-opus` (better performance) * `npm install opusscript` From d096e40f6fe025001dff64f0bee37ac26962998e Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 16 Jan 2020 12:59:03 +0100 Subject: [PATCH 346/428] feat/fix: use updated eslint action (#3699) --- .github/workflows/test-cron.yml | 2 +- .github/workflows/test.yml | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-cron.yml b/.github/workflows/test-cron.yml index 99ee1067..10f97e38 100644 --- a/.github/workflows/test-cron.yml +++ b/.github/workflows/test-cron.yml @@ -19,7 +19,7 @@ jobs: run: npm install - name: Run ESLint - run: npm run lint + uses: icrawl/action-eslint@v1 typings: name: TSLint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 641a6327..0c51d68e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,11 +17,7 @@ jobs: run: npm install - name: Run ESLint - uses: discordjs/action-eslint@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - job-name: ESLint + uses: icrawl/action-eslint@v1 typings: name: TSLint From 6a0fe467e5f6e08360bcce0bf3039f01e6dc8678 Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 16 Jan 2020 14:10:48 +0100 Subject: [PATCH 347/428] docs: replace all occurances of node-opus with @discordjs/opus (#3698) * docs: replace all occurances of node-opus with @discordjs/opus * chore: leave in node-opus in case not everyone switched --- .github/CONTRIBUTING.md | 2 +- README.md | 8 ++++---- docs/general/faq.md | 10 +++++----- docs/general/welcome.md | 8 ++++---- docs/topics/voice.md | 8 ++++---- package.json | 1 + 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9f84b107..fd6de724 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,7 +11,7 @@ To get ready to work on the codebase, please do the following: 1. Fork & clone the repository, and make sure you're on the **master** branch 2. Run `npm install` -3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript` +3. If you're working on voice, also run `npm install @discordjs/opus` or `npm install opusscript` 4. Code your heart out! 5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid 6. [Submit a pull request](https://github.com/discordjs/discord.js/compare) diff --git a/README.md b/README.md index 2003f60d..3ec09092 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,13 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discordjs/discord.js node-opus` +With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discordjs/discord.js @discordjs/opus` With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript` ### Audio engines -The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. -Using opusscript is only recommended for development environments where node-opus is tough to get working. -For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. +The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus. +Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working. +For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) diff --git a/docs/general/faq.md b/docs/general/faq.md index c4ce363d..d57060db 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -7,8 +7,8 @@ Update to Node.js 10.0.0 or newer. ## How do I get voice working? - Install FFMPEG. -- Install either the `node-opus` package or the `opusscript` package. - node-opus is greatly preferred, due to it having significantly better performance. +- Install either the `@discordjs/opus` package or the `opusscript` package. + @discordjs/opus is greatly preferred, due to it having significantly better performance. ## How do I install FFMPEG? - **npm:** `npm install ffmpeg-binaries` @@ -16,10 +16,10 @@ Update to Node.js 10.0.0 or newer. - **Ubuntu 14.04:** `sudo apt-get install libav-tools` - **Windows:** `npm install ffmpeg-binaries` or see the [FFMPEG section of AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md#download-ffmpeg). -## How do I set up node-opus? -- **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats! +## How do I set up @discordjs/opus? +- **Ubuntu:** Simply run `npm install @discordjs/opus`, and it's done. Congrats! - **Windows:** Run `npm install --global --production windows-build-tools` in an admin command prompt or PowerShell. - Then, running `npm install node-opus` in your bot's directory should successfully build it. Woo! + Then, running `npm install @discordjs/opus` in your bot's directory should successfully build it. Woo! Other questions can be found at the [official Discord.js guide](https://discordjs.guide/popular-topics/common-questions.html) If you have issues not listed here or on the guide, feel free to ask in the [official Discord.js server](https://discord.gg/bRCvFy9). diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 52cb3135..936c01a4 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -37,13 +37,13 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discordjs/discord.js node-opus` +With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discordjs/discord.js @discordjs/opus` With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript` ### Audio engines -The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. -Using opusscript is only recommended for development environments where node-opus is tough to get working. -For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. +The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus. +Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working. +For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index cf336eb5..a716d3e7 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -6,13 +6,13 @@ In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a ` To get started, make sure you have: * FFmpeg - `npm install ffmpeg-static` * an opus encoder, choose one from below: - * `npm install node-opus` (better performance) + * `npm install @discordjs/opus` (better performance) * `npm install opusscript` * a good network connection -The preferred opus engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. -Using opusscript is only recommended for development environments where node-opus is tough to get working. -For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. +The preferred opus engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus. +Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working. +For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ## Joining a voice channel The example below reacts to a message and joins the sender's voice channel, catching any errors. This is important diff --git a/package.json b/package.json index a52ade25..230ec470 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "node": ">=10.2.0" }, "browser": { + "@discordjs/opus": false, "https": false, "ws": false, "erlpack": false, From 69c79a4136619e4dcacfffb935aa9bd60aa2081c Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Fri, 17 Jan 2020 14:11:14 -0500 Subject: [PATCH 348/428] typings/docs(GuildEmoji): id isn't nullable (#3694) * Fix: GuildEmoji#id isn't nullable * Move ID to be alphabetical * Add JSDoc to say it's not nullable * fix linting --- src/structures/GuildEmoji.js | 6 ++++++ typings/index.d.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 79fff52f..a7620911 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -24,6 +24,12 @@ class GuildEmoji extends Emoji { */ this.guild = guild; + /** + * The ID of this emoji + * @type {Snowflake} + * @name GuildEmoji#id + */ + this._roles = []; this._patch(data); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 3e6aec40..d567e6db 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -841,6 +841,7 @@ declare module 'discord.js' { public available: boolean; public readonly deletable: boolean; public guild: Guild; + public id: Snowflake; public managed: boolean; public requiresColons: boolean; public roles: GuildEmojiRoleStore; From cbb8db3058464cd99186da294ad781af2a7f317e Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Sun, 19 Jan 2020 05:24:55 -0500 Subject: [PATCH 349/428] feat(Collectors): make collectors auto-stop when relevant structures are deleted (#3632) * Collectors: make Collectors automatically stop when Channel, Guild, or Message are deleted. * fix potential error with DM collectors * Message collectors dont have a `this.message` you dummy * Fix(various): nitpicks, documentation, typings, and stray error * Pleasing mr tslint * fix: typings * Grammatical fixes Co-Authored-By: SpaceEEC * Fixing the linting after space's suggestions * docs(ReactionCollector): remove whitespace Co-authored-by: SpaceEEC --- src/structures/MessageCollector.js | 31 +++++++++++++++++++ src/structures/ReactionCollector.js | 47 +++++++++++++++++++++++++++++ typings/index.d.ts | 7 +++++ 3 files changed, 85 insertions(+) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 5d45be08..f357722b 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -11,6 +11,7 @@ const { Events } = require('../util/Constants'); /** * Collects messages on a channel. + * Will automatically stop if the channel (`'channelDelete'`) or guild (`'guildDelete'`) are deleted. * @extends {Collector} */ class MessageCollector extends Collector { @@ -38,16 +39,22 @@ class MessageCollector extends Collector { const bulkDeleteListener = (messages => { for (const message of messages.values()) this.handleDispose(message); }).bind(this); + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); this.client.on(Events.MESSAGE_CREATE, this.handleCollect); this.client.on(Events.MESSAGE_DELETE, this.handleDispose); this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); this.once('end', () => { this.client.removeListener(Events.MESSAGE_CREATE, this.handleCollect); this.client.removeListener(Events.MESSAGE_DELETE, this.handleDispose); this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); }); } @@ -93,6 +100,30 @@ class MessageCollector extends Collector { if (this.options.maxProcessed && this.received === this.options.maxProcessed) return 'processedLimit'; return null; } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.channel.id) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (this.channel.guild && guild.id === this.channel.guild.id) { + this.stop('guildDelete'); + } + } } module.exports = MessageCollector; diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index 8e84fb67..e7d5aeb1 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -13,6 +13,8 @@ const { Events } = require('../util/Constants'); /** * Collects reactions on messages. + * Will automatically stop if the message (`'messageDelete'`), + * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. * @extends {Collector} */ class ReactionCollector extends Collector { @@ -43,16 +45,25 @@ class ReactionCollector extends Collector { this.total = 0; this.empty = this.empty.bind(this); + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + this._handleMessageDeletion = this._handleMessageDeletion.bind(this); if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); this.client.on(Events.MESSAGE_REACTION_ADD, this.handleCollect); this.client.on(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); this.client.on(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); + this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); this.once('end', () => { this.client.removeListener(Events.MESSAGE_REACTION_ADD, this.handleCollect); this.client.removeListener(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); this.client.removeListener(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); + this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); }); @@ -131,6 +142,42 @@ class ReactionCollector extends Collector { return null; } + /** + * Handles checking if the message has been deleted, and if so, stops the collector with the reason 'messageDelete'. + * @private + * @param {Message} message The message that was deleted + * @returns {void} + */ + _handleMessageDeletion(message) { + if (message.id === this.message.id) { + this.stop('messageDelete'); + } + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.message.channel.id) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (this.message.guild && guild.id === this.message.guild.id) { + this.stop('guildDelete'); + } + } + /** * Gets the collector key for a reaction. * @param {MessageReaction} reaction The message reaction to get the key for diff --git a/typings/index.d.ts b/typings/index.d.ts index d567e6db..6d719a34 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1023,6 +1023,9 @@ declare module 'discord.js' { export class MessageCollector extends Collector { constructor(channel: TextChannel | DMChannel, filter: CollectorFilter, options?: MessageCollectorOptions); + private _handleChannelDeletion(channel: GuildChannel): void; + private _handleGuildDeletion(guild: Guild): void; + public channel: Channel; public options: MessageCollectorOptions; public received: number; @@ -1153,6 +1156,10 @@ declare module 'discord.js' { export class ReactionCollector extends Collector { constructor(message: Message, filter: CollectorFilter, options?: ReactionCollectorOptions); + private _handleChannelDeletion(channel: GuildChannel): void; + private _handleGuildDeletion(guild: Guild): void; + private _handleMessageDeletion(message: Message): void; + public message: Message; public options: ReactionCollectorOptions; public total: number; From f501d06c0d5de872327be683c7f80d962eff3864 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 19 Jan 2020 13:05:45 +0100 Subject: [PATCH 350/428] fix(Presence): account for multiple activities everywhere (#3703) * fix(Presence): account for multiple activities everywhere * refactor(Presence): make initialization of 'activities' more readable --- src/structures/ClientPresence.js | 2 +- src/structures/ClientUser.js | 2 +- src/structures/Presence.js | 27 +++++++++++++++++---------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 90db53bd..213b46f2 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -68,7 +68,7 @@ class ClientPresence extends Presence { }; if ((status || afk || since) && !activity) { - packet.game = this.activity; + packet.game = this.activities[0] || null; } if (packet.game) { diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 16dd13d0..3f2883ef 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -149,7 +149,7 @@ class ClientUser extends Structures.get('User') { * @example * // Set the client user's activity * client.user.setActivity('discord.js', { type: 'WATCHING' }) - * .then(presence => console.log(`Activity set to ${presence.activity.name}`)) + * .then(presence => console.log(`Activity set to ${presence.activities[0].name}`)) * .catch(console.error); */ setActivity(name, options = {}) { diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 1ad6229a..12888f1c 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -85,11 +85,17 @@ class Presence { */ this.status = data.status || this.status || 'offline'; - /** - * The activities of this presence - * @type {Activity[]} - */ - this.activities = data.activities ? data.activities.map(activity => new Activity(this, activity)) : []; + if (data.activities) { + /** + * The activities of this presence + * @type {Activity[]} + */ + this.activities = data.activities.map(activity => new Activity(this, activity)); + } else if (data.activity || data.game) { + this.activities = [new Activity(this, data.game || data.activity)]; + } else { + this.activities = []; + } /** * The devices this presence is on @@ -105,7 +111,7 @@ class Presence { _clone() { const clone = Object.assign(Object.create(this), this); - if (this.activity) clone.activity = this.activity._clone(); + if (this.activities) clone.activities = this.activities.map(activity => activity._clone()); return clone; } @@ -118,10 +124,11 @@ class Presence { return this === presence || ( presence && this.status === presence.status && - this.activity ? this.activity.equals(presence.activity) : !presence.activity && - this.clientStatus.web === presence.clientStatus.web && - this.clientStatus.mobile === presence.clientStatus.mobile && - this.clientStatus.desktop === presence.clientStatus.desktop + this.activities.length === presence.activities.length && + this.activities.every((activity, index) => activity.equals(presence.activities[index])) && + this.clientStatus.web === presence.clientStatus.web && + this.clientStatus.mobile === presence.clientStatus.mobile && + this.clientStatus.desktop === presence.clientStatus.desktop ); } From 6302afb84bb05907d12790280c6074ff850497a3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 19 Jan 2020 13:06:21 +0100 Subject: [PATCH 351/428] docs(MessageMentions): channels are actually in order (#3705) * docs(MessageMentions): channels are actually in order * docs(MessageMentions): readd info about order for channels * docs(MessageMentions): reword info to account for rtl locales --- src/structures/MessageMentions.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 81bd791c..ab2d5c81 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -42,7 +42,7 @@ class MessageMentions { if (users instanceof Collection) { /** * Any users that were mentioned - * Order as received from the API, not left to right by occurence in the message content + * Order as received from the API, not as they appear in the message content * @type {Collection} */ this.users = new Collection(users); @@ -64,7 +64,7 @@ class MessageMentions { if (roles instanceof Collection) { /** * Any roles that were mentioned - * Order as received from the API, not left to right by occurence in the message content + * Order as received from the API, not as they appear in the message content * @type {Collection} */ this.roles = new Collection(roles); @@ -106,7 +106,7 @@ class MessageMentions { if (crosspostedChannels instanceof Collection) { /** * A collection of crossposted channels - * Order as received from the API, not left to right by occurence in the message content + * Order as received from the API, not as they appear in the message content * @type {Collection} */ this.crosspostedChannels = new Collection(crosspostedChannels); @@ -130,7 +130,7 @@ class MessageMentions { /** * Any members that were mentioned (only in {@link TextChannel}s) - * Order as received from the API, not left to right by occurence in the message content + * Order as received from the API, not as they appear in the message content * @type {?Collection} * @readonly */ @@ -147,7 +147,7 @@ class MessageMentions { /** * Any channels that were mentioned - * Order as received from the API, not left to right by occurence in the message content + * Order as they appear first in the message content * @type {Collection} * @readonly */ From 877577badcaa17b0221b75d79c55b6de3eff4e3a Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 20 Jan 2020 21:02:02 +0000 Subject: [PATCH 352/428] typings(RichPresenceAssets): *ImageURL's options are optional (#3727) --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 6d719a34..457f86a6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1197,8 +1197,8 @@ declare module 'discord.js' { public largeText: string | null; public smallImage: Snowflake | null; public smallText: string | null; - public largeImageURL(options: ImageURLOptions): string | null; - public smallImageURL(options: ImageURLOptions): string | null; + public largeImageURL(options?: ImageURLOptions): string | null; + public smallImageURL(options?: ImageURLOptions): string | null; } export class Role extends Base { From 63293fe14d99a2915adc52681dc5a722ea81f464 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Wed, 22 Jan 2020 01:21:43 -0700 Subject: [PATCH 353/428] chore(License): bump license year (#3734) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 9a4257e6..9997d13f 100644 --- a/LICENSE +++ b/LICENSE @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - Copyright 2015 - 2019 Amish Shah + Copyright 2015 - 2020 Amish Shah Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From c779fe3670b2279de8638de6f2e70e008b1b1804 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 24 Jan 2020 14:29:53 +0000 Subject: [PATCH 354/428] feat(Guild): add fetchBan method (#3726) * Add error for not resolving ID to fetch ban * Add Guild#fetchBan * add missing ! * typings * lint fixes * add jsdoc description --- src/errors/Messages.js | 1 + src/structures/Guild.js | 15 +++++++++++++++ typings/index.d.ts | 1 + 3 files changed, 17 insertions(+) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index ae42f73c..db339ca2 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -71,6 +71,7 @@ const Messages = { SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.', BAN_RESOLVE_ID: (ban = false) => `Couldn't resolve the user ID to ${ban ? 'ban' : 'unban'}.`, + FETCH_BAN_RESOLVE_ID: 'Couldn\'t resolve the user ID to fetch the ban.', PRUNE_DAYS_TYPE: 'Days must be a number', diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 6b22cd25..3dd518d4 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -554,6 +554,21 @@ class Guild extends Base { * @property {?string} reason Reason the user was banned */ + /** + * Fetches information on a banned user from this guild. + * @param {UserResolvable} user The User to fetch the ban info of + * @returns {BanInfo} + */ + fetchBan(user) { + const id = this.client.users.resolveID(user); + if (!id) throw new Error('FETCH_BAN_RESOLVE_ID'); + return this.client.api.guilds(this.id).bans(id).get() + .then(ban => ({ + reason: ban.reason, + user: this.client.users.add(ban.user), + })); + } + /** * Fetches a collection of banned users in this guild. * @returns {Promise>} diff --git a/typings/index.d.ts b/typings/index.d.ts index 457f86a6..4c5a5ae2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -733,6 +733,7 @@ declare module 'discord.js' { public equals(guild: Guild): boolean; public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; + public fetchBan(user: UserResolvable): Promise<{ user: User, reason: string }>; public fetchBans(): Promise>; public fetchEmbed(): Promise; public fetchIntegrations(): Promise>; From 929ff9ccd0f1aa3c1d070915d567f594077b8288 Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Fri, 24 Jan 2020 10:38:26 -0500 Subject: [PATCH 355/428] feat(Client): add support for INVITE_CREATE and INVITE_DELETE events (#3720) * Add support for new Invite events * Merge typings for events Co-Authored-By: Sugden * Add warning about requiring permissions * Null check channel and guild * fix: .guilds not .channels --- src/client/actions/ActionsManager.js | 2 ++ src/client/actions/InviteCreate.js | 28 ++++++++++++++++++ src/client/actions/InviteDelete.js | 29 +++++++++++++++++++ .../websocket/handlers/INVITE_CREATE.js | 5 ++++ .../websocket/handlers/INVITE_DELETE.js | 5 ++++ src/util/Constants.js | 4 +++ typings/index.d.ts | 1 + 7 files changed, 74 insertions(+) create mode 100644 src/client/actions/InviteCreate.js create mode 100644 src/client/actions/InviteDelete.js create mode 100644 src/client/websocket/handlers/INVITE_CREATE.js create mode 100644 src/client/websocket/handlers/INVITE_DELETE.js diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 268e4371..730f507c 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -16,6 +16,8 @@ class ActionsManager { this.register(require('./ChannelUpdate')); this.register(require('./GuildDelete')); this.register(require('./GuildUpdate')); + this.register(require('./InviteCreate')); + this.register(require('./InviteDelete')); this.register(require('./GuildMemberRemove')); this.register(require('./GuildBanRemove')); this.register(require('./GuildRoleCreate')); diff --git a/src/client/actions/InviteCreate.js b/src/client/actions/InviteCreate.js new file mode 100644 index 00000000..5552ea2f --- /dev/null +++ b/src/client/actions/InviteCreate.js @@ -0,0 +1,28 @@ +'use strict'; + +const Action = require('./Action'); +const Invite = require('../../structures/Invite'); +const { Events } = require('../../util/Constants'); + +class InviteCreateAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.get(data.channel_id); + const guild = client.guilds.get(data.guild_id); + if (!channel && !guild) return false; + + const inviteData = Object.assign(data, { channel, guild }); + const invite = new Invite(client, inviteData); + /** + * Emitted when an invite is created. + * This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, + * or `MANAGE_CHANNEL` permissions for the channel. + * @event Client#inviteCreate + * @param {Invite} invite The invite that was created + */ + client.emit(Events.INVITE_CREATE, invite); + return { invite }; + } +} + +module.exports = InviteCreateAction; diff --git a/src/client/actions/InviteDelete.js b/src/client/actions/InviteDelete.js new file mode 100644 index 00000000..83933d34 --- /dev/null +++ b/src/client/actions/InviteDelete.js @@ -0,0 +1,29 @@ +'use strict'; + +const Action = require('./Action'); +const Invite = require('../../structures/Invite'); +const { Events } = require('../../util/Constants'); + +class InviteDeleteAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.get(data.channel_id); + const guild = client.guilds.get(data.guild_id); + if (!channel && !guild) return false; + + const inviteData = Object.assign(data, { channel, guild }); + const invite = new Invite(client, inviteData); + + /** + * Emitted when an invite is deleted. + * This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, + * or `MANAGE_CHANNEL` permissions for the channel. + * @event Client#inviteDelete + * @param {Invite} invite The invite that was deleted + */ + client.emit(Events.INVITE_DELETE, invite); + return { invite }; + } +} + +module.exports = InviteDeleteAction; diff --git a/src/client/websocket/handlers/INVITE_CREATE.js b/src/client/websocket/handlers/INVITE_CREATE.js new file mode 100644 index 00000000..50a2e72d --- /dev/null +++ b/src/client/websocket/handlers/INVITE_CREATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.InviteCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/INVITE_DELETE.js b/src/client/websocket/handlers/INVITE_DELETE.js new file mode 100644 index 00000000..59718523 --- /dev/null +++ b/src/client/websocket/handlers/INVITE_DELETE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.InviteDelete.handle(packet.d); +}; diff --git a/src/util/Constants.js b/src/util/Constants.js index a2e86d4a..a8f93a85 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -243,6 +243,8 @@ exports.Events = { GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate', GUILD_ROLE_CREATE: 'roleCreate', GUILD_ROLE_DELETE: 'roleDelete', + INVITE_CREATE: 'inviteCreate', + INVITE_DELETE: 'inviteDelete', GUILD_ROLE_UPDATE: 'roleUpdate', GUILD_EMOJI_CREATE: 'emojiCreate', GUILD_EMOJI_DELETE: 'emojiDelete', @@ -353,6 +355,8 @@ exports.WSEvents = keyMirror([ 'GUILD_CREATE', 'GUILD_DELETE', 'GUILD_UPDATE', + 'INVITE_CREATE', + 'INVITE_DELETE', 'GUILD_MEMBER_ADD', 'GUILD_MEMBER_REMOVE', 'GUILD_MEMBER_UPDATE', diff --git a/typings/index.d.ts b/typings/index.d.ts index 4c5a5ae2..fc23f027 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -188,6 +188,7 @@ declare module 'discord.js' { public on(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public on(event: 'inviteCreate' | 'inviteDelete', listener: (invite: Invite) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; From b81f771007739bc56dd82bad235dedeb67b3ff6e Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Fri, 24 Jan 2020 10:58:23 -0500 Subject: [PATCH 356/428] cleanup: fix deepscan issues (#3740) * fix: don't double check if shards are auto. * fix: remove useless roles array. * fix: remove useless undefined checks. * fix: remove useless `this` binding * Apply suggestions from code review Co-Authored-By: Sugden <28943913+NotSugden@users.noreply.github.com> * Fix: Space's suggestion * Fix: time is always truthy * Check if it's an invalid date. Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> --- src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- src/client/websocket/WebSocketManager.js | 2 +- src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 +- src/structures/Guild.js | 1 + src/structures/MessageCollector.js | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 2923eb61..20a63fd9 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -73,10 +73,10 @@ class StreamDispatcher extends Writable { this._setSpeaking(0); }); - if (typeof volume !== 'undefined') this.setVolume(volume); + this.setVolume(volume); + this.setBitrate(bitrate); if (typeof fec !== 'undefined') this.setFEC(fec); if (typeof plp !== 'undefined') this.setPLP(plp); - if (typeof bitrate !== 'undefined') this.setBitrate(bitrate); const streamError = (type, err) => { /** diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index becc570c..1c22a5bf 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -158,7 +158,7 @@ class WebSocketManager extends EventEmitter { if (shards === 'auto') { this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); this.totalShards = this.client.options.shardCount = recommendedShards; - if (shards === 'auto' || !this.client.options.shards.length) { + if (!this.client.options.shards.length) { this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); } } diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index da73693c..6da643ff 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -6,7 +6,7 @@ module.exports = (client, { d: data }) => { const channel = client.channels.get(data.channel_id); const time = new Date(data.last_pin_timestamp); - if (channel && time) { + if (channel && !Number.isNaN(time.getTime())) { // Discord sends null for last_pin_timestamp if the last pinned message was removed channel.lastPinTimestamp = time.getTime() || null; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 3dd518d4..5827ff49 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -775,6 +775,7 @@ class Guild extends Base { } roles.push(role.id); } + options.roles = roles; } return this.client.api.guilds(this.id).members(user).put({ data: options }) .then(data => this.members.add(data)); diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index f357722b..f8f3d5ab 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -36,9 +36,9 @@ class MessageCollector extends Collector { */ this.received = 0; - const bulkDeleteListener = (messages => { + const bulkDeleteListener = messages => { for (const message of messages.values()) this.handleDispose(message); - }).bind(this); + }; this._handleChannelDeletion = this._handleChannelDeletion.bind(this); this._handleGuildDeletion = this._handleGuildDeletion.bind(this); From 90aa5b3500a2ba819db1c4facb65f9949b38fbc3 Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Fri, 24 Jan 2020 11:08:40 -0500 Subject: [PATCH 357/428] feat(GuildMemberStore): make timeout refresh after every GUILD_MEMBERS_CHUNK (#3645) --- src/stores/GuildMemberStore.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index e02aa3dc..7abc81ca 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -207,6 +207,7 @@ class GuildMemberStore extends DataStore { const fetchedMembers = new Collection(); const handler = (members, guild) => { if (guild.id !== this.guild.id) return; + timeout.refresh(); for (const member of members.values()) { if (query || limit) fetchedMembers.set(member.id, member); } @@ -217,11 +218,11 @@ class GuildMemberStore extends DataStore { resolve(query || limit ? fetchedMembers : this); } }; - this.guild.client.on(Events.GUILD_MEMBERS_CHUNK, handler); - this.guild.client.setTimeout(() => { + const timeout = this.guild.client.setTimeout(() => { this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); reject(new Error('GUILD_MEMBERS_TIMEOUT')); }, 120e3); + this.guild.client.on(Events.GUILD_MEMBERS_CHUNK, handler); }); } } From 3ea9ac57ddb6fa48ba26c5e181488bfcdef6b48a Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 25 Jan 2020 14:08:25 +0000 Subject: [PATCH 358/428] fix(ClientUser): verified and enabled properties resetting (#3733) * fix(ClientUser) verified and enabled properties resetting * set this.mfaEnabled to null if it is undefined * add missing curly brackets * fix typo --- src/structures/ClientUser.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 3f2883ef..a9512a64 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -16,17 +16,23 @@ class ClientUser extends Structures.get('User') { _patch(data) { super._patch(data); - /** - * Whether or not this account has been verified - * @type {boolean} - */ - this.verified = data.verified; + if ('verified' in data) { + /** + * Whether or not this account has been verified + * @type {boolean} + */ + this.verified = data.verified; + } - /** - * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account - * @type {?boolean} - */ - this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + if ('mfa_enabled' in data) { + /** + * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account + * @type {?boolean} + */ + this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + } else if (typeof this.mfaEnabled === 'undefined') { + this.mfaEnabled = null; + } if (data.token) this.client.token = data.token; } From d8b4725caa2e188e16c94f20ff7b3e5b821589ad Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Sun, 26 Jan 2020 01:27:39 +1100 Subject: [PATCH 359/428] fix(TextChannel#bulkDelete): use GenericAction#getMessage to handle return value correctly (#3664) * Corrected the handling of the action * Apply same fix to handling of single message in bulkDelete * Revert to using await --- src/structures/interfaces/TextBasedChannel.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 634dff33..a6b18375 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -306,19 +306,16 @@ class TextBasedChannel { if (messageIDs.length === 0) return new Collection(); if (messageIDs.length === 1) { await this.client.api.channels(this.id).messages(messageIDs[0]).delete(); - const message = this.client.actions.MessageDelete.handle({ - channel_id: this.id, - id: messageIDs[0], - }).message; - if (message) return new Collection([[message.id, message]]); - return new Collection(); + const message = this.client.actions.MessageDelete.getMessage({ + message_id: messageIDs[0], + }, this); + return message ? new Collection([[message.id, message]]) : new Collection(); } await this.client.api.channels[this.id].messages['bulk-delete'] .post({ data: { messages: messageIDs } }); - return this.client.actions.MessageDeleteBulk.handle({ - channel_id: this.id, - ids: messageIDs, - }).messages; + return messageIDs.reduce((col, id) => col.set(id, this.client.actions.MessageDeleteBulk.getMessage({ + message_id: id, + }, this)), new Collection()); } if (!isNaN(messages)) { const msgs = await this.messages.fetch({ limit: messages }); From 030d263a9e018a766c5f399624fe4f8ec2b7349c Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Sat, 25 Jan 2020 14:00:53 -0500 Subject: [PATCH 360/428] feat(MessageReaction): add remove method and Client#messageReactionRemoveEmoji (#3723) * Add support for MessageReaction#remove and MESSAGE_REACTION_REMOVE_EMOJI * Remove reaction from cache Co-Authored-By: matthewfripp <50251454+matthewfripp@users.noreply.github.com> * fix: message may be partial * Clarify what the event entails * Document client in MessageReaction Co-Authored-By: SpaceEEC * await the REST call * Add MessageReaction#remove to typings Co-authored-by: matthewfripp <50251454+matthewfripp@users.noreply.github.com> Co-authored-by: SpaceEEC --- src/client/actions/Action.js | 2 +- src/client/actions/ActionsManager.js | 1 + .../actions/MessageReactionRemoveEmoji.js | 28 +++++++++++++++++++ .../handlers/MESSAGE_REACTION_REMOVE_EMOJI.js | 5 ++++ src/structures/MessageReaction.js | 17 +++++++++++ src/util/Constants.js | 2 ++ typings/index.d.ts | 2 ++ 7 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/client/actions/MessageReactionRemoveEmoji.js create mode 100644 src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index bc9ed267..ce4fd6db 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -54,7 +54,7 @@ class GenericAction { return this.getPayload({ emoji: data.emoji, count: message.partial ? null : 0, - me: user.id === this.client.user.id, + me: user ? user.id === this.client.user.id : false, }, message.reactions, id, PartialTypes.REACTION); } diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 730f507c..7e1df121 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -11,6 +11,7 @@ class ActionsManager { this.register(require('./MessageReactionAdd')); this.register(require('./MessageReactionRemove')); this.register(require('./MessageReactionRemoveAll')); + this.register(require('./MessageReactionRemoveEmoji')); this.register(require('./ChannelCreate')); this.register(require('./ChannelDelete')); this.register(require('./ChannelUpdate')); diff --git a/src/client/actions/MessageReactionRemoveEmoji.js b/src/client/actions/MessageReactionRemoveEmoji.js new file mode 100644 index 00000000..ab0eaa77 --- /dev/null +++ b/src/client/actions/MessageReactionRemoveEmoji.js @@ -0,0 +1,28 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class MessageReactionRemoveEmoji extends Action { + handle(data) { + const channel = this.getChannel(data); + if (!channel || channel.type === 'voice') return false; + + const message = this.getMessage(data, channel); + if (!message) return false; + + const reaction = this.getReaction(data, message); + if (!reaction) return false; + if (!message.partial) message.reactions.delete(reaction.emoji.id || reaction.emoji.name); + + /** + * Emitted when a bot removes an emoji reaction from a cached message. + * @event Client#messageReactionRemoveEmoji + * @param {MessageReaction} reaction The reaction that was removed + */ + this.client.emit(Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction); + return { reaction }; + } +} + +module.exports = MessageReactionRemoveEmoji; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js new file mode 100644 index 00000000..579444c9 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageReactionRemoveEmoji.handle(packet.d); +}; diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index f32f0843..5cb9210f 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -15,6 +15,13 @@ class MessageReaction { * @param {Message} message The message the reaction refers to */ constructor(client, data, message) { + /** + * The client that instantiated this message reaction + * @name MessageReaction#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); /** * The message that this reaction refers to * @type {Message} @@ -47,6 +54,16 @@ class MessageReaction { if (this.count == undefined) this.count = data.count; } + /** + * Removes all users from this reaction. + * @returns {Promise} + */ + async remove() { + await this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions(this._emoji.identifier) + .delete(); + return this; + } + /** * The emoji of this reaction, either an GuildEmoji object for known custom emojis, or a ReactionEmoji * object which has fewer properties. Whatever the prototype of the emoji, it will still have diff --git a/src/util/Constants.js b/src/util/Constants.js index a8f93a85..0417d881 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -262,6 +262,7 @@ exports.Events = { MESSAGE_REACTION_ADD: 'messageReactionAdd', MESSAGE_REACTION_REMOVE: 'messageReactionRemove', MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', + MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji', USER_UPDATE: 'userUpdate', PRESENCE_UPDATE: 'presenceUpdate', VOICE_SERVER_UPDATE: 'voiceServerUpdate', @@ -379,6 +380,7 @@ exports.WSEvents = keyMirror([ 'MESSAGE_REACTION_ADD', 'MESSAGE_REACTION_REMOVE', 'MESSAGE_REACTION_REMOVE_ALL', + 'MESSAGE_REACTION_REMOVE_EMOJI', 'USER_UPDATE', 'PRESENCE_UPDATE', 'TYPING_START', diff --git a/typings/index.d.ts b/typings/index.d.ts index fc23f027..8154f3f0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -191,6 +191,7 @@ declare module 'discord.js' { public on(event: 'inviteCreate' | 'inviteDelete', listener: (invite: Invite) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; + public on(event: 'messageReactionRemoveEmoji', listener: (reaction: MessageReaction) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User | PartialUser) => void): this; public on(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; @@ -1113,6 +1114,7 @@ declare module 'discord.js' { public message: Message; public readonly partial: boolean; public users: ReactionUserStore; + public remove(): Promise; public fetch(): Promise; public toJSON(): object; } From 8e9e93da1d9c96a7acf63cc1c5317a42e56afbd8 Mon Sep 17 00:00:00 2001 From: Jyguy Date: Wed, 29 Jan 2020 12:54:10 -0500 Subject: [PATCH 361/428] docs(Guild): fetchBan returns a promise (#3752) --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 5827ff49..04d091b0 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -557,7 +557,7 @@ class Guild extends Base { /** * Fetches information on a banned user from this guild. * @param {UserResolvable} user The User to fetch the ban info of - * @returns {BanInfo} + * @returns {Promise} */ fetchBan(user) { const id = this.client.users.resolveID(user); From 6a381c68a28ac6961dcf51b0e7d0f5d7d163e8a6 Mon Sep 17 00:00:00 2001 From: PLASMAchicken Date: Fri, 31 Jan 2020 12:38:10 +0100 Subject: [PATCH 362/428] chore(README): update link to Discord.js guide v12 changes (#3751) * Update link to discord.js guide v12 changes * Suggested Changes * Suggested Changes Co-Authored-By: Amish Shah Co-authored-by: Amish Shah --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ec09092..3a295798 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ client.login('token'); * [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) * [Documentation](https://discord.js.org/#/docs/main/master/general/welcome) * [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable - See also the WIP [Update Guide](https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md) also including updated and removed items in the library. + See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v12.html), including updated and removed items in the library. * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/discordjs/discord.js) From b4e56d3e0e8c3bf242d6bf52af03b0826af62bd8 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 2 Feb 2020 12:12:58 +0200 Subject: [PATCH 363/428] src: fix up WebSocketShard errors (#3722) * src: Fix up WebSocketShard errors * typings: Forgot to update * src: Forgot debug variable * src: Fix issue Bella found If the WS was not connected when the HELLO timeout passes (CONNECTING, etc), the shard would get stuck due to never rejecting the WebSocketShard#connect Promise with the DESTROYED event --- src/client/websocket/WebSocketManager.js | 8 +- src/client/websocket/WebSocketShard.js | 118 +++++++++++++++-------- typings/index.d.ts | 3 +- 3 files changed, 85 insertions(+), 44 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 1c22a5bf..3e299c58 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -18,7 +18,7 @@ const BeforeReadyWhitelist = [ WSEvents.GUILD_MEMBER_REMOVE, ]; -const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011]; +const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1); const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007]; /** @@ -235,7 +235,7 @@ class WebSocketManager extends EventEmitter { this.debug(`Session ID is present, attempting an immediate reconnect...`, shard); this.reconnect(true); } else { - shard.destroy(undefined, true); + shard.destroy({ reset: true, emit: false, log: false }); this.reconnect(); } }); @@ -245,8 +245,6 @@ class WebSocketManager extends EventEmitter { }); shard.on(ShardEvents.DESTROYED, () => { - shard._cleanupConnection(); - this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); this.client.emit(Events.SHARD_RECONNECTING, shard.id); @@ -342,7 +340,7 @@ class WebSocketManager extends EventEmitter { this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); this.destroyed = true; this.shardQueue.clear(); - for (const shard of this.shards.values()) shard.destroy(1000, true); + for (const shard of this.shards.values()) shard.destroy({ closeCode: 1000, reset: true, emit: false }); } /** diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 1b7d5495..b313c751 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -178,7 +178,8 @@ class WebSocketShard extends EventEmitter { this.off(ShardEvents.CLOSE, onClose); this.off(ShardEvents.READY, onReady); this.off(ShardEvents.RESUMED, onResumed); - this.off(ShardEvents.INVALID_SESSION, onInvalid); + this.off(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.off(ShardEvents.DESTROYED, onInvalidOrDestroyed); }; const onReady = () => { @@ -196,7 +197,7 @@ class WebSocketShard extends EventEmitter { reject(event); }; - const onInvalid = () => { + const onInvalidOrDestroyed = () => { cleanup(); // eslint-disable-next-line prefer-promise-reject-errors reject(); @@ -205,7 +206,8 @@ class WebSocketShard extends EventEmitter { this.once(ShardEvents.READY, onReady); this.once(ShardEvents.RESUMED, onResumed); this.once(ShardEvents.CLOSE, onClose); - this.once(ShardEvents.INVALID_SESSION, onInvalid); + this.once(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.once(ShardEvents.DESTROYED, onInvalidOrDestroyed); if (this.connection && this.connection.readyState === WebSocket.OPEN) { this.debug('An open connection was found, attempting an immediate identify.'); @@ -214,10 +216,9 @@ class WebSocketShard extends EventEmitter { } if (this.connection) { - this.debug(`A connection was found. Cleaning up before continuing. + this.debug(`A connection object was found. Cleaning up before continuing. State: ${CONNECTION_STATE[this.connection.readyState]}`); - this._cleanupConnection(); - this.connection.close(1000); + this.destroy({ emit: false }); } const wsQuery = { v: client.options.ws.version }; @@ -233,9 +234,9 @@ class WebSocketShard extends EventEmitter { this.debug( `[CONNECT] - Gateway: ${gateway} - Version: ${client.options.ws.version} - Encoding: ${WebSocket.encoding} + Gateway : ${gateway} + Version : ${client.options.ws.version} + Encoding : ${WebSocket.encoding} Compression: ${zlib ? 'zlib-stream' : 'none'}`); this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; @@ -338,11 +339,13 @@ class WebSocketShard extends EventEmitter { this.debug(`[CLOSE] Event Code: ${event.code} - Clean: ${event.wasClean} - Reason: ${event.reason || 'No reason received'}`); + Clean : ${event.wasClean} + Reason : ${event.reason || 'No reason received'}`); this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); + // If we still have a connection object, clean up its listeners + if (this.connection) this._cleanupConnection(); this.status = Status.DISCONNECTED; @@ -362,7 +365,7 @@ class WebSocketShard extends EventEmitter { */ onPacket(packet) { if (!packet) { - this.debug(`Received broken packet: ${packet}.`); + this.debug(`Received broken packet: '${packet}'.`); return; } @@ -406,7 +409,7 @@ class WebSocketShard extends EventEmitter { this.identify(); break; case OPCodes.RECONNECT: - this.connection.close(1001); + this.destroy({ closeCode: 4000 }); break; case OPCodes.INVALID_SESSION: this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`); @@ -418,7 +421,7 @@ class WebSocketShard extends EventEmitter { // Reset the sequence this.sequence = -1; // Reset the session ID as it's invalid - this.sessionID = null; + this.sessionID = undefined; // Set the status to reconnecting this.status = Status.RECONNECTING; // Finally, emit the INVALID_SESSION event @@ -495,7 +498,7 @@ class WebSocketShard extends EventEmitter { this.debug('Setting a HELLO timeout for 20s.'); this.helloTimeout = this.manager.client.setTimeout(() => { this.debug('Did not receive HELLO in time. Destroying and connecting again.'); - this.destroy(4009); + this.destroy({ reset: true, closeCode: 4009 }); }, 20000); } @@ -535,9 +538,9 @@ class WebSocketShard extends EventEmitter { `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. Status : ${STATUS_KEYS[this.status]} Sequence : ${this.sequence} - Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}` - ); - this.destroy(4009); + Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`); + + this.destroy({ closeCode: 4009, reset: true }); return; } @@ -636,8 +639,8 @@ class WebSocketShard extends EventEmitter { */ _send(data) { if (!this.connection || this.connection.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available! Resetting the shard...`); - this.destroy(4000); + this.debug(`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`); + this.destroy({ close: 4000 }); return; } @@ -670,35 +673,61 @@ class WebSocketShard extends EventEmitter { /** * Destroys this shard and closes its WebSocket connection. - * @param {number} [closeCode=1000] The close code to use - * @param {boolean} [cleanup=false] If the shard should attempt a reconnect + * @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard * @private */ - destroy(closeCode = 1000, cleanup = false) { - this.debug(`Destroying with close code ${closeCode}, attempting a reconnect: ${!cleanup}`); + destroy({ closeCode = 1000, reset = false, emit = true, log = true } = {}) { + if (log) { + this.debug(`[DESTROY] + Close Code : ${closeCode} + Reset : ${reset} + Emit DESTROYED: ${emit}`); + } + // Step 0: Remove all timers this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); - // Close the WebSocket connection, if any - if (this.connection && this.connection.readyState === WebSocket.OPEN) { - this.connection.close(closeCode); - } else if (!cleanup) { - /** - * Emitted when a shard is destroyed, but no WebSocket connection was present. - * @private - * @event WebSocketShard#destroyed - */ - this.emit(ShardEvents.DESTROYED); + // Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED + if (this.connection) { + // If the connection is currently opened, we will (hopefully) receive close + if (this.connection.readyState === WebSocket.OPEN) { + this.connection.close(closeCode); + } else { + // Connection is not OPEN + this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`); + // Remove listeners from the connection + this._cleanupConnection(); + // Attempt to close the connection just in case + try { + this.connection.close(closeCode); + } catch { + // No-op + } + // Emit the destroyed event if needed + if (emit) this._emitDestroyed(); + } + } else if (emit) { + // We requested a destroy, but we had no connection. Emit destroyed + this._emitDestroyed(); } + // Step 2: Null the connection object this.connection = null; - // Set the shard status + + // Step 3: Set the shard status to DISCONNECTED this.status = Status.DISCONNECTED; + + // Step 4: Cache the old sequence (use to attempt a resume) if (this.sequence !== -1) this.closeSequence = this.sequence; - // Reset the sequence - this.sequence = -1; - // Reset the ratelimit data + + // Step 5: Reset the sequence and session ID if requested + if (reset) { + this.sequence = -1; + this.sessionID = undefined; + } + + // Step 6: reset the ratelimit data this.ratelimit.remaining = this.ratelimit.total; this.ratelimit.queue.length = 0; if (this.ratelimit.timer) { @@ -717,6 +746,19 @@ class WebSocketShard extends EventEmitter { this.connection.onerror = this.connection.onmessage = null; } + + /** + * Emits the DESTROYED event on the shard + * @private + */ + _emitDestroyed() { + /** + * Emitted when a shard is destroyed, but no WebSocket connection was present. + * @private + * @event WebSocketShard#destroyed + */ + this.emit(ShardEvents.DESTROYED); + } } module.exports = WebSocketShard; diff --git a/typings/index.d.ts b/typings/index.d.ts index 8154f3f0..e59387f4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1727,8 +1727,9 @@ declare module 'discord.js' { private identifyResume(): void; private _send(data: object): void; private processQueue(): void; - private destroy(closeCode: number): void; + private destroy(destroyOptions?: { closeCode?: number; reset?: boolean; emit?: boolean; log?: boolean }): void; private _cleanupConnection(): void; + private _emitDestroyed(): void; public send(data: object): void; public on(event: 'ready', listener: () => void): this; From 3f039016af127c31cffe0733c3b4b31566a6d146 Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 7 Feb 2020 18:27:05 +0100 Subject: [PATCH 364/428] fix(GuildMember): manageable - let owner override (#3765) * if the bot is owner of the guild the target is managebale * even though both roles are on the same position --- src/structures/GuildMember.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index dcd5a93d..8a8b8bb6 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -216,6 +216,7 @@ class GuildMember extends Base { get manageable() { if (this.user.id === this.guild.ownerID) return false; if (this.user.id === this.client.user.id) return false; + if (this.client.user.id === this.guild.ownerID) return true; if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); return this.guild.me.roles.highest.comparePositionTo(this.roles.highest) > 0; } From fe7df708e44e0280dfaf0f8e457b154781bb5140 Mon Sep 17 00:00:00 2001 From: didinele Date: Fri, 7 Feb 2020 19:46:03 +0200 Subject: [PATCH 365/428] typings: add HTTPOptions#api and export Constants as a value (#3768) * fix(typings): Export Constants correctly * fix(typings): HTTPOptions#api was missing * fix some odd indent * add semi to make CI happy uwu --- typings/index.d.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index e59387f4..07f725bc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -372,7 +372,7 @@ declare module 'discord.js' { type AllowedImageFormat = 'webp' | 'png' | 'jpg' | 'gif'; - export interface Constants { + export const Constants: { Package: { name: string; version: string; @@ -627,7 +627,7 @@ declare module 'discord.js' { ActivityTypes: ActivityType[]; DefaultMessageNotifications: DefaultMessageNotifications[]; MembershipStates: 'INVITED' | 'ACCEPTED'; - } + }; export class DataResolver { public static resolveBase64(data: Base64Resolvable): string; @@ -2363,6 +2363,7 @@ declare module 'discord.js' { } interface HTTPOptions { + api?: string; version?: number; host?: string; cdn?: string; From bbdbc4cfa789383b7b3dbecf5e6b8401ea2dd998 Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Tue, 11 Feb 2020 14:21:07 -0500 Subject: [PATCH 366/428] feat: remove datastores and implement Managers (#3696) * Initial commit: add 5 initial managers - Base manager - GuildChannelManager - MessageManager - PresenceManager - Reaction Manager - Added LimitedCollection * Add GuildEmojiManager, various fixes * Modify some managers and add guildmembermanager * Initial integration * Delete old stores * Integration part two, removed LRUCollection - Most of the integration has been finished - TODO typings - Removed LRUCollection, needless sweeping * Typings + stuff i somehow missed in ChannelManager * LimitedCollection typings/ final changes * Various jsdoc and syntactical fixes, Removed Util.mixin() * tslint fix * Grammatical and logical changes * Delete temporary file placed by mistake * Grammatical changes * Add missing type * Update jsdoc examples * fix: ChannelManager#remove should call cache#delete not cache#remove * fix recursive require * Fix missed cache in util * fix: more missed cache * Remove accidental _fetchMany change from #3645 * fix: use .cache.delete() over .remove() * fix: missing cache in ReactionCollector * fix: missed cache in client * fix: members is a collection not a manager Co-Authored-By: Sugden <28943913+NotSugden@users.noreply.github.com> * fix: various docs and cache fixes * fix: missed cache * fix: missing _roles * Final testing and debugging * LimitedCollection: return the Collection instead of undefined on .set * Add cache to BaseManager in typings * Commit fixes i forgot to stage yesterday * Update invite events * Account for new commit * fix: MessageReactionRemoveAll should call .cache.clear() * fix: add .cache at various places, correct return type * docs: remove mentions of 'store' * Add extra documented properties to typings Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> Co-authored-by: SpaceEEC --- docs/examples/greeting.js | 2 +- src/client/Client.js | 34 ++-- src/client/actions/Action.js | 6 +- src/client/actions/ChannelCreate.js | 2 +- src/client/actions/ChannelDelete.js | 4 +- src/client/actions/ChannelUpdate.js | 6 +- src/client/actions/GuildBanRemove.js | 2 +- .../actions/GuildChannelsPositionUpdate.js | 4 +- src/client/actions/GuildDelete.js | 8 +- src/client/actions/GuildEmojiDelete.js | 2 +- src/client/actions/GuildEmojisUpdate.js | 6 +- src/client/actions/GuildIntegrationsUpdate.js | 2 +- src/client/actions/GuildMemberRemove.js | 6 +- src/client/actions/GuildRoleCreate.js | 4 +- src/client/actions/GuildRoleDelete.js | 6 +- src/client/actions/GuildRoleUpdate.js | 4 +- .../actions/GuildRolesPositionUpdate.js | 4 +- src/client/actions/GuildUpdate.js | 2 +- src/client/actions/InviteCreate.js | 4 +- src/client/actions/InviteDelete.js | 4 +- src/client/actions/MessageCreate.js | 4 +- src/client/actions/MessageDelete.js | 2 +- src/client/actions/MessageDeleteBulk.js | 4 +- .../actions/MessageReactionRemoveAll.js | 2 +- .../actions/MessageReactionRemoveEmoji.js | 2 +- src/client/actions/PresenceUpdate.js | 8 +- src/client/actions/UserUpdate.js | 2 +- src/client/actions/VoiceStateUpdate.js | 8 +- src/client/actions/WebhooksUpdate.js | 2 +- src/client/voice/ClientVoiceManager.js | 2 +- src/client/voice/VoiceConnection.js | 2 +- src/client/websocket/WebSocketManager.js | 2 +- .../websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 +- .../websocket/handlers/GUILD_BAN_ADD.js | 2 +- src/client/websocket/handlers/GUILD_CREATE.js | 2 +- .../websocket/handlers/GUILD_MEMBERS_CHUNK.js | 2 +- .../websocket/handlers/GUILD_MEMBER_ADD.js | 2 +- .../websocket/handlers/GUILD_MEMBER_UPDATE.js | 4 +- src/client/websocket/handlers/READY.js | 2 +- src/client/websocket/handlers/TYPING_START.js | 4 +- src/index.js | 29 ++-- .../DataStore.js => managers/BaseManager.js} | 59 ++++--- .../ChannelManager.js} | 73 +++------ .../GuildChannelManager.js} | 30 ++-- .../GuildEmojiManager.js} | 21 ++- .../GuildEmojiRoleManager.js} | 57 ++++--- .../GuildManager.js} | 23 ++- .../GuildMemberManager.js} | 33 ++-- .../GuildMemberRoleManager.js} | 62 +++---- src/managers/MessageManager.js | 141 ++++++++++++++++ .../PresenceManager.js} | 21 ++- .../ReactionManager.js} | 32 ++-- .../ReactionUserManager.js} | 25 ++- .../RoleStore.js => managers/RoleManager.js} | 39 +++-- .../UserStore.js => managers/UserManager.js} | 19 ++- src/managers/VoiceStateManager.js | 37 +++++ src/sharding/ShardClientUtil.js | 4 +- src/sharding/ShardingManager.js | 2 +- src/stores/MessageStore.js | 135 ---------------- src/stores/VoiceStateStore.js | 26 --- src/structures/CategoryChannel.js | 2 +- src/structures/Channel.js | 4 +- src/structures/DMChannel.js | 8 +- src/structures/Emoji.js | 2 +- src/structures/Guild.js | 85 +++++----- src/structures/GuildAuditLogs.js | 16 +- src/structures/GuildChannel.js | 8 +- src/structures/GuildEmoji.js | 16 +- src/structures/GuildMember.js | 20 +-- src/structures/Integration.js | 2 +- src/structures/Invite.js | 2 +- src/structures/Message.js | 12 +- src/structures/MessageMentions.js | 6 +- src/structures/MessageReaction.js | 18 +-- src/structures/Presence.js | 4 +- src/structures/ReactionCollector.js | 2 +- src/structures/Role.js | 2 +- src/structures/TextChannel.js | 8 +- src/structures/User.js | 10 +- src/structures/VoiceChannel.js | 2 +- src/structures/VoiceState.js | 4 +- src/structures/Webhook.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 10 +- src/util/LimitedCollection.js | 29 ++++ src/util/Structures.js | 2 +- src/util/Util.js | 38 +---- typings/index.d.ts | 153 +++++++++--------- 87 files changed, 804 insertions(+), 705 deletions(-) rename src/{stores/DataStore.js => managers/BaseManager.js} (50%) rename src/{stores/ChannelStore.js => managers/ChannelManager.js} (52%) rename src/{stores/GuildChannelStore.js => managers/GuildChannelManager.js} (85%) rename src/{stores/GuildEmojiStore.js => managers/GuildEmojiManager.js} (89%) rename src/{stores/GuildEmojiRoleStore.js => managers/GuildEmojiRoleManager.js} (73%) rename src/{stores/GuildStore.js => managers/GuildManager.js} (85%) rename src/{stores/GuildMemberStore.js => managers/GuildMemberManager.js} (91%) rename src/{stores/GuildMemberRoleStore.js => managers/GuildMemberRoleManager.js} (77%) create mode 100644 src/managers/MessageManager.js rename src/{stores/PresenceStore.js => managers/PresenceManager.js} (73%) rename src/{stores/ReactionStore.js => managers/ReactionManager.js} (74%) rename src/{stores/ReactionUserStore.js => managers/ReactionUserManager.js} (77%) rename src/{stores/RoleStore.js => managers/RoleManager.js} (78%) rename src/{stores/UserStore.js => managers/UserManager.js} (80%) create mode 100644 src/managers/VoiceStateManager.js delete mode 100644 src/stores/MessageStore.js delete mode 100644 src/stores/VoiceStateStore.js create mode 100644 src/util/LimitedCollection.js diff --git a/docs/examples/greeting.js b/docs/examples/greeting.js index 8fc1dfad..314a7598 100644 --- a/docs/examples/greeting.js +++ b/docs/examples/greeting.js @@ -19,7 +19,7 @@ client.on('ready', () => { // Create an event listener for new guild members client.on('guildMemberAdd', member => { // Send the message to a designated channel on a server: - const channel = member.guild.channels.find(ch => ch.name === 'member-log'); + const channel = member.guild.channels.cache.find(ch => ch.name === 'member-log'); // Do nothing if the channel wasn't found on this server if (!channel) return; // Send the message, mentioning the member diff --git a/src/client/Client.js b/src/client/Client.js index a1ab6672..ae7bbf8b 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -11,10 +11,10 @@ const Webhook = require('../structures/Webhook'); const Invite = require('../structures/Invite'); const ClientApplication = require('../structures/ClientApplication'); const ShardClientUtil = require('../sharding/ShardClientUtil'); -const UserStore = require('../stores/UserStore'); -const ChannelStore = require('../stores/ChannelStore'); -const GuildStore = require('../stores/GuildStore'); -const GuildEmojiStore = require('../stores/GuildEmojiStore'); +const UserManager = require('../managers/UserManager'); +const ChannelManager = require('../managers/ChannelManager'); +const GuildManager = require('../managers/GuildManager'); +const GuildEmojiManager = require('../managers/GuildEmojiManager'); const { Events, browser, DefaultOptions } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); const Structures = require('../util/Structures'); @@ -99,25 +99,25 @@ class Client extends BaseClient { /** * All of the {@link User} objects that have been cached at any point, mapped by their IDs - * @type {UserStore} + * @type {UserManager} */ - this.users = new UserStore(this); + this.users = new UserManager(this); /** * All of the guilds the client is currently handling, mapped by their IDs - * as long as sharding isn't being used, this will be *every* guild the bot is a member of - * @type {GuildStore} + * @type {GuildManager} */ - this.guilds = new GuildStore(this); + this.guilds = new GuildManager(this); /** * All of the {@link Channel}s that the client is currently handling, mapped by their IDs - * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot * is a member of. Note that DM channels will not be initially cached, and thus not be present - * in the store without their explicit fetching or use. - * @type {ChannelStore} + * in the Manager without their explicit fetching or use. + * @type {ChannelManager} */ - this.channels = new ChannelStore(this); + this.channels = new ChannelManager(this); const ClientPresence = Structures.get('ClientPresence'); /** @@ -159,13 +159,13 @@ class Client extends BaseClient { /** * All custom emojis that the client has access to, mapped by their IDs - * @type {GuildEmojiStore} + * @type {GuildEmojiManager} * @readonly */ get emojis() { - const emojis = new GuildEmojiStore({ client: this }); - for (const guild of this.guilds.values()) { - if (guild.available) for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji); + const emojis = new GuildEmojiManager({ client: this }); + for (const guild of this.guilds.cache.values()) { + if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji); } return emojis; } @@ -298,11 +298,11 @@ class Client extends BaseClient { let channels = 0; let messages = 0; - for (const channel of this.channels.values()) { + for (const channel of this.channels.cache.values()) { if (!channel.messages) continue; channels++; - messages += channel.messages.sweep( + messages += channel.messages.cache.sweep( message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs ); } diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index ce4fd6db..2a0bc861 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -23,10 +23,10 @@ class GenericAction { return data; } - getPayload(data, store, id, partialType, cache) { - const existing = store.get(id); + getPayload(data, manager, id, partialType, cache) { + const existing = manager.cache.get(id); if (!existing && this.client.options.partials.includes(partialType)) { - return store.add(data, cache); + return manager.add(data, cache); } return existing; } diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 6830f2ab..fa60a0b3 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -6,7 +6,7 @@ const { Events } = require('../../util/Constants'); class ChannelCreateAction extends Action { handle(data) { const client = this.client; - const existing = client.channels.has(data.id); + const existing = client.channels.cache.has(data.id); const channel = client.channels.add(data); if (!existing && channel) { /** diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index fdfc3870..66fad20d 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -12,13 +12,13 @@ class ChannelDeleteAction extends Action { handle(data) { const client = this.client; - let channel = client.channels.get(data.id); + let channel = client.channels.cache.get(data.id); if (channel) { client.channels.remove(channel.id); channel.deleted = true; if (channel.messages && !(channel instanceof DMChannel)) { - for (const message of channel.messages.values()) { + for (const message of channel.messages.cache.values()) { message.deleted = true; } } diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js index 7b716de0..06bb71b8 100644 --- a/src/client/actions/ChannelUpdate.js +++ b/src/client/actions/ChannelUpdate.js @@ -8,16 +8,16 @@ class ChannelUpdateAction extends Action { handle(data) { const client = this.client; - let channel = client.channels.get(data.id); + let channel = client.channels.cache.get(data.id); if (channel) { const old = channel._update(data); if (ChannelTypes[channel.type.toUpperCase()] !== data.type) { const newChannel = Channel.create(this.client, data, channel.guild); - for (const [id, message] of channel.messages) newChannel.messages.set(id, message); + for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message); newChannel._typing = new Map(channel._typing); channel = newChannel; - this.client.channels.set(channel.id, channel); + this.client.channels.cache.set(channel.id, channel); } return { diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 5a4c0a90..fc28e113 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -6,7 +6,7 @@ const { Events } = require('../../util/Constants'); class GuildBanRemove extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); const user = client.users.add(data.user); /** * Emitted whenever a member is unbanned from a guild. diff --git a/src/client/actions/GuildChannelsPositionUpdate.js b/src/client/actions/GuildChannelsPositionUpdate.js index b2111594..a393167e 100644 --- a/src/client/actions/GuildChannelsPositionUpdate.js +++ b/src/client/actions/GuildChannelsPositionUpdate.js @@ -6,10 +6,10 @@ class GuildChannelsPositionUpdate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { for (const partialChannel of data.channels) { - const channel = guild.channels.get(partialChannel.id); + const channel = guild.channels.cache.get(partialChannel.id); if (channel) channel.rawPosition = partialChannel.position; } } diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 193abc47..69cd9143 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -12,9 +12,9 @@ class GuildDeleteAction extends Action { handle(data) { const client = this.client; - let guild = client.guilds.get(data.id); + let guild = client.guilds.cache.get(data.id); if (guild) { - for (const channel of guild.channels.values()) { + for (const channel of guild.channels.cache.values()) { if (channel.type === 'text') channel.stopTyping(true); } @@ -36,11 +36,11 @@ class GuildDeleteAction extends Action { }; } - for (const channel of guild.channels.values()) this.client.channels.remove(channel.id); + for (const channel of guild.channels.cache.values()) this.client.channels.remove(channel.id); if (guild.voice && guild.voice.connection) guild.voice.connection.disconnect(); // Delete guild - client.guilds.remove(guild.id); + client.guilds.cache.delete(guild.id); guild.deleted = true; /** diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index d5c973af..42af70c1 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -5,7 +5,7 @@ const { Events } = require('../../util/Constants'); class GuildEmojiDeleteAction extends Action { handle(emoji) { - emoji.guild.emojis.remove(emoji.id); + emoji.guild.emojis.cache.delete(emoji.id); emoji.deleted = true; /** * Emitted whenever a custom emoji is deleted in a guild. diff --git a/src/client/actions/GuildEmojisUpdate.js b/src/client/actions/GuildEmojisUpdate.js index d6902d53..77119333 100644 --- a/src/client/actions/GuildEmojisUpdate.js +++ b/src/client/actions/GuildEmojisUpdate.js @@ -4,14 +4,14 @@ const Action = require('./Action'); class GuildEmojisUpdateAction extends Action { handle(data) { - const guild = this.client.guilds.get(data.guild_id); + const guild = this.client.guilds.cache.get(data.guild_id); if (!guild || !guild.emojis) return; - const deletions = new Map(guild.emojis); + const deletions = new Map(guild.emojis.cache); for (const emoji of data.emojis) { // Determine type of emoji event - const cachedEmoji = guild.emojis.get(emoji.id); + const cachedEmoji = guild.emojis.cache.get(emoji.id); if (cachedEmoji) { deletions.delete(emoji.id); if (!cachedEmoji.equals(emoji)) { diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js index a8e91077..4129be6a 100644 --- a/src/client/actions/GuildIntegrationsUpdate.js +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -6,7 +6,7 @@ const { Events } = require('../../util/Constants'); class GuildIntegrationsUpdate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); /** * Emitted whenever a guild integration is updated * @event Client#guildIntegrationsUpdate diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 28aa5038..ed6c0d14 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -6,14 +6,14 @@ const { Events, Status } = require('../../util/Constants'); class GuildMemberRemoveAction extends Action { handle(data, shard) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); let member = null; if (guild) { member = this.getMember(data, guild); guild.memberCount--; if (member) { member.deleted = true; - guild.members.remove(member.id); + guild.members.cache.delete(member.id); /** * Emitted whenever a member leaves a guild, or is kicked. * @event Client#guildMemberRemove @@ -21,7 +21,7 @@ class GuildMemberRemoveAction extends Action { */ if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } - guild.voiceStates.delete(data.user.id); + guild.voiceStates.cache.delete(data.user.id); } return { guild, member }; } diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js index 36111f06..65c64f36 100644 --- a/src/client/actions/GuildRoleCreate.js +++ b/src/client/actions/GuildRoleCreate.js @@ -6,10 +6,10 @@ const { Events } = require('../../util/Constants'); class GuildRoleCreate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); let role; if (guild) { - const already = guild.roles.has(data.role.id); + const already = guild.roles.cache.has(data.role.id); role = guild.roles.add(data.role); /** * Emitted whenever a role is created. diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js index 31b13b81..ec9df823 100644 --- a/src/client/actions/GuildRoleDelete.js +++ b/src/client/actions/GuildRoleDelete.js @@ -6,13 +6,13 @@ const { Events } = require('../../util/Constants'); class GuildRoleDeleteAction extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); let role; if (guild) { - role = guild.roles.get(data.role_id); + role = guild.roles.cache.get(data.role_id); if (role) { - guild.roles.remove(data.role_id); + guild.roles.cache.delete(data.role_id); role.deleted = true; /** * Emitted whenever a guild role is deleted. diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js index bf61c787..631746d2 100644 --- a/src/client/actions/GuildRoleUpdate.js +++ b/src/client/actions/GuildRoleUpdate.js @@ -6,12 +6,12 @@ const { Events } = require('../../util/Constants'); class GuildRoleUpdateAction extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { let old = null; - const role = guild.roles.get(data.role.id); + const role = guild.roles.cache.get(data.role.id); if (role) { old = role._update(data.role); /** diff --git a/src/client/actions/GuildRolesPositionUpdate.js b/src/client/actions/GuildRolesPositionUpdate.js index f09f1143..d7abca97 100644 --- a/src/client/actions/GuildRolesPositionUpdate.js +++ b/src/client/actions/GuildRolesPositionUpdate.js @@ -6,10 +6,10 @@ class GuildRolesPositionUpdate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { for (const partialRole of data.roles) { - const role = guild.roles.get(partialRole.id); + const role = guild.roles.cache.get(partialRole.id); if (role) role.rawPosition = partialRole.position; } } diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js index 6d7cf9b4..c40fc0cd 100644 --- a/src/client/actions/GuildUpdate.js +++ b/src/client/actions/GuildUpdate.js @@ -7,7 +7,7 @@ class GuildUpdateAction extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.id); + const guild = client.guilds.cache.get(data.id); if (guild) { const old = guild._update(data); /** diff --git a/src/client/actions/InviteCreate.js b/src/client/actions/InviteCreate.js index 5552ea2f..63813315 100644 --- a/src/client/actions/InviteCreate.js +++ b/src/client/actions/InviteCreate.js @@ -7,8 +7,8 @@ const { Events } = require('../../util/Constants'); class InviteCreateAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); - const guild = client.guilds.get(data.guild_id); + const channel = client.channels.cache.get(data.channel_id); + const guild = client.guilds.cache.get(data.guild_id); if (!channel && !guild) return false; const inviteData = Object.assign(data, { channel, guild }); diff --git a/src/client/actions/InviteDelete.js b/src/client/actions/InviteDelete.js index 83933d34..92692c3e 100644 --- a/src/client/actions/InviteDelete.js +++ b/src/client/actions/InviteDelete.js @@ -7,8 +7,8 @@ const { Events } = require('../../util/Constants'); class InviteDeleteAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); - const guild = client.guilds.get(data.guild_id); + const channel = client.channels.cache.get(data.channel_id); + const guild = client.guilds.cache.get(data.guild_id); if (!channel && !guild) return false; const inviteData = Object.assign(data, { channel, guild }); diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index 3772c4a1..ddb56ea9 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -6,9 +6,9 @@ const { Events } = require('../../util/Constants'); class MessageCreateAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); + const channel = client.channels.cache.get(data.channel_id); if (channel) { - const existing = channel.messages.get(data.id); + const existing = channel.messages.cache.get(data.id); if (existing) return { message: existing }; const message = channel.messages.add(data); const user = message.author; diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index feb118c1..7869365c 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -11,7 +11,7 @@ class MessageDeleteAction extends Action { if (channel) { message = this.getMessage(data, channel); if (message) { - channel.messages.delete(message.id); + channel.messages.cache.delete(message.id); message.deleted = true; /** * Emitted whenever a message is deleted. diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index f80bc7c4..00fd54b5 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -7,7 +7,7 @@ const { Events } = require('../../util/Constants'); class MessageDeleteBulkAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); + const channel = client.channels.cache.get(data.channel_id); if (channel) { const ids = data.ids; @@ -20,7 +20,7 @@ class MessageDeleteBulkAction extends Action { if (message) { message.deleted = true; messages.set(message.id, message); - channel.messages.delete(id); + channel.messages.cache.delete(id); } } diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js index 0921ce50..14b79bf0 100644 --- a/src/client/actions/MessageReactionRemoveAll.js +++ b/src/client/actions/MessageReactionRemoveAll.js @@ -13,7 +13,7 @@ class MessageReactionRemoveAll extends Action { const message = this.getMessage(data, channel); if (!message) return false; - message.reactions.clear(); + message.reactions.cache.clear(); this.client.emit(Events.MESSAGE_REACTION_REMOVE_ALL, message); return { message }; diff --git a/src/client/actions/MessageReactionRemoveEmoji.js b/src/client/actions/MessageReactionRemoveEmoji.js index ab0eaa77..143702c0 100644 --- a/src/client/actions/MessageReactionRemoveEmoji.js +++ b/src/client/actions/MessageReactionRemoveEmoji.js @@ -13,7 +13,7 @@ class MessageReactionRemoveEmoji extends Action { const reaction = this.getReaction(data, message); if (!reaction) return false; - if (!message.partial) message.reactions.delete(reaction.emoji.id || reaction.emoji.name); + if (!message.partial) message.reactions.cache.delete(reaction.emoji.id || reaction.emoji.name); /** * Emitted when a bot removes an emoji reaction from a cached message. diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 538789e2..f74fbeb5 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -5,7 +5,7 @@ const { Events } = require('../../util/Constants'); class PresenceUpdateAction extends Action { handle(data) { - let user = this.client.users.get(data.user.id); + let user = this.client.users.cache.get(data.user.id); if (!user && data.user.username) user = this.client.users.add(data.user); if (!user) return; @@ -13,12 +13,12 @@ class PresenceUpdateAction extends Action { if (!user.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); } - const guild = this.client.guilds.get(data.guild_id); + const guild = this.client.guilds.cache.get(data.guild_id); if (!guild) return; - let oldPresence = guild.presences.get(user.id); + let oldPresence = guild.presences.cache.get(user.id); if (oldPresence) oldPresence = oldPresence._clone(); - let member = guild.members.get(user.id); + let member = guild.members.cache.get(user.id); if (!member && data.status !== 'offline') { member = guild.members.add({ user, diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index a762511f..7279ca7d 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -7,7 +7,7 @@ class UserUpdateAction extends Action { handle(data) { const client = this.client; - const newUser = client.users.get(data.id); + const newUser = client.users.cache.get(data.id); const oldUser = newUser._update(data); if (!oldUser.equals(newUser)) { diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 386bf778..b2a4b11b 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -7,17 +7,17 @@ const VoiceState = require('../../structures/VoiceState'); class VoiceStateUpdate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { // Update the state - const oldState = guild.voiceStates.has(data.user_id) ? - guild.voiceStates.get(data.user_id)._clone() : + const oldState = guild.voiceStates.cache.has(data.user_id) ? + guild.voiceStates.cache.get(data.user_id)._clone() : new VoiceState(guild, { user_id: data.user_id }); const newState = guild.voiceStates.add(data); // Get the member - let member = guild.members.get(data.user_id); + let member = guild.members.cache.get(data.user_id); if (member && data.member) { member._patch(data.member); } else if (data.member && data.member.user && data.member.joined_at) { diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 69e28aec..f6a515b2 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -6,7 +6,7 @@ const { Events } = require('../../util/Constants'); class WebhooksUpdate extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); + const channel = client.channels.cache.get(data.channel_id); /** * Emitted whenever a guild text channel has its webhooks changed. * @event Client#webhookUpdate diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index cf752662..fbf5d1ba 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -56,7 +56,7 @@ class ClientVoiceManager { this.connections.delete(guild_id); return; } - connection.channel = this.client.channels.get(channel_id); + connection.channel = this.client.channels.cache.get(channel_id); connection.setSessionID(session_id); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b4b1e9da..050775b6 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -483,7 +483,7 @@ class VoiceConnection extends EventEmitter { onSpeaking({ user_id, speaking }) { speaking = new Speaking(speaking).freeze(); const guild = this.channel.guild; - const user = this.client.users.get(user_id); + const user = this.client.users.cache.get(user_id); const old = this._speaking.get(user_id); this._speaking.set(user_id, speaking); /** diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 3e299c58..02139f48 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -408,7 +408,7 @@ class WebSocketManager extends EventEmitter { if (this.client.options.fetchAllMembers) { try { - const promises = this.client.guilds.map(guild => { + const promises = this.client.guilds.cache.map(guild => { if (guild.available) return guild.members.fetch(); // Return empty promise if guild is unavailable return Promise.resolve(); diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index 6da643ff..13e6f0fa 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -3,7 +3,7 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - const channel = client.channels.get(data.channel_id); + const channel = client.channels.cache.get(data.channel_id); const time = new Date(data.last_pin_timestamp); if (channel && !Number.isNaN(time.getTime())) { diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js index cbb60e13..5d4a0965 100644 --- a/src/client/websocket/handlers/GUILD_BAN_ADD.js +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -3,7 +3,7 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); const user = client.users.add(data.user); /** diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index 33cc0c23..eb897443 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -3,7 +3,7 @@ const { Events, Status } = require('../../../util/Constants'); module.exports = async (client, { d: data }, shard) => { - let guild = client.guilds.get(data.id); + let guild = client.guilds.cache.get(data.id); if (guild) { if (!guild.available && !data.unavailable) { // A newly available guild diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js index 0738eaa6..9a0a880a 100644 --- a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -4,7 +4,7 @@ const { Events } = require('../../../util/Constants'); const Collection = require('../../../util/Collection'); module.exports = (client, { d: data }) => { - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (!guild) return; const members = new Collection(); diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js index 796b6c37..964e3da2 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -3,7 +3,7 @@ const { Events, Status } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { guild.memberCount++; const member = guild.members.add(data); diff --git a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js index 9341329a..92c9da6c 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -3,9 +3,9 @@ const { Status, Events } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { - const member = guild.members.get(data.user.id); + const member = guild.members.cache.get(data.user.id); if (member) { const old = member._update(data); if (shard.status === Status.READY) { diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 00257502..c38b681c 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -9,7 +9,7 @@ module.exports = (client, { d: data }, shard) => { if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); const clientUser = new ClientUser(client, data.user); client.user = clientUser; - client.users.set(clientUser.id, clientUser); + client.users.cache.set(clientUser.id, clientUser); } for (const guild of data.guilds) { diff --git a/src/client/websocket/handlers/TYPING_START.js b/src/client/websocket/handlers/TYPING_START.js index 9df76dc7..92e0125f 100644 --- a/src/client/websocket/handlers/TYPING_START.js +++ b/src/client/websocket/handlers/TYPING_START.js @@ -3,8 +3,8 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - const channel = client.channels.get(data.channel_id); - const user = client.users.get(data.user_id); + const channel = client.channels.cache.get(data.channel_id); + const user = client.users.cache.get(data.user_id); if (channel && user) { /** diff --git a/src/index.js b/src/index.js index 896919c4..155d2f53 100644 --- a/src/index.js +++ b/src/index.js @@ -17,7 +17,8 @@ module.exports = { Collection: require('./util/Collection'), Constants: require('./util/Constants'), DataResolver: require('./util/DataResolver'), - DataStore: require('./stores/DataStore'), + LimitedCollection: require('./util/LimitedCollection'), + BaseManager: require('./managers/BaseManager'), DiscordAPIError: require('./rest/DiscordAPIError'), HTTPError: require('./rest/HTTPError'), MessageFlags: require('./util/MessageFlags'), @@ -30,19 +31,19 @@ module.exports = { Util: Util, version: require('../package.json').version, - // Stores - ChannelStore: require('./stores/ChannelStore'), - GuildChannelStore: require('./stores/GuildChannelStore'), - GuildEmojiStore: require('./stores/GuildEmojiStore'), - GuildEmojiRoleStore: require('./stores/GuildEmojiRoleStore'), - GuildMemberStore: require('./stores/GuildMemberStore'), - GuildMemberRoleStore: require('./stores/GuildMemberRoleStore'), - GuildStore: require('./stores/GuildStore'), - ReactionUserStore: require('./stores/ReactionUserStore'), - MessageStore: require('./stores/MessageStore'), - PresenceStore: require('./stores/PresenceStore'), - RoleStore: require('./stores/RoleStore'), - UserStore: require('./stores/UserStore'), + // Managers + ChannelManager: require('./managers/ChannelManager'), + GuildChannelManager: require('./managers/GuildChannelManager'), + GuildEmojiManager: require('./managers/GuildEmojiManager'), + GuildEmojiRoleManager: require('./managers/GuildEmojiRoleManager'), + GuildMemberManager: require('./managers/GuildMemberManager'), + GuildMemberRoleManager: require('./managers/GuildMemberRoleManager'), + GuildManager: require('./managers/GuildManager'), + ReactionUserManager: require('./managers/ReactionUserManager'), + MessageManager: require('./managers/MessageManager'), + PresenceManager: require('./managers/PresenceManager'), + RoleManager: require('./managers/RoleManager'), + UserManager: require('./managers/UserManager'), // Shortcuts to Util methods discordSort: Util.discordSort, diff --git a/src/stores/DataStore.js b/src/managers/BaseManager.js similarity index 50% rename from src/stores/DataStore.js rename to src/managers/BaseManager.js index be1e93d7..b9d0be93 100644 --- a/src/stores/DataStore.js +++ b/src/managers/BaseManager.js @@ -4,44 +4,67 @@ const Collection = require('../util/Collection'); let Structures; /** - * Manages the creation, retrieval and deletion of a specific data model. - * @extends {Collection} + * Manages the API methods of a data model and holds its cache. + * @abstract */ -class DataStore extends Collection { - constructor(client, iterable, holds) { - super(); +class BaseManager { + constructor(client, iterable, holds, cacheType = Collection, ...cacheOptions) { if (!Structures) Structures = require('../util/Structures'); - Object.defineProperty(this, 'client', { value: client }); + /** + * The data structure belonging to this manager + * @name BaseManager#holds + * @type {Function} + * @private + * @readonly + */ Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) || holds }); - if (iterable) for (const item of iterable) this.add(item); + + /** + * The client that instantiated this Manager + * @name BaseManager#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The type of Collection of the Manager + * @type {Collection} + */ + this.cacheType = cacheType; + + /** + * Holds the cache for the data model + * @type {?Collection} + */ + this.cache = new cacheType(...cacheOptions); + if (iterable) for (const i of iterable) this.add(i); } add(data, cache = true, { id, extras = [] } = {}) { - const existing = this.get(id || data.id); + const existing = this.cache.get(id || data.id); if (existing && existing._patch && cache) existing._patch(data); if (existing) return existing; const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; - if (cache) this.set(id || entry.id, entry); + if (cache) this.cache.set(id || entry.id, entry); return entry; } - remove(key) { return this.delete(key); } - /** * Resolves a data entry to a data Object. - * @param {string|Object} idOrInstance The id or instance of something in this DataStore - * @returns {?Object} An instance from this DataStore + * @param {string|Object} idOrInstance The id or instance of something in this Manager + * @returns {?Object} An instance from this Manager */ resolve(idOrInstance) { if (idOrInstance instanceof this.holds) return idOrInstance; - if (typeof idOrInstance === 'string') return this.get(idOrInstance) || null; + if (typeof idOrInstance === 'string') return this.cache.get(idOrInstance) || null; return null; } /** * Resolves a data entry to a instance ID. - * @param {string|Instance} idOrInstance The id or instance of something in this DataStore + * @param {string|Instance} idOrInstance The id or instance of something in this Manager * @returns {?Snowflake} */ resolveID(idOrInstance) { @@ -49,10 +72,6 @@ class DataStore extends Collection { if (typeof idOrInstance === 'string') return idOrInstance; return null; } - - static get [Symbol.species]() { - return Collection; - } } -module.exports = DataStore; +module.exports = BaseManager; diff --git a/src/stores/ChannelStore.js b/src/managers/ChannelManager.js similarity index 52% rename from src/stores/ChannelStore.js rename to src/managers/ChannelManager.js index 762f0e19..dcb098c1 100644 --- a/src/stores/ChannelStore.js +++ b/src/managers/ChannelManager.js @@ -1,59 +1,26 @@ 'use strict'; -const DataStore = require('./DataStore'); const Channel = require('../structures/Channel'); +const BaseManager = require('./BaseManager'); const { Events } = require('../util/Constants'); -const kLru = Symbol('LRU'); -const lruable = ['dm']; - /** - * Stores channels. - * @extends {DataStore} + * A manager of channels belonging to a client */ -class ChannelStore extends DataStore { - constructor(client, iterableOrOptions = {}, options) { - if (!options && typeof iterableOrOptions[Symbol.iterator] !== 'function') { - options = iterableOrOptions; - iterableOrOptions = undefined; - } - super(client, iterableOrOptions, Channel); - - if (options.lru) { - const lru = this[kLru] = []; - lru.add = item => { - lru.remove(item); - lru.unshift(item); - while (lru.length > options.lru) this.remove(lru[lru.length - 1]); - }; - lru.remove = item => { - const index = lru.indexOf(item); - if (index > -1) lru.splice(index, 1); - }; - } +class ChannelManager extends BaseManager { + constructor(client, iterable) { + super(client, iterable, Channel); } - get(key, peek = false) { - const item = super.get(key); - if (!item || !lruable.includes(item.type)) return item; - if (!peek && this[kLru]) this[kLru].add(key); - return item; - } - - set(key, val) { - if (this[kLru] && lruable.includes(val.type)) this[kLru].add(key); - return super.set(key, val); - } - - delete(key) { - const item = this.get(key, true); - if (!item) return false; - if (this[kLru] && lruable.includes(item.type)) this[kLru].remove(key); - return super.delete(key); - } + /** + * The cache of Channels + * @property {Collection} cache + * @memberof ChannelManager + * @instance + */ add(data, guild, cache = true) { - const existing = this.get(data.id); + const existing = this.cache.get(data.id); if (existing) { if (existing._patch && cache) existing._patch(data); if (guild) guild.channels.add(existing); @@ -67,15 +34,15 @@ class ChannelStore extends DataStore { return null; } - if (cache) this.set(channel.id, channel); + if (cache) this.cache.set(channel.id, channel); return channel; } remove(id) { - const channel = this.get(id); - if (channel.guild) channel.guild.channels.remove(id); - super.remove(id); + const channel = this.cache.get(id); + if (channel.guild) channel.guild.channels.cache.delete(id); + this.cache.delete(id); } /** @@ -88,7 +55,7 @@ class ChannelStore extends DataStore { /** * Resolves a ChannelResolvable to a Channel object. * @method resolve - * @memberof ChannelStore + * @memberof ChannelManager * @instance * @param {ChannelResolvable} channel The channel resolvable to resolve * @returns {?Channel} @@ -97,7 +64,7 @@ class ChannelStore extends DataStore { /** * Resolves a ChannelResolvable to a channel ID string. * @method resolveID - * @memberof ChannelStore + * @memberof ChannelManager * @instance * @param {ChannelResolvable} channel The channel resolvable to resolve * @returns {?Snowflake} @@ -115,7 +82,7 @@ class ChannelStore extends DataStore { * .catch(console.error); */ async fetch(id, cache = true) { - const existing = this.get(id); + const existing = this.cache.get(id); if (existing && !existing.partial) return existing; const data = await this.client.api.channels(id).get(); @@ -123,4 +90,4 @@ class ChannelStore extends DataStore { } } -module.exports = ChannelStore; +module.exports = ChannelManager; diff --git a/src/stores/GuildChannelStore.js b/src/managers/GuildChannelManager.js similarity index 85% rename from src/stores/GuildChannelStore.js rename to src/managers/GuildChannelManager.js index 552b40f0..147a0b20 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/managers/GuildChannelManager.js @@ -1,24 +1,36 @@ 'use strict'; const { ChannelTypes } = require('../util/Constants'); -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const GuildChannel = require('../structures/GuildChannel'); const PermissionOverwrites = require('../structures/PermissionOverwrites'); /** - * Stores guild channels. - * @extends {DataStore} + * Manages API methods for GuildChannels and stores their cache. + * @extends {BaseManager} */ -class GuildChannelStore extends DataStore { +class GuildChannelManager extends BaseManager { constructor(guild, iterable) { super(guild.client, iterable, GuildChannel); + + /** + * The guild this Manager belongs to + * @type {Guild} + */ this.guild = guild; } + /** + * The cache of this Manager + * @property {Collection} cache + * @memberof GuildChannelManager + * @instance + */ + add(channel) { - const existing = this.get(channel.id); + const existing = this.cache.get(channel.id); if (existing) return existing; - this.set(channel.id, channel); + this.cache.set(channel.id, channel); return channel; } @@ -32,7 +44,7 @@ class GuildChannelStore extends DataStore { /** * Resolves a GuildChannelResolvable to a Channel object. * @method resolve - * @memberof GuildChannelStore + * @memberof GuildChannelManager * @instance * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve * @returns {?Channel} @@ -41,7 +53,7 @@ class GuildChannelStore extends DataStore { /** * Resolves a GuildChannelResolvable to a channel ID string. * @method resolveID - * @memberof GuildChannelStore + * @memberof GuildChannelManager * @instance * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve * @returns {?Snowflake} @@ -117,4 +129,4 @@ class GuildChannelStore extends DataStore { } } -module.exports = GuildChannelStore; +module.exports = GuildChannelManager; diff --git a/src/stores/GuildEmojiStore.js b/src/managers/GuildEmojiManager.js similarity index 89% rename from src/stores/GuildEmojiStore.js rename to src/managers/GuildEmojiManager.js index b992a68c..27ac363c 100644 --- a/src/stores/GuildEmojiStore.js +++ b/src/managers/GuildEmojiManager.js @@ -1,22 +1,33 @@ 'use strict'; const Collection = require('../util/Collection'); -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const GuildEmoji = require('../structures/GuildEmoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); const DataResolver = require('../util/DataResolver'); const { TypeError } = require('../errors'); /** - * Stores guild emojis. - * @extends {DataStore} + * Manages API methods for GuildEmojis and stores their cache. + * @extends {BaseManager} */ -class GuildEmojiStore extends DataStore { +class GuildEmojiManager extends BaseManager { constructor(guild, iterable) { super(guild.client, iterable, GuildEmoji); + /** + * The guild this manager belongs to + * @type {Guild} + */ this.guild = guild; } + /** + * The cache of GuildEmojis + * @property {Collection} cache + * @memberof GuildEmojiManager + * @instance + */ + add(data, cache) { return super.add(data, cache, { extras: [this.guild] }); } @@ -114,4 +125,4 @@ class GuildEmojiStore extends DataStore { } } -module.exports = GuildEmojiStore; +module.exports = GuildEmojiManager; diff --git a/src/stores/GuildEmojiRoleStore.js b/src/managers/GuildEmojiRoleManager.js similarity index 73% rename from src/stores/GuildEmojiRoleStore.js rename to src/managers/GuildEmojiRoleManager.js index 6db2003e..57759558 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/managers/GuildEmojiRoleManager.js @@ -1,18 +1,28 @@ 'use strict'; const Collection = require('../util/Collection'); -const Util = require('../util/Util'); const { TypeError } = require('../errors'); /** - * Stores emoji roles - * @extends {Collection} + * Manages API methods for roles belonging to emojis and stores their cache. */ -class GuildEmojiRoleStore extends Collection { +class GuildEmojiRoleManager { constructor(emoji) { - super(); + /** + * The emoji belonging to this manager + * @type {GuildEmoji} + */ this.emoji = emoji; + /** + * The guild belonging to this manager + * @type {Guild} + */ this.guild = emoji.guild; + /** + * The client belonging to this manager + * @type {Client} + * @readonly + */ Object.defineProperty(this, 'client', { value: emoji.client }); } @@ -22,8 +32,17 @@ class GuildEmojiRoleStore extends Collection { * @private * @readonly */ - get _filtered() { - return this.guild.roles.filter(role => this.emoji._roles.includes(role.id)); + get _roles() { + return this.guild.roles.cache.filter(role => this.emoji._roles.includes(role.id)); + } + + /** + * The cache of roles belonging to this emoji + * @type {Collection} + * @readonly + */ + get cache() { + return this._roles; } /** @@ -41,7 +60,7 @@ class GuildEmojiRoleStore extends Collection { 'Array or Collection of Roles or Snowflakes', true)); } - const newRoles = [...new Set(roleOrRoles.concat(...this.values()))]; + const newRoles = [...new Set(roleOrRoles.concat(...this._roles.values()))]; return this.set(newRoles); } @@ -60,7 +79,7 @@ class GuildEmojiRoleStore extends Collection { 'Array or Collection of Roles or Snowflakes', true)); } - const newRoles = this.keyArray().filter(role => !roleOrRoles.includes(role)); + const newRoles = this._roles.keyArray().filter(role => !roleOrRoles.includes(role)); return this.set(newRoles); } @@ -85,32 +104,18 @@ class GuildEmojiRoleStore extends Collection { clone() { const clone = new this.constructor(this.emoji); - clone._patch(this.keyArray().slice()); + clone._patch(this._roles.keyArray().slice()); return clone; } /** - * Patches the roles for this store + * Patches the roles for this manager's cache * @param {Snowflake[]} roles The new roles * @private */ _patch(roles) { this.emoji._roles = roles; } - - *[Symbol.iterator]() { - yield* this._filtered.entries(); - } - - valueOf() { - return this._filtered; - } - - static get [Symbol.species]() { - return Collection; - } } -Util.mixin(GuildEmojiRoleStore, ['set']); - -module.exports = GuildEmojiRoleStore; +module.exports = GuildEmojiRoleManager; diff --git a/src/stores/GuildStore.js b/src/managers/GuildManager.js similarity index 85% rename from src/stores/GuildStore.js rename to src/managers/GuildManager.js index eb7090b6..5666156b 100644 --- a/src/stores/GuildStore.js +++ b/src/managers/GuildManager.js @@ -1,6 +1,6 @@ 'use strict'; -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const DataResolver = require('../util/DataResolver'); const { Events } = require('../util/Constants'); const Guild = require('../structures/Guild'); @@ -9,14 +9,21 @@ const GuildMember = require('../structures/GuildMember'); const Role = require('../structures/Role'); /** - * Stores guilds. - * @extends {DataStore} + * Manages API methods for Guilds and stores their cache. + * @extends {BaseManager} */ -class GuildStore extends DataStore { +class GuildManager extends BaseManager { constructor(client, iterable) { super(client, iterable, Guild); } + /** + * The cache of this Manager + * @property {Collection} cache + * @memberof GuildManager + * @instance + */ + /** * Data that resolves to give a Guild object. This can be: * * A Guild object @@ -29,7 +36,7 @@ class GuildStore extends DataStore { /** * Resolves a GuildResolvable to a Guild object. * @method resolve - * @memberof GuildStore + * @memberof GuildManager * @instance * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Guild} @@ -44,7 +51,7 @@ class GuildStore extends DataStore { /** * Resolves a GuildResolvable to a Guild ID string. * @method resolveID - * @memberof GuildStore + * @memberof GuildManager * @instance * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Snowflake} @@ -70,7 +77,7 @@ class GuildStore extends DataStore { return new Promise((resolve, reject) => this.client.api.guilds.post({ data: { name, region, icon } }) .then(data => { - if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id)); + if (this.client.guilds.cache.has(data.id)) return resolve(this.client.guilds.cache.get(data.id)); const handleGuild = guild => { if (guild.id === data.id) { @@ -95,4 +102,4 @@ class GuildStore extends DataStore { } } -module.exports = GuildStore; +module.exports = GuildManager; diff --git a/src/stores/GuildMemberStore.js b/src/managers/GuildMemberManager.js similarity index 91% rename from src/stores/GuildMemberStore.js rename to src/managers/GuildMemberManager.js index 7abc81ca..b7d49f5c 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/managers/GuildMemberManager.js @@ -1,21 +1,32 @@ 'use strict'; -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const GuildMember = require('../structures/GuildMember'); const { Events, OPCodes } = require('../util/Constants'); const Collection = require('../util/Collection'); const { Error, TypeError } = require('../errors'); /** - * Stores guild members. - * @extends {DataStore} + * Manages API methods for GuildMembers and stores their cache. + * @extends {BaseManager} */ -class GuildMemberStore extends DataStore { +class GuildMemberManager extends BaseManager { constructor(guild, iterable) { super(guild.client, iterable, GuildMember); + /** + * The guild this manager belongs to + * @type {Guild} + */ this.guild = guild; } + /** + * The cache of this Manager + * @property {Collection} cache + * @memberof GuildMemberManager + * @instance + */ + add(data, cache = true) { return super.add(data, cache, { id: data.user.id, extras: [this.guild] }); } @@ -49,7 +60,7 @@ class GuildMemberStore extends DataStore { const memberResolvable = super.resolveID(member); if (memberResolvable) return memberResolvable; const userResolvable = this.client.users.resolveID(member); - return this.has(userResolvable) ? userResolvable : null; + return this.cache.has(userResolvable) ? userResolvable : null; } /** @@ -184,7 +195,7 @@ class GuildMemberStore extends DataStore { _fetchSingle({ user, cache }) { - const existing = this.get(user); + const existing = this.cache.get(user); if (existing && !existing.partial) return Promise.resolve(existing); return this.client.api.guilds(this.guild.id).members(user).get() .then(data => this.add(data, cache)); @@ -192,8 +203,8 @@ class GuildMemberStore extends DataStore { _fetchMany({ query = '', limit = 0 } = {}) { return new Promise((resolve, reject) => { - if (this.guild.memberCount === this.size && !query && !limit) { - resolve(this); + if (this.guild.memberCount === this.cache.size && !query && !limit) { + resolve(this.cache); return; } this.guild.shard.send({ @@ -211,11 +222,11 @@ class GuildMemberStore extends DataStore { for (const member of members.values()) { if (query || limit) fetchedMembers.set(member.id, member); } - if (this.guild.memberCount <= this.size || + if (this.guild.memberCount <= this.cache.size || ((query || limit) && members.size < 1000) || (limit && fetchedMembers.size >= limit)) { this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); - resolve(query || limit ? fetchedMembers : this); + resolve(query || limit ? fetchedMembers : this.cache); } }; const timeout = this.guild.client.setTimeout(() => { @@ -227,4 +238,4 @@ class GuildMemberStore extends DataStore { } } -module.exports = GuildMemberStore; +module.exports = GuildMemberManager; diff --git a/src/stores/GuildMemberRoleStore.js b/src/managers/GuildMemberRoleManager.js similarity index 77% rename from src/stores/GuildMemberRoleStore.js rename to src/managers/GuildMemberRoleManager.js index 047b8086..d9186f90 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/managers/GuildMemberRoleManager.js @@ -1,17 +1,22 @@ 'use strict'; const Collection = require('../util/Collection'); -const Util = require('../util/Util'); const { TypeError } = require('../errors'); /** - * Stores member roles - * @extends {Collection} + * Manages API methods for roles of a GuildMember and stores their cache. */ -class GuildMemberRoleStore extends Collection { +class GuildMemberRoleManager { constructor(member) { - super(); + /** + * The GuildMember this manager belongs to + * @type {GuildMember} + */ this.member = member; + /** + * The Guild this manager belongs to + * @type {Guild} + */ this.guild = member.guild; Object.defineProperty(this, 'client', { value: member.client }); } @@ -22,9 +27,18 @@ class GuildMemberRoleStore extends Collection { * @private * @readonly */ - get _filtered() { + get _roles() { const everyone = this.guild.roles.everyone; - return this.guild.roles.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); + return this.guild.roles.cache.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); + } + + /** + * The roles of this member + * @type {Collection} + * @readonly + */ + get cache() { + return this._roles; } /** @@ -33,7 +47,7 @@ class GuildMemberRoleStore extends Collection { * @readonly */ get hoist() { - const hoistedRoles = this._filtered.filter(role => role.hoist); + const hoistedRoles = this._roles.filter(role => role.hoist); if (!hoistedRoles.size) return null; return hoistedRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); } @@ -44,7 +58,7 @@ class GuildMemberRoleStore extends Collection { * @readonly */ get color() { - const coloredRoles = this._filtered.filter(role => role.color); + const coloredRoles = this._roles.filter(role => role.color); if (!coloredRoles.size) return null; return coloredRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); } @@ -55,7 +69,7 @@ class GuildMemberRoleStore extends Collection { * @readonly */ get highest() { - return this._filtered.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this.first()); + return this._roles.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this._roles.first()); } /** @@ -72,7 +86,7 @@ class GuildMemberRoleStore extends Collection { 'Array or Collection of Roles or Snowflakes', true); } - const newRoles = [...new Set(roleOrRoles.concat(...this.values()))]; + const newRoles = [...new Set(roleOrRoles.concat(...this._roles.values()))]; return this.set(newRoles, reason); } else { roleOrRoles = this.guild.roles.resolve(roleOrRoles); @@ -84,7 +98,7 @@ class GuildMemberRoleStore extends Collection { await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason }); const clone = this.member._clone(); - clone._roles = [...this.keys(), roleOrRoles.id]; + clone._roles = [...this._roles.keys(), roleOrRoles.id]; return clone; } } @@ -103,7 +117,7 @@ class GuildMemberRoleStore extends Collection { 'Array or Collection of Roles or Snowflakes', true); } - const newRoles = this.filter(role => !roleOrRoles.includes(role)); + const newRoles = this._roles.filter(role => !roleOrRoles.includes(role)); return this.set(newRoles, reason); } else { roleOrRoles = this.guild.roles.resolve(roleOrRoles); @@ -115,7 +129,7 @@ class GuildMemberRoleStore extends Collection { await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].delete({ reason }); const clone = this.member._clone(); - const newRoles = this.filter(role => role.id !== roleOrRoles.id); + const newRoles = this._roles.filter(role => role.id !== roleOrRoles.id); clone._roles = [...newRoles.keys()]; return clone; } @@ -134,7 +148,7 @@ class GuildMemberRoleStore extends Collection { * @example * // Remove all the roles from a member * guildMember.roles.set([]) - * .then(member => console.log(`Member roles is now of ${member.roles.size} size`)) + * .then(member => console.log(`Member roles is now of ${member.roles.cache.size} size`)) * .catch(console.error); */ set(roles, reason) { @@ -143,23 +157,9 @@ class GuildMemberRoleStore extends Collection { clone() { const clone = new this.constructor(this.member); - clone.member._roles = [...this.keyArray()]; + clone.member._roles = [...this._roles.keyArray()]; return clone; } - - *[Symbol.iterator]() { - yield* this._filtered.entries(); - } - - valueOf() { - return this._filtered; - } - - static get [Symbol.species]() { - return Collection; - } } -Util.mixin(GuildMemberRoleStore, ['set']); - -module.exports = GuildMemberRoleStore; +module.exports = GuildMemberRoleManager; diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js new file mode 100644 index 00000000..19eb82b8 --- /dev/null +++ b/src/managers/MessageManager.js @@ -0,0 +1,141 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const Message = require('../structures/Message'); +const LimitedCollection = require('../util/LimitedCollection'); +const Collection = require('../util/Collection'); + +/** +* Manages API methods for Messages and holds their cache. +* @extends {BaseManager} +*/ +class MessageManager extends BaseManager { + constructor(channel, iterable) { + super(channel.client, iterable, Message, LimitedCollection, channel.client.options.messageCacheMaxSize); + /** + * The channel that the messages belong to + * @type {TextBasedChannel} + */ + this.channel = channel; + } + + /** + * The cache of Messages + * @property {LimitedCollection} cache + * @memberof MessageManager + * @instance + */ + + add(data, cache) { + return super.add(data, cache, { extras: [this.channel] }); + } + + /** + * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and + * `after` are mutually exclusive. All the parameters are optional. + * @typedef {Object} ChannelLogsQueryOptions + * @property {number} [limit=50] Number of messages to acquire + * @property {Snowflake} [before] ID of a message to get the messages that were posted before it + * @property {Snowflake} [after] ID of a message to get the messages that were posted after it + * @property {Snowflake} [around] ID of a message to get the messages that were posted around it + */ + + /** + * Gets a message, or messages, from this channel. + * The returned Collection does not contain reaction users of the messages if they were not cached. + * Those need to be fetched separately in such a case. + * @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters. + * @param {boolean} [cache=true] Whether to cache the message(s) + * @returns {Promise|Promise>} + * @example + * // Get message + * channel.messages.fetch('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + * @example + * // Get messages + * channel.messages.fetch({ limit: 10 }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Get messages and filter by user ID + * channel.messages.fetch() + * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) + * .catch(console.error); + */ + fetch(message, cache = true) { + return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache); + } + + /** + * Fetches the pinned messages of this channel and returns a collection of them. + * The returned Collection does not contain any reaction data of the messages. + * Those need to be fetched separately. + * @param {boolean} [cache=true] Whether to cache the message(s) + * @returns {Promise>} + * @example + * // Get pinned messages + * channel.fetchPinned() + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ + fetchPinned(cache = true) { + return this.client.api.channels[this.channel.id].pins.get().then(data => { + const messages = new Collection(); + for (const message of data) messages.set(message.id, this.add(message, cache)); + return messages; + }); + } + + /** + * Data that can be resolved to a Message object. This can be: + * * A Message + * * A Snowflake + * @typedef {Message|Snowflake} MessageResolvable + */ + + /** + * Resolves a MessageResolvable to a Message object. + * @method resolve + * @memberof MessageManager + * @instance + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Message} + */ + + /** + * Resolves a MessageResolvable to a Message ID string. + * @method resolveID + * @memberof MessageManager + * @instance + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Snowflake} + */ + + + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user + */ + async delete(message, reason) { + message = this.resolveID(message); + if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); + } + + async _fetchId(messageID, cache) { + const existing = this.cache.get(messageID); + if (existing && !existing.partial) return existing; + const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); + return this.add(data, cache); + } + + async _fetchMany(options = {}, cache) { + const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this.add(message, cache)); + return messages; + } +} + +module.exports = MessageManager; diff --git a/src/stores/PresenceStore.js b/src/managers/PresenceManager.js similarity index 73% rename from src/stores/PresenceStore.js rename to src/managers/PresenceManager.js index 061d3f1e..0a38fd87 100644 --- a/src/stores/PresenceStore.js +++ b/src/managers/PresenceManager.js @@ -1,19 +1,26 @@ 'use strict'; -const DataStore = require('./DataStore'); const { Presence } = require('../structures/Presence'); +const BaseManager = require('./BaseManager'); /** - * Stores presences. - * @extends {DataStore} + * Manages API methods for Presences and holds their cache. + * @extends {BaseManager} */ -class PresenceStore extends DataStore { +class PresenceManager extends BaseManager { constructor(client, iterable) { super(client, iterable, Presence); } + /** + * The cache of Presences + * @property {Collection} cache + * @memberof PresenceManager + * @instance + */ + add(data, cache) { - const existing = this.get(data.user.id); + const existing = this.cache.get(data.user.id); return existing ? existing.patch(data) : super.add(data, cache, { id: data.user.id }); } @@ -46,8 +53,8 @@ class PresenceStore extends DataStore { const presenceResolvable = super.resolveID(presence); if (presenceResolvable) return presenceResolvable; const userResolvable = this.client.users.resolveID(presence); - return this.has(userResolvable) ? userResolvable : null; + return this.cache.has(userResolvable) ? userResolvable : null; } } -module.exports = PresenceStore; +module.exports = PresenceManager; diff --git a/src/stores/ReactionStore.js b/src/managers/ReactionManager.js similarity index 74% rename from src/stores/ReactionStore.js rename to src/managers/ReactionManager.js index 1b1fb603..7350aebd 100644 --- a/src/stores/ReactionStore.js +++ b/src/managers/ReactionManager.js @@ -1,15 +1,20 @@ 'use strict'; -const DataStore = require('./DataStore'); const MessageReaction = require('../structures/MessageReaction'); +const BaseManager = require('./BaseManager'); /** - * Stores reactions. - * @extends {DataStore} + * Manages API methods for reactions and holds their cache. + * @extends {BaseManager} */ -class ReactionStore extends DataStore { +class ReactionManager extends BaseManager { constructor(message, iterable) { super(message.client, iterable, MessageReaction); + + /** + * The message that this manager belongs to + * @type {Message} + */ this.message = message; } @@ -17,6 +22,13 @@ class ReactionStore extends DataStore { return super.add(data, cache, { id: data.emoji.id || data.emoji.name, extras: [this.message] }); } + /** + * The reaction cache of this manager + * @property {Collection} cache + * @memberof ReactionManager + * @instance + */ + /** * Data that can be resolved to a MessageReaction object. This can be: * * A MessageReaction @@ -27,7 +39,7 @@ class ReactionStore extends DataStore { /** * Resolves a MessageReactionResolvable to a MessageReaction object. * @method resolve - * @memberof ReactionStore + * @memberof ReactionManager * @instance * @param {MessageReactionResolvable} reaction The MessageReaction to resolve * @returns {?MessageReaction} @@ -36,7 +48,7 @@ class ReactionStore extends DataStore { /** * Resolves a MessageReactionResolvable to a MessageReaction ID string. * @method resolveID - * @memberof ReactionStore + * @memberof ReactionManager * @instance * @param {MessageReactionResolvable} reaction The MessageReaction to resolve * @returns {?Snowflake} @@ -53,18 +65,18 @@ class ReactionStore extends DataStore { _partial(emoji) { const id = emoji.id || emoji.name; - const existing = this.get(id); + const existing = this.cache.get(id); return !existing || existing.partial; } async _fetchReaction(reactionEmoji, cache) { const id = reactionEmoji.id || reactionEmoji.name; - const existing = this.get(id); + const existing = this.cache.get(id); if (!this._partial(reactionEmoji)) return existing; const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { reactionEmoji.reaction._patch({ count: 0 }); - this.message.reactions.remove(id); + this.message.reactions.cache.delete(id); return existing; } for (const reaction of data.reactions) { @@ -74,4 +86,4 @@ class ReactionStore extends DataStore { } } -module.exports = ReactionStore; +module.exports = ReactionManager; diff --git a/src/stores/ReactionUserStore.js b/src/managers/ReactionUserManager.js similarity index 77% rename from src/stores/ReactionUserStore.js rename to src/managers/ReactionUserManager.js index dc250a9f..c82b119d 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/managers/ReactionUserManager.js @@ -1,19 +1,30 @@ 'use strict'; const Collection = require('../util/Collection'); -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const { Error } = require('../errors'); /** - * A data store to store User models who reacted to a MessageReaction. - * @extends {DataStore} + * Manages API methods for users who reacted to a reaction and stores their cache. + * @extends {BaseManager} */ -class ReactionUserStore extends DataStore { +class ReactionUserManager extends BaseManager { constructor(client, iterable, reaction) { - super(client, iterable, require('../structures/User')); + super(client, iterable, { name: 'User' }); + /** + * The reaction that this manager belongs to + * @type {MessageReaction} + */ this.reaction = reaction; } + /** + * The cache of this manager + * @property {Collection} cache + * @memberof GuildManager + * @instance + */ + /** * 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 @@ -30,7 +41,7 @@ class ReactionUserStore extends DataStore { const users = new Collection(); for (const rawUser of data) { const user = this.client.users.add(rawUser); - this.set(user.id, user); + this.cache.set(user.id, user); users.set(user.id, user); } return users; @@ -52,4 +63,4 @@ class ReactionUserStore extends DataStore { } } -module.exports = ReactionUserStore; +module.exports = ReactionUserManager; diff --git a/src/stores/RoleStore.js b/src/managers/RoleManager.js similarity index 78% rename from src/stores/RoleStore.js rename to src/managers/RoleManager.js index 649048be..9eb03cf2 100644 --- a/src/stores/RoleStore.js +++ b/src/managers/RoleManager.js @@ -1,20 +1,31 @@ 'use strict'; -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const Role = require('../structures/Role'); const { resolveColor } = require('../util/Util'); const Permissions = require('../util/Permissions'); /** - * Stores roles. - * @extends {DataStore} + * Manages API methods for roles and stores their cache. + * @extends {BaseManager} */ -class RoleStore extends DataStore { +class RoleManager extends BaseManager { constructor(guild, iterable) { super(guild.client, iterable, Role); + /** + * The guild belonging to this manager + * @type {Guild} + */ this.guild = guild; } + /** + * The role cache of this manager + * @property {Collection} cache + * @memberof RoleManager + * @instance + */ + add(data, cache) { return super.add(data, cache, { extras: [this.guild] }); } @@ -23,11 +34,11 @@ class RoleStore extends DataStore { * Obtains one or more roles from Discord, or the role cache if they're already available. * @param {Snowflake} [id] ID or IDs of the role(s) * @param {boolean} [cache=true] Whether to cache the new roles objects if it weren't already - * @returns {Promise} + * @returns {Promise} * @example * // Fetch all roles from the guild * message.guild.roles.fetch() - * .then(roles => console.log(`There are ${roles.size} roles.`)) + * .then(roles => console.log(`There are ${roles.cache.size} roles.`)) * .catch(console.error); * @example * // Fetch a single role @@ -37,14 +48,14 @@ class RoleStore extends DataStore { */ async fetch(id, cache = true) { if (id) { - const existing = this.get(id); + const existing = this.cache.get(id); if (existing) return existing; } // We cannot fetch a single role, as of this commit's date, Discord API throws with 405 const roles = await this.client.api.guilds(this.guild.id).roles.get(); for (const role of roles) this.add(role, cache); - return id ? this.get(id) || null : this; + return id ? this.cache.get(id) || null : this; } /** @@ -57,7 +68,7 @@ class RoleStore extends DataStore { /** * Resolves a RoleResolvable to a Role object. * @method resolve - * @memberof RoleStore + * @memberof RoleManager * @instance * @param {RoleResolvable} role The role resolvable to resolve * @returns {?Role} @@ -66,7 +77,7 @@ class RoleStore extends DataStore { /** * Resolves a RoleResolvable to a role ID string. * @method resolveID - * @memberof RoleStore + * @memberof RoleManager * @instance * @param {RoleResolvable} role The role resolvable to resolve * @returns {?Snowflake} @@ -116,17 +127,17 @@ class RoleStore extends DataStore { * @readonly */ get everyone() { - return this.get(this.guild.id) || null; + return this.cache.get(this.guild.id) || null; } /** - * The role with the highest position in the store + * The role with the highest position in the cache * @type {Role} * @readonly */ get highest() { - return this.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this.first()); + return this.cache.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this.cache.first()); } } -module.exports = RoleStore; +module.exports = RoleManager; diff --git a/src/stores/UserStore.js b/src/managers/UserManager.js similarity index 80% rename from src/stores/UserStore.js rename to src/managers/UserManager.js index 20c25d05..8818206a 100644 --- a/src/stores/UserStore.js +++ b/src/managers/UserManager.js @@ -1,19 +1,26 @@ 'use strict'; -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const User = require('../structures/User'); const GuildMember = require('../structures/GuildMember'); const Message = require('../structures/Message'); /** - * A data store to store User models. - * @extends {DataStore} + * Manages API methods for users and stores their cache. + * @extends {BaseManager} */ -class UserStore extends DataStore { +class UserManager extends BaseManager { constructor(client, iterable) { super(client, iterable, User); } + /** + * The cache of this manager + * @property {Collection} cache + * @memberof UserManager + * @instance + */ + /** * Data that resolves to give a User object. This can be: * * A User object @@ -52,11 +59,11 @@ class UserStore extends DataStore { * @returns {Promise} */ async fetch(id, cache = true) { - const existing = this.get(id); + const existing = this.cache.get(id); if (existing && !existing.partial) return existing; const data = await this.client.api.users(id).get(); return this.add(data, cache); } } -module.exports = UserStore; +module.exports = UserManager; diff --git a/src/managers/VoiceStateManager.js b/src/managers/VoiceStateManager.js new file mode 100644 index 00000000..755392b2 --- /dev/null +++ b/src/managers/VoiceStateManager.js @@ -0,0 +1,37 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const VoiceState = require('../structures/VoiceState'); + +/** + * Manages API methods for VoiceStates and stores their cache. + * @extends {BaseManager} + */ +class VoiceStateManager extends BaseManager { + constructor(guild, iterable) { + super(guild.client, iterable, VoiceState); + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this manager + * @property {Collection} cache + * @memberof VoiceStateManager + * @instance + */ + + add(data, cache = true) { + const existing = this.cache.get(data.user_id); + if (existing) return existing._patch(data); + + const entry = new VoiceState(this.guild, data); + if (cache) this.cache.set(data.user_id, entry); + return entry; + } +} + +module.exports = VoiceStateManager; diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 8ed1b978..8b76efd6 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -86,7 +86,7 @@ class ShardClientUtil { * @param {string} prop Name of the client property to get, using periods for nesting * @returns {Promise>} * @example - * client.shard.fetchClientValues('guilds.size') + * client.shard.fetchClientValues('guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); * @see {@link ShardingManager#fetchClientValues} @@ -114,7 +114,7 @@ class ShardClientUtil { * @param {string|Function} script JavaScript to run on each shard * @returns {Promise>} Results of the script execution * @example - * client.shard.broadcastEval('this.guilds.size') + * client.shard.broadcastEval('this.guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); * @see {@link ShardingManager#broadcastEval} diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 7126148d..b6818d30 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -229,7 +229,7 @@ class ShardingManager extends EventEmitter { * @param {string} prop Name of the client property to get, using periods for nesting * @returns {Promise>} * @example - * manager.fetchClientValues('guilds.size') + * manager.fetchClientValues('guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); */ diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js deleted file mode 100644 index 59b224f7..00000000 --- a/src/stores/MessageStore.js +++ /dev/null @@ -1,135 +0,0 @@ -'use strict'; - -const DataStore = require('./DataStore'); -const Collection = require('../util/Collection'); -const Message = require('../structures/Message'); - -/** - * Stores messages for text-based channels. - * @extends {DataStore} - */ -class MessageStore extends DataStore { - constructor(channel, iterable) { - super(channel.client, iterable, Message); - this.channel = channel; - } - - add(data, cache) { - return super.add(data, cache, { extras: [this.channel] }); - } - - set(key, value) { - const maxSize = this.client.options.messageCacheMaxSize; - if (maxSize === 0) return; - if (this.size >= maxSize && maxSize > 0) this.delete(this.firstKey()); - super.set(key, value); - } - - /** - * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and - * `after` are mutually exclusive. All the parameters are optional. - * @typedef {Object} ChannelLogsQueryOptions - * @property {number} [limit=50] Number of messages to acquire - * @property {Snowflake} [before] ID of a message to get the messages that were posted before it - * @property {Snowflake} [after] ID of a message to get the messages that were posted after it - * @property {Snowflake} [around] ID of a message to get the messages that were posted around it - */ - - /** - * Gets a message, or messages, from this channel. - * The returned Collection does not contain reaction users of the messages if they were not cached. - * Those need to be fetched separately in such a case. - * @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters. - * @param {boolean} [cache=true] Whether to cache the message(s) - * @returns {Promise|Promise>} - * @example - * // Get message - * channel.messages.fetch('99539446449315840') - * .then(message => console.log(message.content)) - * .catch(console.error); - * @example - * // Get messages - * channel.messages.fetch({ limit: 10 }) - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); - * @example - * // Get messages and filter by user ID - * channel.messages.fetch() - * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) - * .catch(console.error); - */ - fetch(message, cache = true) { - return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache); - } - - /** - * Fetches the pinned messages of this channel and returns a collection of them. - * The returned Collection does not contain any reaction data of the messages. - * Those need to be fetched separately. - * @param {boolean} [cache=true] Whether to cache the message(s) - * @returns {Promise>} - * @example - * // Get pinned messages - * channel.fetchPinned() - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); - */ - fetchPinned(cache = true) { - return this.client.api.channels[this.channel.id].pins.get().then(data => { - const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message, cache)); - return messages; - }); - } - - /** - * Data that can be resolved to a Message object. This can be: - * * A Message - * * A Snowflake - * @typedef {Message|Snowflake} MessageResolvable - */ - - /** - * Resolves a MessageResolvable to a Message object. - * @method resolve - * @memberof MessageStore - * @instance - * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?Message} - */ - - /** - * Resolves a MessageResolvable to a Message ID string. - * @method resolveID - * @memberof MessageStore - * @instance - * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?Snowflake} - */ - - /** - * Deletes a message, even if it's not cached. - * @param {MessageResolvable} message The message to delete - * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user - */ - async remove(message, reason) { - message = this.resolveID(message); - if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); - } - - async _fetchId(messageID, cache) { - const existing = this.get(messageID); - if (existing && !existing.partial) return existing; - const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); - return this.add(data, cache); - } - - async _fetchMany(options = {}, cache) { - const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); - const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message, cache)); - return messages; - } -} - -module.exports = MessageStore; diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js deleted file mode 100644 index a5eaac2d..00000000 --- a/src/stores/VoiceStateStore.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const DataStore = require('./DataStore'); -const VoiceState = require('../structures/VoiceState'); - -/** - * Stores voice states. - * @extends {DataStore} - */ -class VoiceStateStore extends DataStore { - constructor(guild, iterable) { - super(guild.client, iterable, VoiceState); - this.guild = guild; - } - - add(data, cache = true) { - const existing = this.get(data.user_id); - if (existing) return existing._patch(data); - - const entry = new VoiceState(this.guild, data); - if (cache) this.set(data.user_id, entry); - return entry; - } -} - -module.exports = VoiceStateStore; diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index 60be408c..4ac9fbbb 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -13,7 +13,7 @@ class CategoryChannel extends GuildChannel { * @readonly */ get children() { - return this.guild.channels.filter(c => c.parentID === this.id); + return this.guild.channels.cache.filter(c => c.parentID === this.id); } /** diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 9eade660..d5949c3c 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -100,7 +100,7 @@ class Channel extends Base { const DMChannel = Structures.get('DMChannel'); channel = new DMChannel(client, data); } else { - guild = guild || client.guilds.get(data.guild_id); + guild = guild || client.guilds.cache.get(data.guild_id); if (guild) { switch (data.type) { case ChannelTypes.TEXT: { @@ -129,7 +129,7 @@ class Channel extends Base { break; } } - if (channel) guild.channels.set(channel.id, channel); + if (channel) guild.channels.cache.set(channel.id, channel); } } return channel; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 006a9fab..9042519a 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -2,7 +2,7 @@ const Channel = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); -const MessageStore = require('../stores/MessageStore'); +const MessageManager = require('../managers/MessageManager'); /** * Represents a direct message channel between two users. @@ -19,10 +19,10 @@ class DMChannel extends Channel { // Override the channel type so partials have a known type this.type = 'dm'; /** - * A collection containing the messages sent to this channel - * @type {MessageStore} + * A manager of the messages belonging to this channel + * @type {MessageManager} */ - this.messages = new MessageStore(this); + this.messages = new MessageManager(this); this._typing = new Map(); } diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 803fc500..5cbf913d 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -82,7 +82,7 @@ class Emoji extends Base { * @returns {string} * @example * // Send a custom emoji from a guild: - * const emoji = guild.emojis.first(); + * const emoji = guild.emojis.cache.first(); * msg.reply(`Hello! ${emoji}`); * @example * // Send the emoji used in a reaction to the channel the reaction is part of diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 04d091b0..d9bad7ce 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -11,12 +11,12 @@ const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); const Snowflake = require('../util/Snowflake'); const SystemChannelFlags = require('../util/SystemChannelFlags'); -const GuildMemberStore = require('../stores/GuildMemberStore'); -const RoleStore = require('../stores/RoleStore'); -const GuildEmojiStore = require('../stores/GuildEmojiStore'); -const GuildChannelStore = require('../stores/GuildChannelStore'); -const PresenceStore = require('../stores/PresenceStore'); -const VoiceStateStore = require('../stores/VoiceStateStore'); +const GuildMemberManager = require('../managers/GuildMemberManager'); +const RoleManager = require('../managers/RoleManager'); +const GuildEmojiManager = require('../managers/GuildEmojiManager'); +const GuildChannelManager = require('../managers/GuildChannelManager'); +const PresenceManager = require('../managers/PresenceManager'); +const VoiceStateManager = require('../managers/VoiceStateManager'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); @@ -35,34 +35,34 @@ class Guild extends Base { super(client); /** - * A collection of members that are in this guild. The key is the member's ID, the value is the member - * @type {GuildMemberStore} + * A manager of the members belonging to this guild + * @type {GuildMemberManager} */ - this.members = new GuildMemberStore(this); + this.members = new GuildMemberManager(this); /** - * A collection of channels that are in this guild. The key is the channel's ID, the value is the channel - * @type {GuildChannelStore} + * A manager of the members belonging to this guild + * @type {GuildChannelManager} */ - this.channels = new GuildChannelStore(this); + this.channels = new GuildChannelManager(this); /** - * A collection of roles that are in this guild. The key is the role's ID, the value is the role - * @type {RoleStore} + * A manager of the roles belonging to this guild + * @type {RoleManager} */ - this.roles = new RoleStore(this); + this.roles = new RoleManager(this); /** - * A collection of presences in this guild - * @type {PresenceStore} + * A manager of the presences belonging to this guild + * @type {PresenceManager} */ - this.presences = new PresenceStore(this.client); + this.presences = new PresenceManager(this.client); /** - * A collection of voice states in this guild - * @type {VoiceStateStore} + * A manager of the voice states of this guild + * @type {VoiceStateManager} */ - this.voiceStates = new VoiceStateStore(this); + this.voiceStates = new VoiceStateManager(this); /** * Whether the bot has been removed from the guild @@ -321,19 +321,19 @@ class Guild extends Base { this.features = data.features || this.features || []; if (data.channels) { - this.channels.clear(); + this.channels.cache.clear(); for (const rawChannel of data.channels) { this.client.channels.add(rawChannel, this); } } if (data.roles) { - this.roles.clear(); + this.roles.cache.clear(); for (const role of data.roles) this.roles.add(role); } if (data.members) { - this.members.clear(); + this.members.cache.clear(); for (const guildUser of data.members) this.members.add(guildUser); } @@ -352,7 +352,7 @@ class Guild extends Base { } if (data.voice_states) { - this.voiceStates.clear(); + this.voiceStates.cache.clear(); for (const voiceState of data.voice_states) { this.voiceStates.add(voiceState); } @@ -360,10 +360,10 @@ class Guild extends Base { if (!this.emojis) { /** - * A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji. - * @type {GuildEmojiStore} + * A manager of the emojis belonging to this guild + * @type {GuildEmojiManager} */ - this.emojis = new GuildEmojiStore(this); + this.emojis = new GuildEmojiManager(this); if (data.emojis) for (const emoji of data.emojis) this.emojis.add(emoji); } else if (data.emojis) { this.client.actions.GuildEmojisUpdate.handle({ @@ -463,7 +463,7 @@ class Guild extends Base { * @readonly */ get owner() { - return this.members.get(this.ownerID) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? + return this.members.cache.get(this.ownerID) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? this.members.add({ user: { id: this.ownerID } }, true) : null); } @@ -474,7 +474,7 @@ class Guild extends Base { * @readonly */ get afkChannel() { - return this.client.channels.get(this.afkChannelID) || null; + return this.client.channels.cache.get(this.afkChannelID) || null; } /** @@ -483,7 +483,7 @@ class Guild extends Base { * @readonly */ get systemChannel() { - return this.client.channels.get(this.systemChannelID) || null; + return this.client.channels.cache.get(this.systemChannelID) || null; } /** @@ -492,7 +492,7 @@ class Guild extends Base { * @readonly */ get widgetChannel() { - return this.client.channels.get(this.widgetChannelID) || null; + return this.client.channels.cache.get(this.widgetChannelID) || null; } /** @@ -501,7 +501,7 @@ class Guild extends Base { * @readonly */ get embedChannel() { - return this.client.channels.get(this.embedChannelID) || null; + return this.client.channels.cache.get(this.embedChannelID) || null; } /** @@ -510,9 +510,10 @@ class Guild extends Base { * @readonly */ get me() { - return this.members.get(this.client.user.id) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? - this.members.add({ user: { id: this.client.user.id } }, true) : - null); + return this.members.cache.get(this.client.user.id) || + (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? + this.members.add({ user: { id: this.client.user.id } }, true) : + null); } /** @@ -521,7 +522,7 @@ class Guild extends Base { * @readonly */ get voice() { - return this.voiceStates.get(this.client.user.id); + return this.voiceStates.cache.get(this.client.user.id); } /** @@ -716,7 +717,7 @@ class Guild extends Base { fetchEmbed() { return this.client.api.guilds(this.id).embed.get().then(data => ({ enabled: data.enabled, - channel: data.channel_id ? this.channels.get(data.channel_id) : null, + channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null, })); } @@ -763,7 +764,7 @@ class Guild extends Base { addMember(user, options) { 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)); + if (this.members.cache.has(user)) return Promise.resolve(this.members.cache.get(user)); options.access_token = options.accessToken; if (options.roles) { const roles = []; @@ -988,7 +989,7 @@ class Guild extends Base { * @returns {Promise} * @example * // Edit the guild owner - * guild.setOwner(guild.members.first()) + * guild.setOwner(guild.members.cache.first()) * .then(updated => console.log(`Updated the guild owner to ${updated.owner.displayName}`)) * .catch(console.error); */ @@ -1203,7 +1204,7 @@ class Guild extends Base { * @private */ _sortedRoles() { - return Util.discordSort(this.roles); + return Util.discordSort(this.roles.cache); } /** @@ -1214,7 +1215,7 @@ class Guild extends Base { */ _sortedChannels(channel) { const category = channel.type === ChannelTypes.CATEGORY; - return Util.discordSort(this.channels.filter(c => + return Util.discordSort(this.channels.cache.filter(c => c.type === channel.type && (category || c.parent === channel.parent) )); } diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index d1185bdd..a6327f35 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -285,7 +285,7 @@ class GuildAuditLogsEntry { */ this.executor = guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users.add({ id: data.user_id }) : - guild.client.users.get(data.user_id); + guild.client.users.cache.get(data.user_id); /** * An entry in the audit log representing a specific change. @@ -321,7 +321,7 @@ class GuildAuditLogsEntry { } else if (data.action_type === Actions.MESSAGE_DELETE) { this.extra = { count: data.options.count, - channel: guild.channels.get(data.options.channel_id), + channel: guild.channels.cache.get(data.options.channel_id), }; } else if (data.action_type === Actions.MESSAGE_BULK_DELETE) { this.extra = { @@ -330,11 +330,11 @@ class GuildAuditLogsEntry { } else { switch (data.options.type) { case 'member': - this.extra = guild.members.get(data.options.id); + this.extra = guild.members.cache.get(data.options.id); if (!this.extra) this.extra = { id: data.options.id }; break; case 'role': - this.extra = guild.roles.get(data.options.id); + this.extra = guild.roles.cache.get(data.options.id); if (!this.extra) this.extra = { id: data.options.id, name: data.options.role_name }; break; default: @@ -357,9 +357,9 @@ class GuildAuditLogsEntry { } else if (targetType === Targets.USER) { this.target = guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users.add({ id: data.target_id }) : - guild.client.users.get(data.target_id); + guild.client.users.cache.get(data.target_id); } else if (targetType === Targets.GUILD) { - this.target = guild.client.guilds.get(data.target_id); + this.target = guild.client.guilds.cache.get(data.target_id); } else if (targetType === Targets.WEBHOOK) { this.target = logs.webhooks.get(data.target_id) || new Webhook(guild.client, @@ -386,9 +386,9 @@ class GuildAuditLogsEntry { } }); } else if (targetType === Targets.MESSAGE) { - this.target = guild.client.users.get(data.target_id); + this.target = guild.client.users.cache.get(data.target_id); } else { - this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id) || { id: data.target_id }; + this.target = guild[`${targetType.toLowerCase()}s`].cache.get(data.target_id) || { id: data.target_id }; } } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index fa1f84d4..649e5b22 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -72,7 +72,7 @@ class GuildChannel extends Channel { * @readonly */ get parent() { - return this.guild.channels.get(this.parentID) || null; + return this.guild.channels.cache.get(this.parentID) || null; } /** @@ -118,7 +118,7 @@ class GuildChannel extends Channel { if (!verified) member = this.guild.members.resolve(member); if (!member) return []; - roles = roles || member.roles; + roles = roles || member.roles.cache; const roleOverwrites = []; let memberOverwrites; let everyoneOverwrites; @@ -149,7 +149,7 @@ class GuildChannel extends Channel { memberPermissions(member) { if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze(); - const roles = member.roles; + const roles = member.roles.cache; const permissions = new Permissions(roles.map(role => role.permissions)); if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze(); @@ -274,7 +274,7 @@ class GuildChannel extends Channel { */ get members() { const members = new Collection(); - for (const member of this.guild.members.values()) { + for (const member of this.guild.members.cache.values()) { if (this.permissionsFor(member).has('VIEW_CHANNEL', false)) { members.set(member.id, member); } diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index a7620911..64138b86 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,6 +1,6 @@ 'use strict'; -const GuildEmojiRoleStore = require('../stores/GuildEmojiRoleStore'); +const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager'); const Permissions = require('../util/Permissions'); const { Error } = require('../errors'); const Emoji = require('./Emoji'); @@ -79,12 +79,12 @@ class GuildEmoji extends Emoji { } /** - * A collection of roles this emoji is active for (empty if all), mapped by role ID - * @type {GuildEmojiRoleStore} + * A manager for roles this emoji is active for. + * @type {GuildEmojiRoleManager} * @readonly */ get roles() { - return new GuildEmojiRoleStore(this); + return new GuildEmojiRoleManager(this); } /** @@ -168,15 +168,15 @@ class GuildEmoji extends Emoji { other.name === this.name && other.managed === this.managed && other.requiresColons === this.requiresColons && - other.roles.size === this.roles.size && - other.roles.every(role => this.roles.has(role.id)) + other.roles.cache.size === this.roles.cache.size && + other.roles.cache.every(role => this.roles.cache.has(role.id)) ); } else { return ( other.id === this.id && other.name === this.name && - other.roles.length === this.roles.size && - other.roles.every(role => this.roles.has(role)) + other.roles.length === this.roles.cache.size && + other.roles.every(role => this.roles.cache.has(role)) ); } } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8a8b8bb6..b832f959 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -3,7 +3,7 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Role = require('./Role'); const Permissions = require('../util/Permissions'); -const GuildMemberRoleStore = require('../stores/GuildMemberRoleStore'); +const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager'); const Base = require('./Base'); const VoiceState = require('./VoiceState'); const { Presence } = require('./Presence'); @@ -101,12 +101,12 @@ class GuildMember extends Base { } /** - * A collection of roles that are applied to this member, mapped by the role ID - * @type {GuildMemberRoleStore} + * A manager for the roles belonging to this member + * @type {GuildMemberRoleManager} * @readonly */ get roles() { - return new GuildMemberRoleStore(this); + return new GuildMemberRoleManager(this); } /** @@ -115,8 +115,8 @@ class GuildMember extends Base { * @readonly */ get lastMessage() { - const channel = this.guild.channels.get(this.lastMessageChannelID); - return (channel && channel.messages.get(this.lastMessageID)) || null; + const channel = this.guild.channels.cache.get(this.lastMessageChannelID); + return (channel && channel.messages.cache.get(this.lastMessageID)) || null; } /** @@ -125,7 +125,7 @@ class GuildMember extends Base { * @readonly */ get voice() { - return this.guild.voiceStates.get(this.id) || new VoiceState(this.guild, { user_id: this.id }); + return this.guild.voiceStates.cache.get(this.id) || new VoiceState(this.guild, { user_id: this.id }); } /** @@ -152,7 +152,7 @@ class GuildMember extends Base { * @readonly */ get presence() { - return this.guild.presences.get(this.id) || new Presence(this.client, { + return this.guild.presences.cache.get(this.id) || new Presence(this.client, { user: { id: this.id, }, @@ -205,7 +205,7 @@ class GuildMember extends Base { */ get permissions() { if (this.user.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze(); - return new Permissions(this.roles.map(role => role.permissions)).freeze(); + return new Permissions(this.roles.cache.map(role => role.permissions)).freeze(); } /** @@ -261,7 +261,7 @@ class GuildMember extends Base { */ hasPermission(permission, { checkAdmin = true, checkOwner = true } = {}) { if (checkOwner && this.user.id === this.guild.ownerID) return true; - return this.roles.some(r => r.permissions.has(permission, checkAdmin)); + return this.roles.cache.some(r => r.permissions.has(permission, checkAdmin)); } /** diff --git a/src/structures/Integration.js b/src/structures/Integration.js index 5ff760dc..8fef7aa2 100644 --- a/src/structures/Integration.js +++ b/src/structures/Integration.js @@ -56,7 +56,7 @@ class Integration extends Base { * The role that this integration uses for subscribers * @type {Role} */ - this.role = this.guild.roles.get(data.role_id); + this.role = this.guild.roles.cache.get(data.role_id); /** * The user for this integration diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 4d048b3f..88b62b4a 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -117,7 +117,7 @@ class Invite extends Base { */ get deletable() { const guild = this.guild; - if (!guild || !this.client.guilds.has(guild.id)) return false; + if (!guild || !this.client.guilds.cache.has(guild.id)) return false; if (!guild.me) throw new Error('GUILD_UNCACHED_ME'); return this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) || guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD); diff --git a/src/structures/Message.js b/src/structures/Message.js index b09f1d34..adc4ec83 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -7,7 +7,7 @@ const ReactionCollector = require('./ReactionCollector'); const ClientApplication = require('./ClientApplication'); const Util = require('../util/Util'); const Collection = require('../util/Collection'); -const ReactionStore = require('../stores/ReactionStore'); +const ReactionManager = require('../managers/ReactionManager'); const { MessageTypes } = require('../util/Constants'); const Permissions = require('../util/Permissions'); const Base = require('./Base'); @@ -126,10 +126,10 @@ class Message extends Base { this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; /** - * A collection of reactions to this message, mapped by the reaction ID - * @type {ReactionStore} + * A manager of the reactions belonging to this message + * @type {ReactionManager} */ - this.reactions = new ReactionStore(this); + this.reactions = new ReactionManager(this); if (data.reactions && data.reactions.length > 0) { for (const reaction of data.reactions) { this.reactions.add(reaction); @@ -452,7 +452,7 @@ class Message extends Base { * .catch(console.error); * @example * // React to a message with a custom emoji - * message.react(message.guild.emojis.get('123456789012345678')) + * message.react(message.guild.emojis.cache.get('123456789012345678')) * .then(console.log) * .catch(console.error); */ @@ -484,7 +484,7 @@ class Message extends Base { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.channel.messages.remove(this.id, reason).then(() => this); + return this.channel.messages.delete(this.id, reason).then(() => this); } else { return new Promise(resolve => { this.client.setTimeout(() => { diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index ab2d5c81..6079dc23 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -71,7 +71,7 @@ class MessageMentions { } else { this.roles = new Collection(); for (const mention of roles) { - const role = message.channel.guild.roles.get(mention); + const role = message.channel.guild.roles.cache.get(mention); if (role) this.roles.set(role.id, role); } } @@ -156,7 +156,7 @@ class MessageMentions { this._channels = new Collection(); let matches; while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) { - const chan = this.client.channels.get(matches[1]); + const chan = this.client.channels.cache.get(matches[1]); if (chan) this._channels.set(chan.id, chan); } return this._channels; @@ -175,7 +175,7 @@ class MessageMentions { 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; + for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true; } if (!ignoreDirect) { diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 5cb9210f..150288a5 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -3,7 +3,7 @@ const GuildEmoji = require('./GuildEmoji'); const Util = require('../util/Util'); const ReactionEmoji = require('./ReactionEmoji'); -const ReactionUserStore = require('../stores/ReactionUserStore'); +const ReactionUserManager = require('../managers/ReactionUserManager'); /** * Represents a reaction to a message. @@ -35,10 +35,10 @@ class MessageReaction { this.me = data.me; /** - * The users that have given this reaction, mapped by their ID - * @type {ReactionUserStore} + * A manager of the users that have given this reaction + * @type {ReactionUserManager} */ - this.users = new ReactionUserStore(client, undefined, this); + this.users = new ReactionUserManager(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji); @@ -75,7 +75,7 @@ class MessageReaction { if (this._emoji instanceof GuildEmoji) return this._emoji; // Check to see if the emoji has become known to the client if (this._emoji.id) { - const emojis = this.message.client.emojis; + const emojis = this.message.client.emojis.cache; if (emojis.has(this._emoji.id)) { const emoji = emojis.get(this._emoji.id); this._emoji = emoji; @@ -108,18 +108,18 @@ class MessageReaction { _add(user) { if (this.partial) return; - this.users.set(user.id, user); + this.users.cache.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me) this.me = user.id === this.message.client.user.id; } _remove(user) { if (this.partial) return; - this.users.delete(user.id); + this.users.cache.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; - if (this.count <= 0 && this.users.size === 0) { - this.message.reactions.remove(this.emoji.id || this.emoji.name); + if (this.count <= 0 && this.users.cache.size === 0) { + this.message.reactions.cache.delete(this.emoji.id || this.emoji.name); } } } diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 12888f1c..fdc038a5 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -66,7 +66,7 @@ class Presence { * @readonly */ get user() { - return this.client.users.get(this.userID) || null; + return this.client.users.cache.get(this.userID) || null; } /** @@ -75,7 +75,7 @@ class Presence { * @readonly */ get member() { - return this.guild.members.get(this.userID) || null; + return this.guild.members.cache.get(this.userID) || null; } patch(data) { diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index e7d5aeb1..65b604cd 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -74,7 +74,7 @@ class ReactionCollector extends Collector { this.on('remove', (reaction, user) => { this.total--; - if (!this.collected.some(r => r.users.has(user.id))) this.users.delete(user.id); + if (!this.collected.some(r => r.users.cache.has(user.id))) this.users.delete(user.id); }); } diff --git a/src/structures/Role.js b/src/structures/Role.js index 9c7ebd49..cedaa545 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -117,7 +117,7 @@ class Role extends Base { * @readonly */ get members() { - return this.guild.members.filter(m => m.roles.has(this.id)); + return this.guild.members.cache.filter(m => m.roles.cache.has(this.id)); } /** diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 66212e70..10b5d7cb 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -5,7 +5,7 @@ const Webhook = require('./Webhook'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Collection = require('../util/Collection'); const DataResolver = require('../util/DataResolver'); -const MessageStore = require('../stores/MessageStore'); +const MessageManager = require('../managers/MessageManager'); /** * Represents a guild text channel on Discord. @@ -20,10 +20,10 @@ class TextChannel extends GuildChannel { constructor(guild, data) { super(guild, data); /** - * A collection containing the messages sent to this channel - * @type {MessageStore} + * A manager of the messages sent to this channel + * @type {MessageManager} */ - this.messages = new MessageStore(this); + this.messages = new MessageManager(this); this._typing = new Map(); } diff --git a/src/structures/User.js b/src/structures/User.js index 7ae67451..ae301705 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -119,8 +119,8 @@ class User extends Base { * @readonly */ get lastMessage() { - const channel = this.client.channels.get(this.lastMessageChannelID); - return (channel && channel.messages.get(this.lastMessageID)) || null; + const channel = this.client.channels.cache.get(this.lastMessageChannelID); + return (channel && channel.messages.cache.get(this.lastMessageID)) || null; } /** @@ -129,8 +129,8 @@ class User extends Base { * @readonly */ get presence() { - for (const guild of this.client.guilds.values()) { - if (guild.presences.has(this.id)) return guild.presences.get(this.id); + for (const guild of this.client.guilds.cache.values()) { + if (guild.presences.cache.has(this.id)) return guild.presences.cache.get(this.id); } return new Presence(this.client, { user: { id: this.id } }); } @@ -209,7 +209,7 @@ class User extends Base { * @readonly */ get dmChannel() { - return this.client.channels.find(c => c.type === 'dm' && c.recipient.id === this.id) || null; + return this.client.channels.cache.find(c => c.type === 'dm' && c.recipient.id === this.id) || null; } /** diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 15f2e496..6b3850df 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -34,7 +34,7 @@ class VoiceChannel extends GuildChannel { */ get members() { const coll = new Collection(); - for (const state of this.guild.voiceStates.values()) { + for (const state of this.guild.voiceStates.cache.values()) { if (state.channelID === this.id && state.member) { coll.set(state.id, state.member); } diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index c60f6ac6..3ad02dea 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -72,7 +72,7 @@ class VoiceState extends Base { * @readonly */ get member() { - return this.guild.members.get(this.id) || null; + return this.guild.members.cache.get(this.id) || null; } /** @@ -81,7 +81,7 @@ class VoiceState extends Base { * @readonly */ get channel() { - return this.guild.channels.get(this.channelID) || null; + return this.guild.channels.cache.get(this.channelID) || null; } /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 9b6b43c8..b2960704 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -70,7 +70,7 @@ class Webhook { * The owner of the webhook * @type {?User|Object} */ - this.owner = this.client.users ? this.client.users.get(data.user.id) : data.user; + this.owner = this.client.users ? this.client.users.cache.get(data.user.id) : data.user; } else { this.owner = null; } @@ -154,7 +154,7 @@ class Webhook { query: { wait: true }, auth: false, }).then(d => { - const channel = this.client.channels ? this.client.channels.get(d.channel_id) : undefined; + const channel = this.client.channels ? this.client.channels.cache.get(d.channel_id) : undefined; if (!channel) return d; return channel.messages.add(d, false); }); diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index a6b18375..4106e00d 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -13,10 +13,10 @@ const APIMessage = require('../APIMessage'); class TextBasedChannel { constructor() { /** - * A collection containing the messages sent to this channel - * @type {MessageStore} + * A manager of the messages sent to this channel + * @type {MessageManager} */ - this.messages = new MessageStore(this); + this.messages = new MessageManager(this); /** * The ID of the last message in the channel, if one was sent @@ -37,7 +37,7 @@ class TextBasedChannel { * @readonly */ get lastMessage() { - return this.messages.get(this.lastMessageID) || null; + return this.messages.cache.get(this.lastMessageID) || null; } /** @@ -350,4 +350,4 @@ class TextBasedChannel { module.exports = TextBasedChannel; // Fixes Circular -const MessageStore = require('../../stores/MessageStore'); +const MessageManager = require('../../managers/MessageManager'); diff --git a/src/util/LimitedCollection.js b/src/util/LimitedCollection.js new file mode 100644 index 00000000..5719a9fa --- /dev/null +++ b/src/util/LimitedCollection.js @@ -0,0 +1,29 @@ +'use strict'; + +const Collection = require('./Collection.js'); + +/** + * A Collection which holds a max amount of entries. The first key is deleted if the Collection has + * reached max size. + * @extends {Collection} + * @param {number} [maxSize=0] The maximum size of the Collection + * @param {Iterable} [iterable=null] Optional entries passed to the Map constructor. + */ +class LimitedCollection extends Collection { + constructor(maxSize = 0, iterable = null) { + super(iterable); + /** + * The max size of the Collection. + * @type {number} + */ + this.maxSize = maxSize; + } + + set(key, value) { + if (this.maxSize === 0) return this; + if (this.size >= this.maxSize && !this.has(key)) this.delete(this.firstKey()); + return super.set(key, value); + } +} + +module.exports = LimitedCollection; diff --git a/src/util/Structures.js b/src/util/Structures.js index 02529d26..c4061f62 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -1,7 +1,7 @@ 'use strict'; /** - * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}. + * Allows for the extension of built-in Discord.js structures that are instantiated by {@link BaseManager Managers}. */ class Structures { constructor() { diff --git a/src/util/Util.js b/src/util/Util.js index 6087f3c2..da4bff44 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -528,25 +528,25 @@ class Util { .replace(/<@!?[0-9]+>/g, input => { const id = input.replace(/<|!|>|@/g, ''); if (message.channel.type === 'dm') { - const user = message.client.users.get(id); + const user = message.client.users.cache.get(id); return user ? `@${user.username}` : input; } - const member = message.channel.guild.members.get(id); + const member = message.channel.guild.members.cache.get(id); if (member) { return `@${member.displayName}`; } else { - const user = message.client.users.get(id); + const user = message.client.users.cache.get(id); return user ? `@${user.username}` : input; } }) .replace(/<#[0-9]+>/g, input => { - const channel = message.client.channels.get(input.replace(/<|#|>/g, '')); + const channel = message.client.channels.cache.get(input.replace(/<|#|>/g, '')); return channel ? `#${channel.name}` : input; }) .replace(/<@&[0-9]+>/g, input => { if (message.channel.type === 'dm') return input; - const role = message.guild.roles.get(input.replace(/<|@|>|&/g, '')); + const role = message.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; }); } @@ -571,34 +571,6 @@ class Util { setTimeout(resolve, ms); }); } - - /** - * Adds methods from collections and maps onto the provided store - * @param {DataStore} store The store to mixin - * @param {string[]} ignored The properties to ignore - * @private - */ - /* eslint-disable func-names */ - static mixin(store, ignored) { - const Collection = require('./Collection'); - Object.getOwnPropertyNames(Collection.prototype) - .concat(Object.getOwnPropertyNames(Map.prototype)).forEach(prop => { - if (ignored.includes(prop)) return; - if (prop === 'size') { - Object.defineProperty(store.prototype, prop, { - get: function() { - return this._filtered[prop]; - }, - }); - return; - } - const func = Collection.prototype[prop]; - if (prop === 'constructor' || typeof func !== 'function') return; - store.prototype[prop] = function(...args) { - return func.apply(this._filtered, args); - }; - }); - } } module.exports = Util; diff --git a/typings/index.d.ts b/typings/index.d.ts index 07f725bc..a0f40ce9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -151,16 +151,16 @@ declare module 'discord.js' { private _eval(script: string): any; private _validateOptions(options?: ClientOptions): void; - public channels: ChannelStore; - public readonly emojis: GuildEmojiStore; - public guilds: GuildStore; + public channels: ChannelManager; + public readonly emojis: GuildEmojiManager; + public guilds: GuildManager; public readyAt: Date | null; public readonly readyTimestamp: number | null; public shard: ShardClientUtil | null; public token: string | null; public readonly uptime: number | null; public user: ClientUser | null; - public users: UserStore; + public users: UserManager; public voice: ClientVoiceManager | null; public ws: WebSocketManager; public destroy(): void; @@ -328,16 +328,6 @@ declare module 'discord.js' { public setUsername(username: string): Promise; } - export class Collection extends BaseCollection { - public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; - public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; - public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; - public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; - public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; - public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; - public toJSON(): object; - } - export abstract class Collector extends EventEmitter { constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); private _timeout: NodeJS.Timer | null; @@ -648,7 +638,7 @@ declare module 'discord.js' { export class DMChannel extends TextBasedChannel(Channel) { constructor(client: Client, data?: object); - public messages: MessageStore; + public messages: MessageManager; public recipient: User; public readonly partial: false; public fetch(): Promise; @@ -680,7 +670,7 @@ declare module 'discord.js' { public applicationID: Snowflake; public available: boolean; public banner: string | null; - public channels: GuildChannelStore; + public channels: GuildChannelManager; public readonly createdAt: Date; public readonly createdTimestamp: number; public defaultMessageNotifications: DefaultMessageNotifications | number; @@ -689,7 +679,7 @@ declare module 'discord.js' { public embedChannel: GuildChannel | null; public embedChannelID: Snowflake | null; public embedEnabled: boolean; - public emojis: GuildEmojiStore; + public emojis: GuildEmojiManager; public explicitContentFilter: number; public features: GuildFeatures[]; public icon: string | null; @@ -701,7 +691,7 @@ declare module 'discord.js' { public maximumPresences: number | null; public readonly me: GuildMember | null; public memberCount: number; - public members: GuildMemberStore; + public members: GuildMemberManager; public mfaLevel: number; public name: string; public readonly nameAcronym: string; @@ -710,9 +700,9 @@ declare module 'discord.js' { public readonly partnered: boolean; public premiumSubscriptionCount: number | null; public premiumTier: PremiumTier; - public presences: PresenceStore; + public presences: PresenceManager; public region: string; - public roles: RoleStore; + public roles: RoleManager; public readonly shard: WebSocketShard; public shardID: number; public splash: string | null; @@ -723,7 +713,7 @@ declare module 'discord.js' { public verificationLevel: number; public readonly verified: boolean; public readonly voice: VoiceState | null; - public readonly voiceStates: VoiceStateStore; + public readonly voiceStates: VoiceStateManager; public readonly widgetChannel: TextChannel | null; public widgetChannelID: Snowflake | null; public widgetEnabled: boolean | null; @@ -847,7 +837,7 @@ declare module 'discord.js' { public id: Snowflake; public managed: boolean; public requiresColons: boolean; - public roles: GuildEmojiRoleStore; + public roles: GuildEmojiRoleManager; public readonly url: string; public delete(reason?: string): Promise; public edit(data: GuildEmojiEditData, reason?: string): Promise; @@ -875,7 +865,7 @@ declare module 'discord.js' { public readonly premiumSince: Date | null; public premiumSinceTimestamp: number | null; public readonly presence: Presence; - public roles: GuildMemberRoleStore; + public roles: GuildMemberRoleManager; public user: User; public readonly voice: VoiceState; public ban(options?: BanOptions): Promise; @@ -978,7 +968,7 @@ declare module 'discord.js' { public readonly partial: false; public readonly pinnable: boolean; public pinned: boolean; - public reactions: ReactionStore; + public reactions: ReactionManager; public system: boolean; public tts: boolean; public type: MessageType; @@ -1113,7 +1103,7 @@ declare module 'discord.js' { public me: boolean; public message: Message; public readonly partial: boolean; - public users: ReactionUserStore; + public users: ReactionUserManager; public remove(): Promise; public fetch(): Promise; public toJSON(): object; @@ -1397,7 +1387,7 @@ declare module 'discord.js' { export class TextChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); - public messages: MessageStore; + public messages: MessageManager; public nsfw: boolean; public rateLimitPerUser: number; public topic: string | null; @@ -1409,7 +1399,7 @@ declare module 'discord.js' { export class NewsChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); - public messages: MessageStore; + public messages: MessageManager; public nsfw: boolean; public topic: string | null; public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; @@ -1749,64 +1739,64 @@ declare module 'discord.js' { //#endregion -//#region Stores +//#region Collections - export class ChannelStore extends DataStore { - constructor(client: Client, iterable: Iterable, options?: { lru: boolean }); - constructor(client: Client, options?: { lru: boolean }); - public fetch(id: Snowflake, cache?: boolean): Promise; - } - - export class DataStore, R = any> extends Collection { - constructor(client: Client, iterable: Iterable, holds: VConstructor); - public client: Client; - public holds: VConstructor; - public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; - public remove(key: K): void; - public resolve(resolvable: R): V | null; - public resolveID(resolvable: R): K | null; - // Don't worry about those bunch of ts-ignores here, this is intended https://github.com/microsoft/TypeScript/issues/1213 - // @ts-ignore - public filter(fn: (value: V, key: K, collection: this) => boolean): Collection; - // @ts-ignore - public filter(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): Collection; - // @ts-ignore - public filter(fn: (value: V, key: K, collection: this) => boolean, thisArg?: unknown): Collection; - // @ts-ignore - public partition(fn: (value: V, key: K, collection: this) => boolean): [Collection, Collection]; - // @ts-ignore - public partition(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): [Collection, Collection]; - // @ts-ignore - public partition(fn: (value: V, key: K, collection: this) => boolean, thisArg?: unknown): [Collection, Collection]; + export class Collection extends BaseCollection { public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; - // @ts-ignore - public clone(): Collection; - // @ts-ignore - public concat(...collections: Collection[]): Collection; - // @ts-ignore - public sorted(compareFunction: (firstValue: V, secondValue: V, firstKey: K, secondKey: K) => number): Collection; + public toJSON(): object; } - export class GuildEmojiRoleStore extends OverridableDataStore { + export class LimitedCollection extends Collection { + public constructor(maxSize: number, iterable: Iterable); + public maxSize: number; + } + +//#endregion + +//#region Managers + + export class ChannelManager extends BaseManager { + constructor(client: Client, iterable: Iterable); + public fetch(id: Snowflake, cache?: boolean): Promise; + } + + export abstract class BaseManager { + constructor(client: Client, iterable: Iterable, holds: Constructable, cacheType: Collection); + public holds: Constructable; + public cache: Collection; + public cacheType: Collection; + public readonly client: Client; + public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): Holds; + public remove(key: K): void; + public resolve(resolvable: R): Holds | null; + public resolveID(resolvable: R): K | null; + } + + export class GuildEmojiRoleManager { constructor(emoji: GuildEmoji); + public emoji: GuildEmoji; + public guild: Guild; + public cache: Collection; public add(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection): Promise; public set(roles: RoleResolvable[] | Collection): Promise; public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection): Promise; } - export class GuildEmojiStore extends DataStore { + export class GuildEmojiManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; } - export class GuildChannelStore extends DataStore { + export class GuildChannelManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; public create(name: string, options: GuildCreateChannelOptions & { type: 'voice' }): Promise; public create(name: string, options: GuildCreateChannelOptions & { type: 'category' }): Promise; public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text' }): Promise; @@ -1814,78 +1804,87 @@ declare module 'discord.js' { } // Hacky workaround because changing the signature of an overridden method errors - class OverridableDataStore, R = any> extends DataStore { + class OverridableManager extends BaseManager { public add(data: any, cache: any): any; public set(key: any): any; } - export class GuildMemberRoleStore extends OverridableDataStore { + export class GuildMemberRoleManager extends OverridableManager { constructor(member: GuildMember); public readonly hoist: Role | null; public readonly color: Role | null; public readonly highest: Role; + public member: GuildMember; + public guild: Guild; public add(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; public set(roles: RoleResolvable[] | Collection, reason?: string): Promise; public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; } - export class GuildMemberStore extends DataStore { + export class GuildMemberManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; public ban(user: UserResolvable, options?: BanOptions): Promise; public fetch(options: UserResolvable | FetchMemberOptions): Promise; - public fetch(): Promise; + public fetch(): Promise; public fetch(options: FetchMembersOptions): Promise>; public prune(options: GuildPruneMembersOptions & { dry?: false, count: false }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; public unban(user: UserResolvable, reason?: string): Promise; } - export class GuildStore extends DataStore { + export class GuildManager extends BaseManager { constructor(client: Client, iterable?: Iterable); public create(name: string, options?: { region?: string, icon: BufferResolvable | Base64Resolvable | null }): Promise; } - export class MessageStore extends DataStore { + export class MessageManager extends BaseManager { constructor(channel: TextChannel | DMChannel, iterable?: Iterable); + public channel: TextBasedChannelFields; + public cache: LimitedCollection; public fetch(message: Snowflake, cache?: boolean): Promise; public fetch(options?: ChannelLogsQueryOptions, cache?: boolean): Promise>; public fetchPinned(cache?: boolean): Promise>; - public remove(message: MessageResolvable, reason?: string): Promise; + public delete(message: MessageResolvable, reason?: string): Promise; } - export class PresenceStore extends DataStore { + export class PresenceManager extends BaseManager { constructor(client: Client, iterable?: Iterable); } - export class ReactionStore extends DataStore { + export class ReactionManager extends BaseManager { constructor(message: Message, iterable?: Iterable); + public message: Message; public removeAll(): Promise; } - export class ReactionUserStore extends DataStore { + export class ReactionUserManager extends BaseManager { constructor(client: Client, iterable: Iterable | undefined, reaction: MessageReaction); + public reaction: MessageReaction; public fetch(options?: { limit?: number, after?: Snowflake, before?: Snowflake }): Promise>; public remove(user?: UserResolvable): Promise; } - export class RoleStore extends DataStore { + export class RoleManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); public readonly everyone: Role | null; public readonly highest: Role; + public guild: Guild; public create(options?: { data?: RoleData, reason?: string }): Promise; public fetch(id: Snowflake, cache?: boolean): Promise; public fetch(id?: Snowflake, cache?: boolean): Promise; } - export class UserStore extends DataStore { + export class UserManager extends BaseManager { constructor(client: Client, iterable?: Iterable); public fetch(id: Snowflake, cache?: boolean): Promise; } - export class VoiceStateStore extends DataStore { + export class VoiceStateManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; } //#endregion From 324d9e0a3af5d89dba35f6bc859f084b63adf104 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 17:24:35 +0000 Subject: [PATCH 367/428] fix(TextChannel): remove old nsfw regex check (#3775) --- src/structures/TextChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 10b5d7cb..e77947c2 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -41,7 +41,7 @@ class TextChannel extends GuildChannel { * @type {boolean} * @readonly */ - this.nsfw = data.nsfw || /^nsfw(-|$)/.test(this.name); + this.nsfw = data.nsfw; /** * The ID of the last message sent in this channel, if one was sent From 92bc63452019f22161df68b1684b4b2b7e120a70 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 12 Feb 2020 18:28:26 +0100 Subject: [PATCH 368/428] docs(*Manager): fix child classes' cache type annotations (#3777) --- src/managers/ChannelManager.js | 5 ++--- src/managers/GuildChannelManager.js | 5 ++--- src/managers/GuildEmojiManager.js | 5 ++--- src/managers/GuildManager.js | 5 ++--- src/managers/GuildMemberManager.js | 5 ++--- src/managers/MessageManager.js | 5 ++--- src/managers/PresenceManager.js | 5 ++--- src/managers/ReactionManager.js | 5 ++--- src/managers/ReactionUserManager.js | 5 ++--- src/managers/RoleManager.js | 5 ++--- src/managers/UserManager.js | 5 ++--- src/managers/VoiceStateManager.js | 5 ++--- 12 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/managers/ChannelManager.js b/src/managers/ChannelManager.js index dcb098c1..b47667e5 100644 --- a/src/managers/ChannelManager.js +++ b/src/managers/ChannelManager.js @@ -14,9 +14,8 @@ class ChannelManager extends BaseManager { /** * The cache of Channels - * @property {Collection} cache - * @memberof ChannelManager - * @instance + * @type {Collection} + * @name ChannelManager#cache */ add(data, guild, cache = true) { diff --git a/src/managers/GuildChannelManager.js b/src/managers/GuildChannelManager.js index 147a0b20..6f89501c 100644 --- a/src/managers/GuildChannelManager.js +++ b/src/managers/GuildChannelManager.js @@ -22,9 +22,8 @@ class GuildChannelManager extends BaseManager { /** * The cache of this Manager - * @property {Collection} cache - * @memberof GuildChannelManager - * @instance + * @type {Collection} + * @name GuildChannelManager#cache */ add(channel) { diff --git a/src/managers/GuildEmojiManager.js b/src/managers/GuildEmojiManager.js index 27ac363c..85f030c3 100644 --- a/src/managers/GuildEmojiManager.js +++ b/src/managers/GuildEmojiManager.js @@ -23,9 +23,8 @@ class GuildEmojiManager extends BaseManager { /** * The cache of GuildEmojis - * @property {Collection} cache - * @memberof GuildEmojiManager - * @instance + * @type {Collection} + * @name GuildEmojiManager#cache */ add(data, cache) { diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js index 5666156b..c2747580 100644 --- a/src/managers/GuildManager.js +++ b/src/managers/GuildManager.js @@ -19,9 +19,8 @@ class GuildManager extends BaseManager { /** * The cache of this Manager - * @property {Collection} cache - * @memberof GuildManager - * @instance + * @type {Collection} + * @name GuildManager#cache */ /** diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index b7d49f5c..7fae74c8 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -22,9 +22,8 @@ class GuildMemberManager extends BaseManager { /** * The cache of this Manager - * @property {Collection} cache - * @memberof GuildMemberManager - * @instance + * @type {Collection} + * @name GuildMemberManager#cache */ add(data, cache = true) { diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js index 19eb82b8..fa2b0843 100644 --- a/src/managers/MessageManager.js +++ b/src/managers/MessageManager.js @@ -21,9 +21,8 @@ class MessageManager extends BaseManager { /** * The cache of Messages - * @property {LimitedCollection} cache - * @memberof MessageManager - * @instance + * @type {LimitedCollection} + * @name MessageManager#cache */ add(data, cache) { diff --git a/src/managers/PresenceManager.js b/src/managers/PresenceManager.js index 0a38fd87..7627b0ec 100644 --- a/src/managers/PresenceManager.js +++ b/src/managers/PresenceManager.js @@ -14,9 +14,8 @@ class PresenceManager extends BaseManager { /** * The cache of Presences - * @property {Collection} cache - * @memberof PresenceManager - * @instance + * @type {Collection} + * @name PresenceManager#cache */ add(data, cache) { diff --git a/src/managers/ReactionManager.js b/src/managers/ReactionManager.js index 7350aebd..a9f84709 100644 --- a/src/managers/ReactionManager.js +++ b/src/managers/ReactionManager.js @@ -24,9 +24,8 @@ class ReactionManager extends BaseManager { /** * The reaction cache of this manager - * @property {Collection} cache - * @memberof ReactionManager - * @instance + * @type {Collection} + * @name ReactionManager#cache */ /** diff --git a/src/managers/ReactionUserManager.js b/src/managers/ReactionUserManager.js index c82b119d..feac5d63 100644 --- a/src/managers/ReactionUserManager.js +++ b/src/managers/ReactionUserManager.js @@ -20,9 +20,8 @@ class ReactionUserManager extends BaseManager { /** * The cache of this manager - * @property {Collection} cache - * @memberof GuildManager - * @instance + * @type {Collection} + * @name ReactionUserManager#cache */ /** diff --git a/src/managers/RoleManager.js b/src/managers/RoleManager.js index 9eb03cf2..80f92ad1 100644 --- a/src/managers/RoleManager.js +++ b/src/managers/RoleManager.js @@ -21,9 +21,8 @@ class RoleManager extends BaseManager { /** * The role cache of this manager - * @property {Collection} cache - * @memberof RoleManager - * @instance + * @type {Collection} + * @name RoleManager#cache */ add(data, cache) { diff --git a/src/managers/UserManager.js b/src/managers/UserManager.js index 8818206a..09652007 100644 --- a/src/managers/UserManager.js +++ b/src/managers/UserManager.js @@ -16,9 +16,8 @@ class UserManager extends BaseManager { /** * The cache of this manager - * @property {Collection} cache - * @memberof UserManager - * @instance + * @type {Collection} + * @name UserManager#cache */ /** diff --git a/src/managers/VoiceStateManager.js b/src/managers/VoiceStateManager.js index 755392b2..4a26b304 100644 --- a/src/managers/VoiceStateManager.js +++ b/src/managers/VoiceStateManager.js @@ -19,9 +19,8 @@ class VoiceStateManager extends BaseManager { /** * The cache of this manager - * @property {Collection} cache - * @memberof VoiceStateManager - * @instance + * @type {Collection} + * @name VoiceStateManager#cache */ add(data, cache = true) { From 94bb2686396ce97b50015b0fa34a6c27528afdbd Mon Sep 17 00:00:00 2001 From: Androz Date: Wed, 12 Feb 2020 18:36:08 +0100 Subject: [PATCH 369/428] docs: add extends to ChannelManager, cache is not nullable, resolveID accepts an object (#3771) * Add extends to docs * Cache shouldn't be nullable * jsdoc: use same type for both resolve methods --- src/managers/BaseManager.js | 4 ++-- src/managers/ChannelManager.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/managers/BaseManager.js b/src/managers/BaseManager.js index b9d0be93..ae07ee62 100644 --- a/src/managers/BaseManager.js +++ b/src/managers/BaseManager.js @@ -35,7 +35,7 @@ class BaseManager { /** * Holds the cache for the data model - * @type {?Collection} + * @type {Collection} */ this.cache = new cacheType(...cacheOptions); if (iterable) for (const i of iterable) this.add(i); @@ -64,7 +64,7 @@ class BaseManager { /** * Resolves a data entry to a instance ID. - * @param {string|Instance} idOrInstance The id or instance of something in this Manager + * @param {string|Object} idOrInstance The id or instance of something in this Manager * @returns {?Snowflake} */ resolveID(idOrInstance) { diff --git a/src/managers/ChannelManager.js b/src/managers/ChannelManager.js index b47667e5..e8533044 100644 --- a/src/managers/ChannelManager.js +++ b/src/managers/ChannelManager.js @@ -6,6 +6,7 @@ const { Events } = require('../util/Constants'); /** * A manager of channels belonging to a client + * @extends {BaseManager} */ class ChannelManager extends BaseManager { constructor(client, iterable) { From 149f72b50be3b8ffb134d20911b513cfea1aaaf5 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 17:40:20 +0000 Subject: [PATCH 370/428] typings(GuildMemberManager): fetch with no parameters returns collection (#3773) --- typings/index.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index a0f40ce9..09edcef5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1827,8 +1827,7 @@ declare module 'discord.js' { public guild: Guild; public ban(user: UserResolvable, options?: BanOptions): Promise; public fetch(options: UserResolvable | FetchMemberOptions): Promise; - public fetch(): Promise; - public fetch(options: FetchMembersOptions): Promise>; + public fetch(options?: FetchMembersOptions): Promise>; public prune(options: GuildPruneMembersOptions & { dry?: false, count: false }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; public unban(user: UserResolvable, reason?: string): Promise; From 3f8ea38b3a4217e02e997058ec1562ed41fedcc7 Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Wed, 12 Feb 2020 16:23:06 -0500 Subject: [PATCH 371/428] docs: info tag for ActivityType regarding CUSTOM_STATUS (#3757) --- src/util/Constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index 0417d881..b613021b 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -423,6 +423,7 @@ exports.MessageTypes = [ ]; /** + * Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users * The type of an activity of a users presence, e.g. `PLAYING`. Here are the available types: * * PLAYING * * STREAMING From 592021df924344e44cdd38d39a6574c09fdbf7dd Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 13 Feb 2020 08:25:14 +1100 Subject: [PATCH 372/428] feat(Message): throw a TypeError if delete is passed a non-object (#3772) --- src/structures/Message.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index adc4ec83..a7b55560 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -482,7 +482,9 @@ class Message extends Base { * .then(msg => console.log(`Deleted message from ${msg.author.username}`)) * .catch(console.error); */ - delete({ timeout = 0, reason } = {}) { + delete(options = {}) { + if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); + const { timeout = 0, reason } = options; if (timeout <= 0) { return this.channel.messages.delete(this.id, reason).then(() => this); } else { From 62b227c2bd194c5a6ed90bf5ca5ff45ecf59bdde Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 13 Feb 2020 08:26:17 +1100 Subject: [PATCH 373/428] fix(BaseManager): BaseManager#valueOf should return cache (#3776) * BaseManager#valueOf should return cache * Update Util#flatten to handle valueOf being a Collection * Update Util.js - typo Co-Authored-By: Amish Shah Co-authored-by: Amish Shah --- src/managers/BaseManager.js | 4 ++++ src/util/Util.js | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/managers/BaseManager.js b/src/managers/BaseManager.js index ae07ee62..ea371779 100644 --- a/src/managers/BaseManager.js +++ b/src/managers/BaseManager.js @@ -72,6 +72,10 @@ class BaseManager { if (typeof idOrInstance === 'string') return idOrInstance; return null; } + + valueOf() { + return this.cache; + } } module.exports = BaseManager; diff --git a/src/util/Util.js b/src/util/Util.js index da4bff44..4ab51877 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -36,8 +36,10 @@ class Util { const elemIsObj = isObject(element); const valueOf = elemIsObj && typeof element.valueOf === 'function' ? element.valueOf() : null; - // If it's a collection, make the array of keys + // If it's a Collection, make the array of keys if (element instanceof require('./Collection')) out[newProp] = Array.from(element.keys()); + // If the valueOf is a Collection, use its array of keys + else if (valueOf instanceof require('./Collection')) out[newProp] = Array.from(valueOf.keys()); // If it's an array, flatten each element else if (Array.isArray(element)) out[newProp] = element.map(e => Util.flatten(e)); // If it's an object with a primitive `valueOf`, use that value From d43692b0f2556f4aca6d9c8b9fea4d968d56ee71 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 21:29:16 +0000 Subject: [PATCH 374/428] docs(Guild): channels is a manager of channels (#3779) --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index d9bad7ce..8c180579 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -41,7 +41,7 @@ class Guild extends Base { this.members = new GuildMemberManager(this); /** - * A manager of the members belonging to this guild + * A manager of the channels belonging to this guild * @type {GuildChannelManager} */ this.channels = new GuildChannelManager(this); From 562b5bfca78a9c50d05a5ec658cffda04ff88b5c Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 21:42:43 +0000 Subject: [PATCH 375/428] refactor(VoiceChannel): use Permissions.FLAGS in speakable (#3780) --- src/structures/VoiceChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 6b3850df..e8362878 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -88,7 +88,7 @@ class VoiceChannel extends GuildChannel { * @readonly */ get speakable() { - return this.permissionsFor(this.client.user).has('SPEAK', false); + return this.permissionsFor(this.client.user).has(Permissions.FLAGS.SPEAK, false); } /** From 878cc050d4311a0d81f602ab37beca265cf4be03 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 21:47:24 +0000 Subject: [PATCH 376/428] fix(Guild): use snake case when editing system_channel_flags (#3781) --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 8c180579..ca462381 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -839,7 +839,7 @@ class Guild extends Base { Number(data.defaultMessageNotifications); } if (typeof data.systemChannelFlags !== 'undefined') { - _data.systemChannelFlags = SystemChannelFlags.resolve(data.systemChannelFlags); + _data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); } return this.client.api.guilds(this.id).patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); From a36f3869b3e09fa3875f751797eea9ce92958c79 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 13 Feb 2020 09:07:34 +1100 Subject: [PATCH 377/428] fix(Message): handle undefined/null content in cleanContent getter (#3778) * Handle undefined/null content in Message#cleanContent * Typings * Update typings/index.d.ts Co-Authored-By: SpaceEEC Co-authored-by: SpaceEEC --- src/structures/Message.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index a7b55560..b32c874c 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -301,7 +301,8 @@ class Message extends Base { * @readonly */ get cleanContent() { - return Util.cleanContent(this.content, this); + // eslint-disable-next-line eqeqeq + return this.content != null ? Util.cleanContent(this.content, this) : null; } /** From 6770c7c786a195f166af4f159965a6d3e5541377 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 13 Feb 2020 21:46:46 +0000 Subject: [PATCH 378/428] typings: add invite events to WSEventType and constants (#3782) * fix event typings * add ws events to jsdoc --- src/util/Constants.js | 2 ++ typings/index.d.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index b613021b..e97adafd 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -319,6 +319,8 @@ exports.PartialTypes = keyMirror([ * * GUILD_CREATE * * GUILD_DELETE * * GUILD_UPDATE + * * INVITE_CREATE + * * INVITE_DELETE * * GUILD_MEMBER_ADD * * GUILD_MEMBER_REMOVE * * GUILD_MEMBER_UPDATE diff --git a/typings/index.d.ts b/typings/index.d.ts index 09edcef5..3f72f175 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -416,6 +416,8 @@ declare module 'discord.js' { GUILD_CREATE: 'guildCreate'; GUILD_DELETE: 'guildDelete'; GUILD_UPDATE: 'guildUpdate'; + INVITE_CREATE: 'inviteCreate'; + INVITE_DELETE: 'inviteDelete'; GUILD_UNAVAILABLE: 'guildUnavailable'; GUILD_MEMBER_ADD: 'guildMemberAdd'; GUILD_MEMBER_REMOVE: 'guildMemberRemove'; @@ -2688,6 +2690,8 @@ declare module 'discord.js' { | 'GUILD_CREATE' | 'GUILD_DELETE' | 'GUILD_UPDATE' + | 'INVITE_CREATE' + | 'INVITE_DELETE' | 'GUILD_MEMBER_ADD' | 'GUILD_MEMBER_REMOVE' | 'GUILD_MEMBER_UPDATE' From 21d37ed0cc71b227be0feadc4ffbcd522ca4f00e Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 13 Feb 2020 23:48:36 +0200 Subject: [PATCH 379/428] docs: clarify what zlib-sync does (#3785) * docs: Clarify what zlib-sync does * docs: Update docs site welcome * src/docs: Remove `ws.compress` from docs This is a deprecated parameter and you shouldn't use it unless you have zlib-sync installed, and even then, compression is automatically enabled * docs: Apply suggestion Co-Authored-By: Sugden <28943913+NotSugden@users.noreply.github.com> Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> --- README.md | 2 +- docs/general/welcome.md | 2 +- src/util/Constants.js | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3a295798..c0e35754 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Using opusscript is only recommended for development environments where @discord For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 936c01a4..ae90aa25 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -46,7 +46,7 @@ Using opusscript is only recommended for development environments where @discord For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) diff --git a/src/util/Constants.js b/src/util/Constants.js index e97adafd..c3934a3f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -61,8 +61,6 @@ 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=false] Whether to compress data sent on the connection - * (defaults to `false` for browsers) */ ws: { large_threshold: 250, From bc5e2950d065fd7c65259d050063299fc4468136 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sun, 16 Feb 2020 18:24:12 +0000 Subject: [PATCH 380/428] fix(ReactionManager): update message if partial (#3789) * update message after fetching if it is partial * suggested changes Co-Authored-By: SpaceEEC Co-authored-by: SpaceEEC --- src/managers/ReactionManager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/managers/ReactionManager.js b/src/managers/ReactionManager.js index a9f84709..31a98fdf 100644 --- a/src/managers/ReactionManager.js +++ b/src/managers/ReactionManager.js @@ -73,6 +73,7 @@ class ReactionManager extends BaseManager { const existing = this.cache.get(id); if (!this._partial(reactionEmoji)) return existing; const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); + if (this.message.partial) this.message._patch(data); if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { reactionEmoji.reaction._patch({ count: 0 }); this.message.reactions.cache.delete(id); From 46ee06b4245ee930698f880ae35c90b321382858 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 16 Feb 2020 20:36:10 +0200 Subject: [PATCH 381/428] feat(Message): add support for flag editing / embed suppression (#3674) --- src/structures/APIMessage.js | 19 +++++++++++++++++++ src/structures/Message.js | 17 +++++++++++++++++ typings/index.d.ts | 2 ++ 3 files changed, 38 insertions(+) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 6edd6578..d68ed056 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -6,6 +6,7 @@ const MessageAttachment = require('./MessageAttachment'); const { browser } = require('../util/Constants'); const Util = require('../util/Util'); const { RangeError } = require('../errors'); +const MessageFlags = require('../util/MessageFlags'); /** * Represents a message to be sent to the API. @@ -63,6 +64,16 @@ class APIMessage { return this.target instanceof User || this.target instanceof GuildMember; } + /** + * Whether or not the target is a message + * @type {boolean} + * @readonly + */ + get isMessage() { + const Message = require('./Message'); + return this.target instanceof Message; + } + /** * Makes the content of this message. * @returns {?(string|string[])} @@ -126,6 +137,7 @@ class APIMessage { const content = this.makeContent(); const tts = Boolean(this.options.tts); + let nonce; if (typeof this.options.nonce !== 'undefined') { nonce = parseInt(this.options.nonce); @@ -149,6 +161,12 @@ class APIMessage { if (this.options.avatarURL) avatarURL = this.options.avatarURL; } + let flags; + if (this.isMessage) { + // eslint-disable-next-line eqeqeq + flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield; + } + this.data = { content, tts, @@ -157,6 +175,7 @@ class APIMessage { embeds, username, avatar_url: avatarURL, + flags, }; return this; } diff --git a/src/structures/Message.js b/src/structures/Message.js index b32c874c..0c856fda 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -532,6 +532,23 @@ class Message extends Base { return this.client.fetchWebhook(this.webhookID); } + /** + * Suppresses or unsuppresses embeds on a message + * @param {boolean} [suppress=true] If the embeds should be suppressed or not + * @returns {Promise} + */ + suppressEmbeds(suppress = true) { + const flags = new MessageFlags(this.flags.bitfield); + + if (suppress) { + flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } else { + flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } + + return this.edit({ flags }); + } + /** * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This diff --git a/typings/index.d.ts b/typings/index.d.ts index 3f72f175..d9daf995 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -994,6 +994,7 @@ declare module 'discord.js' { public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; public reply(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; public reply(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + public suppressEmbeds(suppress?: boolean): Promise; public toJSON(): object; public toString(): string; public unpin(): Promise; @@ -2423,6 +2424,7 @@ declare module 'discord.js' { content?: string; embed?: MessageEmbedOptions | null; code?: string | boolean; + flags?: BitFieldResolvable; } interface MessageEmbedOptions { From e4e977f44779bc602e91a998e9338d7c401eff15 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 16 Feb 2020 20:41:37 +0200 Subject: [PATCH 382/428] src: update client options and shards value if fetching shard count (#3787) * src: Update client options and shards value if fetching shard count * src: Fix bug and remove more dead code --- src/client/websocket/WebSocketManager.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 02139f48..0403b52c 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -18,7 +18,7 @@ const BeforeReadyWhitelist = [ WSEvents.GUILD_MEMBER_REMOVE, ]; -const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1); +const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1).map(Number); const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007]; /** @@ -153,24 +153,17 @@ class WebSocketManager extends EventEmitter { this.gateway = `${gatewayURL}/`; - const { shards } = this.client.options; + let { shards } = this.client.options; if (shards === 'auto') { this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); this.totalShards = this.client.options.shardCount = recommendedShards; - if (!this.client.options.shards.length) { - this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); - } + shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); } - if (Array.isArray(shards)) { - this.totalShards = shards.length; - this.debug(`Spawning shards: ${shards.join(', ')}`); - this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); - } else { - this.debug(`Spawning ${this.totalShards} shards`); - this.shardQueue = new Set(Array.from({ length: this.totalShards }, (_, id) => new WebSocketShard(this, id))); - } + this.totalShards = shards.length; + this.debug(`Spawning shards: ${shards.join(', ')}`); + this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); await this._handleSessionLimit(remaining, reset_after); @@ -340,7 +333,7 @@ class WebSocketManager extends EventEmitter { this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); this.destroyed = true; this.shardQueue.clear(); - for (const shard of this.shards.values()) shard.destroy({ closeCode: 1000, reset: true, emit: false }); + for (const shard of this.shards.values()) shard.destroy({ closeCode: 1000, reset: true, emit: false, log: false }); } /** From c4c6ad4a63fcbc46fdf4e6f1ea01472e67c31839 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 17 Feb 2020 13:15:59 +0000 Subject: [PATCH 383/428] docs/typings(WSEvents): add missing, remove duplicated and userbot events (#3800) - Added `MESSAGE_REACTION_REMOVE_ALL` - Remove duplicated `VOICE_STATE_UPDATE` - Remove userbot `USER_SETTINGS_UPDATE` --- src/util/Constants.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index c3934a3f..cd6f5f5a 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -341,8 +341,8 @@ exports.PartialTypes = keyMirror([ * * MESSAGE_REACTION_ADD * * MESSAGE_REACTION_REMOVE * * MESSAGE_REACTION_REMOVE_ALL + * * MESSAGE_REACTION_REMOVE_EMOJI * * USER_UPDATE - * * USER_SETTINGS_UPDATE * * PRESENCE_UPDATE * * TYPING_START * * VOICE_STATE_UPDATE diff --git a/typings/index.d.ts b/typings/index.d.ts index d9daf995..6acc486d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2716,9 +2716,9 @@ declare module 'discord.js' { | 'MESSAGE_REACTION_ADD' | 'MESSAGE_REACTION_REMOVE' | 'MESSAGE_REACTION_REMOVE_ALL' + | 'MESSAGE_REACTION_REMOVE_EMOJI' | 'USER_UPDATE' | 'PRESENCE_UPDATE' - | 'VOICE_STATE_UPDATE' | 'TYPING_START' | 'VOICE_STATE_UPDATE' | 'VOICE_SERVER_UPDATE' From f85230812ff6de4f1ba1c021c93fe308fb0685e6 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 19 Feb 2020 20:34:26 +0000 Subject: [PATCH 384/428] typings(Guild): mark afkChannel* & applicationID as nullable (#3805) --- typings/index.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 6acc486d..4372b4ec 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -666,10 +666,10 @@ declare module 'discord.js' { private _sortedChannels(channel: Channel): Collection; private _memberSpeakUpdate(user: Snowflake, speaking: boolean): void; - public readonly afkChannel: VoiceChannel; - public afkChannelID: Snowflake; + public readonly afkChannel: VoiceChannel | null; + public afkChannelID: Snowflake | null; public afkTimeout: number; - public applicationID: Snowflake; + public applicationID: Snowflake | null; public available: boolean; public banner: string | null; public channels: GuildChannelManager; From f6075a6e3ac31674b66546481dfeae62329dc6cc Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 21 Feb 2020 21:48:08 +0100 Subject: [PATCH 385/428] typings(Constants): add VerificationLevels (#3811) --- typings/index.d.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4372b4ec..dcde300e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -509,6 +509,13 @@ declare module 'discord.js' { DARK_BUT_NOT_BLACK: 0x2C2F33; NOT_QUITE_BLACK: 0x23272A; }; + VerificationLevels: [ + 'None', + 'Low', + 'Medium', + '(╯°□°)╯︵ ┻━┻', + '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', + ]; Status: { READY: 0; CONNECTING: 1; From ef8acecc70542c0ffbebe4626c111c5dbd1f7993 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 22 Feb 2020 11:31:51 +0000 Subject: [PATCH 386/428] feat: add new MessageTypes (14 and 15) (#3812) * document types * typings * move comment --- src/util/Constants.js | 6 ++++++ typings/index.d.ts | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index cd6f5f5a..2bbe580d 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -404,6 +404,8 @@ exports.WSEvents = keyMirror([ * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 * * CHANNEL_FOLLOW_ADD + * * GUILD_DISCOVERY_DISQUALIFIED + * * GUILD_DISCOVERY_REQUALIFIED * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -420,6 +422,10 @@ exports.MessageTypes = [ 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', 'CHANNEL_FOLLOW_ADD', + // 13 isn't yet documented + null, + 'GUILD_DISCOVERY_DISQUALIFIED', + 'GUILD_DISCOVERY_REQUALIFIED', ]; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index dcde300e..76a0dc93 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2479,7 +2479,9 @@ declare module 'discord.js' { | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3' - | 'CHANNEL_FOLLOW_ADD'; + | 'CHANNEL_FOLLOW_ADD' + | 'GUILD_DISCOVERY_DISQUALIFIED' + | 'GUILD_DISCOVERY_REQUALIFIED'; interface OverwriteData { allow?: PermissionResolvable; From bea6da621d0e37000826c6fddf6f33c964704443 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 22 Feb 2020 12:04:41 +0000 Subject: [PATCH 387/428] feat(Guild): add rulesChannel and publicUpdatesChannel (#3810) * add rulesChannel* & publicUpdatesChannel* * update typings --- src/structures/Guild.js | 34 ++++++++++++++++++++++++++++++++++ typings/index.d.ts | 4 ++++ 2 files changed, 38 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index ca462381..fc04530c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -320,6 +320,20 @@ class Guild extends Base { this.available = !data.unavailable; this.features = data.features || this.features || []; + /** + * The ID of the rules channel for the guild + * This is only available on guilds with the `PUBLIC` feature + * @type {?Snowflake} + */ + this.rulesChannelID = data.rules_channel_id; + + /** + * The ID of the public updates channel for the guild + * This is only available on guilds with the `PUBLIC` feature + * @type {?Snowflake} + */ + this.publicUpdatesChannelID = data.public_updates_channel_id; + if (data.channels) { this.channels.cache.clear(); for (const rawChannel of data.channels) { @@ -504,6 +518,26 @@ class Guild extends Base { return this.client.channels.cache.get(this.embedChannelID) || null; } + /** + * Rules channel for this guild + * This is only available on guilds with the `PUBLIC` feature + * @type {?TextChannel} + * @readonly + */ + get rulesChannel() { + return this.client.channels.cache.get(this.rulesChannelID) || null; + } + + /** + * Public updates channel for this guild + * This is only available on guilds with the `PUBLIC` feature + * @type {?TextChannel} + * @readonly + */ + get publicUpdatesChannel() { + return this.client.channels.cache.get(this.publicUpdatesChannelID) || null; + } + /** * The client user as a GuildMember of this guild * @type {?GuildMember} diff --git a/typings/index.d.ts b/typings/index.d.ts index 76a0dc93..373ff405 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -710,8 +710,12 @@ declare module 'discord.js' { public premiumSubscriptionCount: number | null; public premiumTier: PremiumTier; public presences: PresenceManager; + public readonly publicUpdatesChannel: TextChannel | null; + public publicUpdatesChannelID: Snowflake | null; public region: string; public roles: RoleManager; + public readonly rulesChannel: TextChannel | null; + public rulesChannelID: Snowflake | null; public readonly shard: WebSocketShard; public shardID: number; public splash: string | null; From 161f90761aa357f17066f4171a5f43f76a552eb7 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 22 Feb 2020 12:25:27 +0000 Subject: [PATCH 388/428] feat(PartialGroupDMChannel): to support Invite#channel for group dms (#3786) * add PartialGroupDMChannel class * fix lint * add new errors * add new class to Channel.create * fix lint * update typings accordingly * better implement errors * remove unnecessary functions * oops * lint * lint * lint * more lint * more lint * jsdoc typo * suggested changes * i did not forget the typings --- src/errors/Messages.js | 3 ++ src/structures/Channel.js | 16 +++++++-- src/structures/PartialGroupDMChannel.js | 46 +++++++++++++++++++++++++ typings/index.d.ts | 7 ++++ 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/structures/PartialGroupDMChannel.js diff --git a/src/errors/Messages.js b/src/errors/Messages.js index db339ca2..7d055649 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -94,6 +94,9 @@ const Messages = { REACTION_RESOLVE_USER: 'Couldn\'t resolve the user ID to remove from the reaction.', VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', + + DELETE_GROUP_DM_CHANNEL: 'Bots don\'t have access to Group DM Channels and cannot delete them', + FETCH_GROUP_DM_CHANNEL: 'Bots don\'t have access to Group DM Channels and cannot fetch them', }; for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/structures/Channel.js b/src/structures/Channel.js index d5949c3c..5e1194f8 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -96,9 +96,19 @@ class Channel extends Base { static create(client, data, guild) { const Structures = require('../util/Structures'); let channel; - if (data.type === ChannelTypes.DM || (data.type !== ChannelTypes.GROUP && !data.guild_id && !guild)) { - const DMChannel = Structures.get('DMChannel'); - channel = new DMChannel(client, data); + if (!data.guild_id && !guild) { + switch (data.type) { + case ChannelTypes.DM: { + const DMChannel = Structures.get('DMChannel'); + channel = new DMChannel(client, data); + break; + } + case ChannelTypes.GROUP: { + const PartialGroupDMChannel = require('./PartialGroupDMChannel'); + channel = new PartialGroupDMChannel(client, data); + break; + } + } } else { guild = guild || client.guilds.cache.get(data.guild_id); if (guild) { diff --git a/src/structures/PartialGroupDMChannel.js b/src/structures/PartialGroupDMChannel.js new file mode 100644 index 00000000..e398f235 --- /dev/null +++ b/src/structures/PartialGroupDMChannel.js @@ -0,0 +1,46 @@ +'use strict'; + +const Channel = require('./Channel'); +const { Error } = require('../errors'); + +/** + * Represents a Partial Group DM Channel on Discord. + * @extends {Channel} + */ +class PartialGroupDMChannel extends Channel { + constructor(client, data) { + super(client, data); + + /** + * The name of this Group DM Channel + * @type {string} + */ + this.name = data.name; + + /** + * The hash of the channel icon + * @type {?string} + */ + this.icon = data.icon; + } + + /** + * The URL to this channel's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size); + } + + delete() { + return Promise.reject(new Error('DELETE_GROUP_DM_CHANNEL')); + } + + fetch() { + return Promise.reject(new Error('FETCH_GROUP_DM_CHANNEL')); + } +} + +module.exports = PartialGroupDMChannel; diff --git a/typings/index.d.ts b/typings/index.d.ts index 373ff405..f71b9da9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -840,6 +840,13 @@ declare module 'discord.js' { public nsfw: boolean; } + export class PartialGroupDMChannel extends Channel { + constructor(client: Client, data: object); + public name: string; + public icon: string | null; + public iconURL(options?: ImageURLOptions): string | null; + } + export class GuildEmoji extends Emoji { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; From b347e9ec26c74e7b2996c92d57edf206d3b31c54 Mon Sep 17 00:00:00 2001 From: iBisho <37049432+iBisho@users.noreply.github.com> Date: Sat, 22 Feb 2020 18:36:29 -0300 Subject: [PATCH 389/428] refactor(MessageEmbed): simplify initialization of files property (#3814) --- src/structures/MessageEmbed.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 706b20e9..5ee0af07 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -152,10 +152,7 @@ class MessageEmbed { * The files of this embed * @type {Array} */ - this.files = []; - if (data.files) { - this.files = data.files; - } + this.files = data.files || []; } /** From ecd8cccddf9b83f4f7cd858fdcad9e436ac51794 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sun, 23 Feb 2020 08:16:20 +0000 Subject: [PATCH 390/428] typings(AddGuildMemberOptions): change accessToken from String to string (#3815) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index f71b9da9..7ca76442 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2031,7 +2031,7 @@ declare module 'discord.js' { } interface AddGuildMemberOptions { - accessToken: String; + accessToken: string; nick?: string; roles?: Collection | RoleResolvable[]; mute?: boolean; From b727f6c1b9dfca28f77908c28071da423fb3e503 Mon Sep 17 00:00:00 2001 From: Souji Date: Sun, 23 Feb 2020 20:41:48 +0100 Subject: [PATCH 391/428] feat: bring embed builder field manipulation in line with underlying array functionality (#3761) * feat: splice multiple fields * remove MessageEmbed#spliceField * add MessageEmbed#spliceFields * to behave more like Array#splice * and allow multiple fields to be replaced/inserted * update typings accordingly * refactor: rename check to normalize * check suggests boolean return type * feat: allow spread args or array as field input * rewrite: replace addField in favor of addFields * typings: account for changes * chore: bump min node to 11.0.0 * for Array#flat * fix: bump min-node in package engines field * remove addBlankField --- README.md | 2 +- docs/general/faq.md | 2 +- docs/general/welcome.md | 2 +- package.json | 2 +- src/structures/MessageEmbed.js | 42 ++++++++++++++-------------------- typings/index.d.ts | 8 +++---- 6 files changed, 25 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index c0e35754..b0b02969 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 10.2.0 or newer is required.** +**Node.js 11.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` diff --git a/docs/general/faq.md b/docs/general/faq.md index d57060db..59616dfe 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -3,7 +3,7 @@ These questions are some of the most frequently asked. ## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽ -Update to Node.js 10.0.0 or newer. +Update to Node.js 11.0.0 or newer. ## How do I get voice working? - Install FFMPEG. diff --git a/docs/general/welcome.md b/docs/general/welcome.md index ae90aa25..e2acee80 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -33,7 +33,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 10.0.0 or newer is required.** +**Node.js 11.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` diff --git a/package.json b/package.json index 230ec470..b41d42c3 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "webpack-cli": "^3.2.3" }, "engines": { - "node": ">=10.2.0" + "node": ">=11.0.0" }, "browser": { "@discordjs/opus": false, diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 5ee0af07..89664af1 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -188,41 +188,24 @@ class MessageEmbed { } /** - * Adds a field to the embed (max 25). - * @param {StringResolvable} name The name of the field - * @param {StringResolvable} value The value of the field - * @param {boolean} [inline=false] Set the field to display inline + * Adds a fields to the embed (max 25). + * @param {...EmbedField|EmbedField[]} fields The fields to add * @returns {MessageEmbed} */ - addField(name, value, inline) { - this.fields.push(this.constructor.checkField(name, value, inline)); + addFields(...fields) { + this.fields.push(...this.constructor.normalizeFields(fields)); return this; } - /** - * Convenience function for `.addField('\u200B', '\u200B', inline)`. - * @param {boolean} [inline=false] Set the field to display inline - * @returns {MessageEmbed} - */ - addBlankField(inline) { - return this.addField('\u200B', '\u200B', inline); - } - /** * Removes, replaces, and inserts fields in the embed (max 25). * @param {number} index The index to start at * @param {number} deleteCount The number of fields to remove - * @param {StringResolvable} [name] The name of the field - * @param {StringResolvable} [value] The value of the field - * @param {boolean} [inline=false] Set the field to display inline + * @param {...EmbedField|EmbedField[]} [fields] The replacing field objects * @returns {MessageEmbed} */ - spliceField(index, deleteCount, name, value, inline) { - if (name && value) { - this.fields.splice(index, deleteCount, this.constructor.checkField(name, value, inline)); - } else { - this.fields.splice(index, deleteCount); - } + spliceFields(index, deleteCount, ...fields) { + this.fields.splice(index, deleteCount, ...this.constructor.normalizeFields(...fields)); return this; } @@ -373,13 +356,22 @@ class MessageEmbed { * @param {boolean} [inline=false] Set the field to display inline * @returns {EmbedField} */ - static checkField(name, value, inline = false) { + static normalizeField(name, value, inline = false) { name = Util.resolveString(name); if (!name) throw new RangeError('EMBED_FIELD_NAME'); value = Util.resolveString(value); if (!value) throw new RangeError('EMBED_FIELD_VALUE'); return { name, value, inline }; } + + /** + * Check for valid field input and resolves strings + * @param {...EmbedField|EmbedField[]} fields Fields to normalize + * @returns {EmbedField[]} + */ + static normalizeFields(...fields) { + return fields.flat(2).map(({ name, value, inline }) => this.normalizeField(name, value, inline)); + } } module.exports = MessageEmbed; diff --git a/typings/index.d.ts b/typings/index.d.ts index 7ca76442..c15391ea 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1070,8 +1070,7 @@ declare module 'discord.js' { public type: string; public url: string; public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number } | null; - public addBlankField(inline?: boolean): this; - public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; + public addFields(...fields: EmbedField[] | EmbedField[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; public setColor(color: ColorResolvable): this; @@ -1082,10 +1081,11 @@ declare module 'discord.js' { public setTimestamp(timestamp?: Date | number): this; public setTitle(title: StringResolvable): this; public setURL(url: string): this; - public spliceField(index: number, deleteCount: number, name?: StringResolvable, value?: StringResolvable, inline?: boolean): this; + public spliceFields(index: number, deleteCount: number, ...fields: EmbedField[] | EmbedField[][]): this; public toJSON(): object; - public static checkField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; + public static normalizeField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; + public static normalizeFields(...fields: EmbedField[] | EmbedField[][]): Required[]; } export class MessageMentions { From 4ec01ddef56272f6bed23dd0eced8ea9851127b7 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 23 Feb 2020 20:42:47 +0100 Subject: [PATCH 392/428] feat(MessageEmbed): change toJSON method to return an api-compatible object (#3813) --- src/structures/APIMessage.js | 2 +- src/structures/MessageEmbed.js | 9 ++------- typings/index.d.ts | 2 -- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index d68ed056..b05fb68f 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -152,7 +152,7 @@ class APIMessage { } else if (this.options.embed) { embedLikes.push(this.options.embed); } - const embeds = embedLikes.map(e => new MessageEmbed(e)._apiTransform()); + const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON()); let username; let avatarURL; diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 89664af1..19a93c34 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -317,16 +317,11 @@ class MessageEmbed { return this; } - toJSON() { - return Util.flatten(this, { hexColor: true }); - } - /** - * Transforms the embed object to be processed. + * Transforms the embed to a plain object. * @returns {Object} The raw data of this embed - * @private */ - _apiTransform() { + toJSON() { return { title: this.title, type: 'rich', diff --git a/typings/index.d.ts b/typings/index.d.ts index c15391ea..3a046059 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1051,8 +1051,6 @@ declare module 'discord.js' { export class MessageEmbed { constructor(data?: MessageEmbed | MessageEmbedOptions); - private _apiTransform(): MessageEmbedOptions; - public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string } | null; public color: number; public readonly createdAt: Date | null; From 28490e84b069fcab660c9158d27931e9b013209f Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 09:41:29 +0000 Subject: [PATCH 393/428] typings(Invite): channel can be a PartialGroupDMChannel (#3823) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 3a046059..54a04853 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -931,7 +931,7 @@ declare module 'discord.js' { export class Invite extends Base { constructor(client: Client, data: object); - public channel: GuildChannel; + public channel: GuildChannel | PartialGroupDMChannel; public code: string; public readonly deletable: boolean; public readonly createdAt: Date | null; From e57ef25082eb3b2447b73b551dfc7cc0d4d59a19 Mon Sep 17 00:00:00 2001 From: Tenpi <37512637+Tenpi@users.noreply.github.com> Date: Mon, 24 Feb 2020 05:44:54 -0500 Subject: [PATCH 394/428] typings/fix(MessageEmbed): add interfaces for props, fix copy constructor (#3492) * updated typings * Updated docs * fixed types for MessageEmbedOptions * added curly bracket spaces * fix(MessageEmbed): make copy constructor work properly * fix(MessageEmbed): copy the provider too Co-authored-by: SpaceEEC --- src/structures/MessageEmbed.js | 61 +++++++++++++++++++++++--------- typings/index.d.ts | 63 +++++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 19a93c34..66f08722 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -67,81 +67,108 @@ class MessageEmbed { this.fields = data.fields ? data.fields.map(Util.cloneObject) : []; /** - * The thumbnail of this embed (if there is one) - * @type {?Object} + * @typedef {Object} MessageEmbedThumbnail * @property {string} url URL for this thumbnail * @property {string} proxyURL ProxyURL for this thumbnail * @property {number} height Height of this thumbnail * @property {number} width Width of this thumbnail */ + + /** + * The thumbnail of this embed (if there is one) + * @type {?MessageEmbedThumbnail} + */ this.thumbnail = data.thumbnail ? { url: data.thumbnail.url, - proxyURL: data.thumbnail.proxy_url, + proxyURL: data.thumbnail.proxyURL || data.thumbnail.proxy_url, height: data.thumbnail.height, width: data.thumbnail.width, } : null; /** - * The image of this embed, if there is one - * @type {?Object} + * @typedef {Object} MessageEmbedImage * @property {string} url URL for this image * @property {string} proxyURL ProxyURL for this image * @property {number} height Height of this image * @property {number} width Width of this image */ + + /** + * The image of this embed, if there is one + * @type {?MessageEmbedImage} + */ this.image = data.image ? { url: data.image.url, - proxyURL: data.image.proxy_url, + proxyURL: data.image.proxyURL || data.image.proxy_url, height: data.image.height, width: data.image.width, } : null; /** - * The video of this embed (if there is one) - * @type {?Object} + * @typedef {Object} MessageEmbedVideo * @property {string} url URL of this video * @property {string} proxyURL ProxyURL for this video * @property {number} height Height of this video * @property {number} width Width of this video + */ + + /** + * The video of this embed (if there is one) + * @type {?MessageEmbedVideo} * @readonly */ this.video = data.video ? { url: data.video.url, - proxyURL: data.video.proxy_url, + proxyURL: data.video.proxyURL || data.video.proxy_url, height: data.video.height, width: data.video.width, } : null; /** - * The author of this embed (if there is one) - * @type {?Object} + * @typedef {Object} MessageEmbedAuthor * @property {string} name The name of this author * @property {string} url URL of this author * @property {string} iconURL URL of the icon for this author * @property {string} proxyIconURL Proxied URL of the icon for this author */ + + /** + * The author of this embed (if there is one) + * @type {?MessageEmbedAuthor} + */ this.author = data.author ? { name: data.author.name, url: data.author.url, iconURL: data.author.iconURL || data.author.icon_url, - proxyIconURL: data.author.proxyIconUrl || data.author.proxy_icon_url, + proxyIconURL: data.author.proxyIconURL || data.author.proxy_icon_url, } : null; /** - * The provider of this embed (if there is one) - * @type {?Object} + * @typedef {Object} MessageEmbedProvider * @property {string} name The name of this provider * @property {string} url URL of this provider */ - this.provider = data.provider; /** - * The footer of this embed - * @type {?Object} + * The provider of this embed (if there is one) + * @type {?MessageEmbedProvider} + */ + this.provider = data.provider ? { + name: data.provider.name, + url: data.provider.name, + } : null; + + /** + * @typedef {Object} MessageEmbedFooter * @property {string} text The text of this footer * @property {string} iconURL URL of the icon for this footer * @property {string} proxyIconURL Proxied URL of the icon for this footer */ + + /** + * The footer of this embed + * @type {?MessageEmbedFooter} + */ this.footer = data.footer ? { text: data.footer.text, iconURL: data.footer.iconURL || data.footer.icon_url, diff --git a/typings/index.d.ts b/typings/index.d.ts index 54a04853..44851f00 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1051,23 +1051,23 @@ declare module 'discord.js' { export class MessageEmbed { constructor(data?: MessageEmbed | MessageEmbedOptions); - public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string } | null; + public author: MessageEmbedAuthor | null; public color: number; public readonly createdAt: Date | null; public description: string; public fields: EmbedField[]; public files: (MessageAttachment | string | FileOptions)[]; - public footer: { text?: string; iconURL?: string; proxyIconURL?: string } | null; + public footer: MessageEmbedFooter | null; public readonly hexColor: string | null; - public image: { url: string; proxyURL?: string; height?: number; width?: number; } | null; + public image: MessageEmbedImage | null; public readonly length: number; - public provider: { name: string; url: string; }; - public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; } | null; + public provider: MessageEmbedProvider | null; + public thumbnail: MessageEmbedThumbnail | null; public timestamp: number | null; public title: string; public type: string; public url: string; - public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number } | null; + public readonly video: MessageEmbedVideo | null; public addFields(...fields: EmbedField[] | EmbedField[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; @@ -2449,13 +2449,52 @@ declare module 'discord.js' { url?: string; timestamp?: Date | number; color?: ColorResolvable; - fields?: { name: string; value: string; inline?: boolean; }[]; + fields?: EmbedField[]; files?: (MessageAttachment | string | FileOptions)[]; - author?: { name?: string; url?: string; icon_url?: string; iconURL?: string; }; - thumbnail?: { url?: string; height?: number; width?: number; }; - image?: { url?: string; proxy_url?: string; proxyURL?: string; height?: number; width?: number; }; - video?: { url?: string; height?: number; width?: number; }; - footer?: { text?: string; icon_url?: string; iconURL?: string; }; + author?: Partial & { icon_url?: string; proxy_icon_url?: string }; + thumbnail?: Partial & { proxy_url?: string }; + image?: Partial & { proxy_url?: string }; + video?: Partial & { proxy_url?: string }; + footer?: Partial & { icon_url?: string; proxy_icon_url?: string }; + } + + interface MessageEmbedAuthor { + name?: string; + url?: string; + iconURL?: string; + proxyIconURL?: string; + } + + interface MessageEmbedThumbnail { + url: string; + proxyURL?: string; + height?: number; + width?: number; + } + + interface MessageEmbedFooter { + text?: string; + iconURL?: string; + proxyIconURL?: string; + } + + interface MessageEmbedImage { + url: string; + proxyURL?: string; + height?: number; + width?: number; + } + + interface MessageEmbedProvider { + name: string; + url: string; + } + + interface MessageEmbedVideo { + url?: string; + proxyURL?: string; + height?: number; + width?: number; } interface MessageOptions { From ccb83a71ee5b85abaf6e90bc5ca3987c708aa57f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 24 Feb 2020 13:03:02 +0100 Subject: [PATCH 395/428] docs/typings(MessageAttachment): mark spoiler as readonly, order spoiler in typings (#3714) --- src/structures/MessageAttachment.js | 1 + typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 1af5d1ed..f5fb723b 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -84,6 +84,7 @@ class MessageAttachment { /** * Whether or not this attachment has been marked as a spoiler * @type {boolean} + * @readonly */ get spoiler() { return Util.basename(this.url).startsWith('SPOILER_'); diff --git a/typings/index.d.ts b/typings/index.d.ts index 44851f00..69fb66d5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1027,9 +1027,9 @@ declare module 'discord.js' { public name?: string; public proxyURL: string; public size: number; + public readonly spoiler: boolean; public url: string; public width: number | null; - public readonly spoiler: boolean; public setFile(attachment: BufferResolvable | Stream, name?: string): this; public setName(name: string): this; public toJSON(): object; From d406f42ce03f3e1cf1172dd0cfe4577c00ba514a Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 24 Feb 2020 13:03:45 +0100 Subject: [PATCH 396/428] docs/typings(SystemChannelFlags): properly document and use resolvable (#3794) - Change GuildEditData#systemChannelFlags to use SystemChannelFlagsResolvable - Move SystemChannelFlagsResolvable outside of class definition to make the docs generator happy --- src/util/SystemChannelFlags.js | 20 ++++++++++---------- typings/index.d.ts | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/util/SystemChannelFlags.js b/src/util/SystemChannelFlags.js index 14e8fd4f..be953f71 100644 --- a/src/util/SystemChannelFlags.js +++ b/src/util/SystemChannelFlags.js @@ -8,16 +8,16 @@ const BitField = require('./BitField'); * and by setting their corresponding flags you are disabling them * @extends {BitField} */ -class SystemChannelFlags extends BitField { - /** - * Data that can be resolved to give a sytem channel flag bitfield. This can be: - * * A string (see {@link SystemChannelFlags.FLAGS}) - * * A sytem channel flag - * * An instance of SystemChannelFlags - * * An Array of SystemChannelFlagsResolvable - * @typedef {string|number|SystemChannelFlags|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable - */ -} +class SystemChannelFlags extends BitField {} + +/** + * Data that can be resolved to give a sytem channel flag bitfield. This can be: + * * A string (see {@link SystemChannelFlags.FLAGS}) + * * A sytem channel flag + * * An instance of SystemChannelFlags + * * An Array of SystemChannelFlagsResolvable + * @typedef {string|number|SystemChannelFlags|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable + */ /** * Numeric system channel flags. All available properties: diff --git a/typings/index.d.ts b/typings/index.d.ts index 69fb66d5..8ed99645 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2334,7 +2334,7 @@ declare module 'discord.js' { defaultMessageNotifications?: DefaultMessageNotifications | number; afkChannel?: ChannelResolvable; systemChannel?: ChannelResolvable; - systemChannelFlags?: SystemChannelFlags; + systemChannelFlags?: SystemChannelFlagsResolvable; afkTimeout?: number; icon?: Base64Resolvable; owner?: GuildMemberResolvable; From 25cd23e305cee06e8bceaf133e770dd844dbdf50 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:06:00 +0000 Subject: [PATCH 397/428] cleanup(DMChannel): remove _cacheMessage (#3824) * cleanup(DMChannel): remove _cacheMessage * update actions/checkout to v2 Co-authored-by: SpaceEEC --- .github/workflows/test.yml | 6 +++--- src/structures/DMChannel.js | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c51d68e..6bdfc296 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node v12 uses: actions/setup-node@v1 @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node v12 uses: actions/setup-node@v1 @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node v12 uses: actions/setup-node@v1 diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 9042519a..e661bc44 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -91,7 +91,6 @@ class DMChannel extends Channel { createMessageCollector() {} awaitMessages() {} // Doesn't work on DM channels; bulkDelete() {} - _cacheMessage() {} } TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); From 967b533e9d5f08e2be50698fb98d38820d29d88b Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:06:22 +0000 Subject: [PATCH 398/428] typings(MessageEmbed): properly mark properties (#3822) Co-authored-by: SpaceEEC --- typings/index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 8ed99645..d3c1d5b8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1052,9 +1052,9 @@ declare module 'discord.js' { export class MessageEmbed { constructor(data?: MessageEmbed | MessageEmbedOptions); public author: MessageEmbedAuthor | null; - public color: number; + public color?: number; public readonly createdAt: Date | null; - public description: string; + public description?: string; public fields: EmbedField[]; public files: (MessageAttachment | string | FileOptions)[]; public footer: MessageEmbedFooter | null; @@ -1064,9 +1064,9 @@ declare module 'discord.js' { public provider: MessageEmbedProvider | null; public thumbnail: MessageEmbedThumbnail | null; public timestamp: number | null; - public title: string; + public title?: string; public type: string; - public url: string; + public url?: string; public readonly video: MessageEmbedVideo | null; public addFields(...fields: EmbedField[] | EmbedField[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; From 54f24d1fea7ea78672a61a4b1ac7af5efd0d19b0 Mon Sep 17 00:00:00 2001 From: Souji Date: Mon, 24 Feb 2020 14:02:06 +0100 Subject: [PATCH 399/428] typings(MessageEmebd): fix typings for addFields (#3821) * typings(MessageEmebd): fix typings for addFields * fix: add missing semicolon * docs(MessageEmbed): fix various types * in accordance with the scope of the PR * Update src/structures/MessageEmbed.js Co-Authored-By: SpaceEEC Co-authored-by: SpaceEEC --- src/structures/MessageEmbed.js | 13 ++++++++++--- typings/index.d.ts | 14 ++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 66f08722..7cfb6d1a 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -216,7 +216,7 @@ class MessageEmbed { /** * Adds a fields to the embed (max 25). - * @param {...EmbedField|EmbedField[]} fields The fields to add + * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add * @returns {MessageEmbed} */ addFields(...fields) { @@ -228,7 +228,7 @@ class MessageEmbed { * Removes, replaces, and inserts fields in the embed (max 25). * @param {number} index The index to start at * @param {number} deleteCount The number of fields to remove - * @param {...EmbedField|EmbedField[]} [fields] The replacing field objects + * @param {...EmbedFieldData|EmbedFieldData[]} [fields] The replacing field objects * @returns {MessageEmbed} */ spliceFields(index, deleteCount, ...fields) { @@ -386,9 +386,16 @@ class MessageEmbed { return { name, value, inline }; } + /** + * @typedef {Object} EmbedFieldData + * @property {StringResolvable} name The name of this field + * @property {StringResolvable} value The value of this field + * @property {boolean} [inline] If this field will be displayed inline + */ + /** * Check for valid field input and resolves strings - * @param {...EmbedField|EmbedField[]} fields Fields to normalize + * @param {...EmbedFieldData|EmbedFieldData[]} fields Fields to normalize * @returns {EmbedField[]} */ static normalizeFields(...fields) { diff --git a/typings/index.d.ts b/typings/index.d.ts index d3c1d5b8..2bff7f52 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1068,7 +1068,7 @@ declare module 'discord.js' { public type: string; public url?: string; public readonly video: MessageEmbedVideo | null; - public addFields(...fields: EmbedField[] | EmbedField[][]): this; + public addFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; public setColor(color: ColorResolvable): this; @@ -1079,11 +1079,11 @@ declare module 'discord.js' { public setTimestamp(timestamp?: Date | number): this; public setTitle(title: StringResolvable): this; public setURL(url: string): this; - public spliceFields(index: number, deleteCount: number, ...fields: EmbedField[] | EmbedField[][]): this; + public spliceFields(index: number, deleteCount: number, ...fields: EmbedFieldData[] | EmbedFieldData[][]): this; public toJSON(): object; - public static normalizeField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; - public static normalizeFields(...fields: EmbedField[] | EmbedField[][]): Required[]; + public static normalizeField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; + public static normalizeFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): Required[]; } export class MessageMentions { @@ -2184,6 +2184,12 @@ declare module 'discord.js' { interface EmbedField { name: string; value: string; + inline: boolean; + } + + interface EmbedFieldData { + name: StringResolvable; + value: StringResolvable; inline?: boolean; } From e6d22527bb62b04581614d4dbd3de166471b749c Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 17:22:16 +0100 Subject: [PATCH 400/428] chore(typings): semicolon consistency (#3826) --- typings/index.d.ts | 118 ++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 2bff7f52..a924fb4f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -83,7 +83,7 @@ declare module 'discord.js' { export class Base { constructor(client: Client); public readonly client: Client; - public toJSON(...props: { [key: string]: boolean | string }[]): object; + public toJSON(...props: { [key: string]: boolean | string; }[]): object; public valueOf(): string; } @@ -103,7 +103,7 @@ declare module 'discord.js' { public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setImmediate(fn: Function, ...args: any[]): NodeJS.Immediate; - public toJSON(...props: { [key: string]: boolean | string }[]): object; + public toJSON(...props: { [key: string]: boolean | string; }[]): object; } class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { @@ -373,14 +373,14 @@ declare module 'discord.js' { types: PathLike; homepage: string; keywords: string[]; - bugs: { url: string }; - repository: { type: string, url: string }; - browser: { [key: string]: boolean }; - scripts: { [key: string]: string }; - engines: { [key: string]: string }; - dependencies: { [key: string]: string }; - peerDependencies: { [key: string]: string }; - devDependencies: { [key: string]: string }; + bugs: { url: string; }; + repository: { type: string, url: string; }; + browser: { [key: string]: boolean; }; + scripts: { [key: string]: string; }; + engines: { [key: string]: string; }; + dependencies: { [key: string]: string; }; + peerDependencies: { [key: string]: string; }; + devDependencies: { [key: string]: string; }; [key: string]: any; }; browser: boolean; @@ -738,15 +738,15 @@ declare module 'discord.js' { public equals(guild: Guild): boolean; public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; - public fetchBan(user: UserResolvable): Promise<{ user: User, reason: string }>; - public fetchBans(): Promise>; + public fetchBan(user: UserResolvable): Promise<{ user: User; reason: string; }>; + public fetchBans(): Promise>; public fetchEmbed(): Promise; public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; - public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; + public iconURL(options?: ImageURLOptions & { dynamic?: boolean; }): string | null; public leave(): Promise; public member(user: UserResolvable): GuildMember | null; public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; @@ -826,11 +826,11 @@ declare module 'discord.js' { public equals(channel: GuildChannel): boolean; public fetchInvites(): Promise>; public lockPermissions(): Promise; - public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; + public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string; }): Promise; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; public setName(name: string, reason?: string): Promise; - public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; - public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; + public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean; reason?: string; }): Promise; + public setPosition(position: number, options?: { relative?: boolean; reason?: string; }): Promise; public setTopic(topic: string, reason?: string): Promise; public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } @@ -893,7 +893,7 @@ declare module 'discord.js' { public createDM(): Promise; public deleteDM(): Promise; public edit(data: GuildMemberEditData, reason?: string): Promise; - public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean }): boolean; + public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean; }): boolean; public kick(reason?: string): Promise; public permissionsIn(channel: ChannelResolvable): Readonly; public setNickname(nickname: string, reason?: string): Promise; @@ -998,7 +998,7 @@ declare module 'discord.js' { public reference: MessageReference | null; public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; - public delete(options?: { timeout?: number, reason?: string }): Promise; + public delete(options?: { timeout?: number; reason?: string; }): Promise; public edit(content: StringResolvable, options?: MessageEditOptions | MessageEmbed): Promise; public edit(options: MessageEditOptions | MessageEmbed | APIMessage): Promise; public equals(message: Message, rawData: object): boolean; @@ -1007,11 +1007,11 @@ declare module 'discord.js' { public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - public reply(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; - public reply(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions & { split?: false; } | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - public reply(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; - public reply(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; public suppressEmbeds(suppress?: boolean): Promise; public toJSON(): object; public toString(): string; @@ -1138,7 +1138,7 @@ declare module 'discord.js' { public update(options: PermissionOverwriteOption, reason?: string): Promise; public delete(reason?: string): Promise; public toJSON(): object; - public static resolveOverwriteOptions(options: ResolvedOverwriteOptions, initialPermissions: { allow?: PermissionResolvable, deny?: PermissionResolvable }): ResolvedOverwriteOptions; + public static resolveOverwriteOptions(options: ResolvedOverwriteOptions, initialPermissions: { allow?: PermissionResolvable; deny?: PermissionResolvable; }): ResolvedOverwriteOptions; public static resolve(overwrite: OverwriteResolvable, guild: Guild): RawOverwriteData; } @@ -1242,7 +1242,7 @@ declare module 'discord.js' { public setMentionable(mentionable: boolean, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setPermissions(permissions: PermissionResolvable, reason?: string): Promise; - public setPosition(position: number, options?: { relative?: boolean; reason?: string }): Promise; + public setPosition(position: number, options?: { relative?: boolean; reason?: string; }): Promise; public toJSON(): object; public toString(): string; @@ -1410,7 +1410,7 @@ declare module 'discord.js' { public nsfw: boolean; public rateLimitPerUser: number; public topic: string | null; - public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable; reason?: string; }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; public fetchWebhooks(): Promise>; @@ -1421,7 +1421,7 @@ declare module 'discord.js' { public messages: MessageManager; public nsfw: boolean; public topic: string | null; - public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable; reason?: string; }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; public fetchWebhooks(): Promise>; } @@ -1442,10 +1442,10 @@ declare module 'discord.js' { public system?: boolean; public readonly tag: string; public username: string; - public avatarURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; + public avatarURL(options?: ImageURLOptions & { dynamic?: boolean; }): string | null; public createDM(): Promise; public deleteDM(): Promise; - public displayAvatarURL(options?: ImageURLOptions & { dynamic?: boolean }): string; + public displayAvatarURL(options?: ImageURLOptions & { dynamic?: boolean; }): string; public equals(user: User): boolean; public fetch(): Promise; public toString(): string; @@ -1462,7 +1462,7 @@ declare module 'discord.js' { public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; public static discordSort(collection: Collection): Collection; - public static escapeMarkdown(text: string, options?: { codeBlock?: boolean, inlineCode?: boolean, bold?: boolean, italic?: boolean, underline?: boolean, strikethrough?: boolean, spoiler?: boolean, inlineCodeContent?: boolean, codeBlockContent?: boolean }): string; + public static escapeMarkdown(text: string, options?: { codeBlock?: boolean; inlineCode?: boolean; bold?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean; spoiler?: boolean; inlineCodeContent?: boolean; codeBlockContent?: boolean; }): string; public static escapeCodeBlock(text: string): string; public static escapeInlineCode(text: string): string; public static escapeBold(text: string): string; @@ -1472,10 +1472,10 @@ declare module 'discord.js' { public static escapeSpoiler(text: string): string; public static cleanCodeBlockContent(text: string): string; public static fetchRecommendedShards(token: string, guildsPerShard?: number): Promise; - public static flatten(obj: object, ...props: { [key: string]: boolean | string }[]): object; + public static flatten(obj: object, ...props: { [key: string]: boolean | string; }[]): object; public static idToBinary(num: Snowflake): string; - public static makeError(obj: { name: string, message: string, stack: string }): Error; - public static makePlainError(err: Error): { name: string, message: string, stack: string }; + public static makeError(obj: { name: string; message: string; stack: string; }): Error; + public static makePlainError(err: Error): { name: string; message: string; stack: string; }; public static mergeDefault(def: object, given: object): object; public static moveElementInArray(array: any[], element: any, newIndex: number, offset?: boolean): number; public static parseEmoji(text: string): { animated: boolean; name: string; id: string | null; } | null; @@ -1488,7 +1488,7 @@ declare module 'discord.js' { sorted: Collection, route: object, reason?: string - ): Promise<{ id: Snowflake; position: number }[]>; + ): Promise<{ id: Snowflake; position: number; }[]>; public static splitMessage(text: StringResolvable, options?: SplitOptions): string[]; public static str2ab(str: string): ArrayBuffer; } @@ -1592,7 +1592,7 @@ declare module 'discord.js' { class VoiceReceiver extends EventEmitter { constructor(connection: VoiceConnection); - public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm', end?: 'silence' | 'manual' }): Readable; + public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm'; end?: 'silence' | 'manual'; }): Readable; public on(event: 'debug', listener: (error: Error | string) => void): this; public on(event: string, listener: Function): this; @@ -1639,7 +1639,7 @@ declare module 'discord.js' { } class VolumeInterface extends EventEmitter { - constructor(options?: { volume?: number }) + constructor(options?: { volume?: number; }); public readonly volume: number; public readonly volumeDecibels: number; public readonly volumeEditable: boolean; @@ -1736,7 +1736,7 @@ declare module 'discord.js' { private identifyResume(): void; private _send(data: object): void; private processQueue(): void; - private destroy(destroyOptions?: { closeCode?: number; reset?: boolean; emit?: boolean; log?: boolean }): void; + private destroy(destroyOptions?: { closeCode?: number; reset?: boolean; emit?: boolean; log?: boolean; }): void; private _cleanupConnection(): void; private _emitDestroyed(): void; @@ -1790,7 +1790,7 @@ declare module 'discord.js' { public cache: Collection; public cacheType: Collection; public readonly client: Client; - public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): Holds; + public add(data: any, cache?: boolean, { id, extras }?: { id: K; extras: any[]; }): Holds; public remove(key: K): void; public resolve(resolvable: R): Holds | null; public resolveID(resolvable: R): K | null; @@ -1816,9 +1816,9 @@ declare module 'discord.js' { export class GuildChannelManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; - public create(name: string, options: GuildCreateChannelOptions & { type: 'voice' }): Promise; - public create(name: string, options: GuildCreateChannelOptions & { type: 'category' }): Promise; - public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text' }): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'voice'; }): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'category'; }): Promise; + public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text'; }): Promise; public create(name: string, options: GuildCreateChannelOptions): Promise; } @@ -1847,14 +1847,14 @@ declare module 'discord.js' { public ban(user: UserResolvable, options?: BanOptions): Promise; public fetch(options: UserResolvable | FetchMemberOptions): Promise; public fetch(options?: FetchMembersOptions): Promise>; - public prune(options: GuildPruneMembersOptions & { dry?: false, count: false }): Promise; + public prune(options: GuildPruneMembersOptions & { dry?: false; count: false; }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; public unban(user: UserResolvable, reason?: string): Promise; } export class GuildManager extends BaseManager { constructor(client: Client, iterable?: Iterable); - public create(name: string, options?: { region?: string, icon: BufferResolvable | Base64Resolvable | null }): Promise; + public create(name: string, options?: { region?: string; icon: BufferResolvable | Base64Resolvable | null; }): Promise; } export class MessageManager extends BaseManager { @@ -1880,7 +1880,7 @@ declare module 'discord.js' { export class ReactionUserManager extends BaseManager { constructor(client: Client, iterable: Iterable | undefined, reaction: MessageReaction); public reaction: MessageReaction; - public fetch(options?: { limit?: number, after?: Snowflake, before?: Snowflake }): Promise>; + public fetch(options?: { limit?: number; after?: Snowflake; before?: Snowflake; }): Promise>; public remove(user?: UserResolvable): Promise; } @@ -1890,7 +1890,7 @@ declare module 'discord.js' { public readonly highest: Role; public guild: Guild; - public create(options?: { data?: RoleData, reason?: string }): Promise; + public create(options?: { data?: RoleData; reason?: string; }): Promise; public fetch(id: Snowflake, cache?: boolean): Promise; public fetch(id?: Snowflake, cache?: boolean): Promise; } @@ -1924,11 +1924,11 @@ declare module 'discord.js' { lastPinTimestamp: number | null; readonly lastPinAt: Date; send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - send(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; - send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split?: false; } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; - send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; } interface TextBasedChannelFields extends PartialTextBasedChannelFields { @@ -1951,10 +1951,10 @@ declare module 'discord.js' { readonly url: string; delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; - send(content?: StringResolvable, options?: WebhookMessageOptions & { split?: false } | MessageAdditions): Promise; - send(content?: StringResolvable, options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; - send(options?: WebhookMessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; - send(options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions & { split?: false; } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; + send(options?: WebhookMessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + send(options?: WebhookMessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; sendSlackMessage(body: object): Promise; } @@ -2457,11 +2457,11 @@ declare module 'discord.js' { color?: ColorResolvable; fields?: EmbedField[]; files?: (MessageAttachment | string | FileOptions)[]; - author?: Partial & { icon_url?: string; proxy_icon_url?: string }; - thumbnail?: Partial & { proxy_url?: string }; - image?: Partial & { proxy_url?: string }; - video?: Partial & { proxy_url?: string }; - footer?: Partial & { icon_url?: string; proxy_icon_url?: string }; + author?: Partial & { icon_url?: string; proxy_icon_url?: string; }; + thumbnail?: Partial & { proxy_url?: string; }; + image?: Partial & { proxy_url?: string; }; + video?: Partial & { proxy_url?: string; }; + footer?: Partial & { icon_url?: string; proxy_icon_url?: string; }; } interface MessageEmbedAuthor { @@ -2629,8 +2629,8 @@ declare module 'discord.js' { partial: true; fetch(): Promise; } & { - [K in keyof Omit]: T[K] | null; - }; + [K in keyof Omit]: T[K] | null; + }; interface PartialMessage extends Partialize {} interface PartialChannel extends Partialize {} From 98a552107eb980de427621c3444c29e5a0691a25 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 17:56:44 +0100 Subject: [PATCH 401/428] tooling(typings): new linter for typings (#3827) --- package.json | 2 +- tsconfig.json | 26 ++++-- tslint.json | 80 ++++++------------ typings/index.d.ts | 204 ++++++++++++++++++++------------------------- 4 files changed, 134 insertions(+), 178 deletions(-) diff --git a/package.json b/package.json index b41d42c3..f693c27f 100644 --- a/package.json +++ b/package.json @@ -76,12 +76,12 @@ "@types/node": "^10.12.24", "@types/ws": "^6.0.1", "discord.js-docgen": "discordjs/docgen", + "dtslint": "^3.0.0", "eslint": "^5.13.0", "jest": "^24.7.1", "json-filter-loader": "^1.0.0", "terser-webpack-plugin": "^1.2.2", "tslint": "^5.12.1", - "tslint-config-typings": "^0.3.1", "typescript": "^3.3.3", "webpack": "^4.29.3", "webpack-cli": "^3.2.3" diff --git a/tsconfig.json b/tsconfig.json index 37aa901e..dbba5126 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,21 @@ { "compilerOptions": { + "strict": true, + "moduleResolution": "node", + "declaration": false, + "removeComments": false, + "alwaysStrict": true, + "pretty": true, "module": "commonjs", - "target": "es6", - "noImplicitAny": true, - "strictNullChecks": true, - "noEmit": true, - "forceConsistentCasingInFileNames": true - }, - "files": [ - "typings/index.d.ts" - ] + "target": "es2019", + "lib": [ + "esnext", + "esnext.array", + "esnext.asynciterable", + "esnext.intl", + "esnext.symbol" + ], + "sourceMap": false, + "skipLibCheck": true + } } diff --git a/tslint.json b/tslint.json index 71f85026..98a11a30 100644 --- a/tslint.json +++ b/tslint.json @@ -1,62 +1,30 @@ { "extends": [ - "tslint-config-typings" + "dtslint/dtslint.json" ], "rules": { - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "indent": [ - true, - "tabs" - ], - "no-duplicate-variable": true, - "no-unused-variable": [false], - "no-eval": true, - "no-internal-module": true, - "no-trailing-whitespace": true, - "no-unsafe-finally": true, - "no-var-keyword": true, - "one-line": [ - true, - "check-open-brace", - "check-whitespace" - ], - "quotemark": [ - true, - "single" - ], - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "variable-name": [ - true, - "ban-keywords" - ], - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ] + "prefer-readonly": false, + "await-promise": false, + "no-for-in-array": false, + "no-null-undefined-union": false, + "no-promise-as-boolean": false, + "no-void-expression": false, + "strict-string-expressions": false, + "strict-comparisons": false, + "use-default-type-parameter": false, + "no-boolean-literal-compare": false, + "no-unnecessary-qualifier": false, + "no-unnecessary-type-assertion": false, + "expect": false, + "no-import-default-of-export-equals": false, + "no-relative-import-in-test": false, + "no-unnecessary-generics": false, + "strict-export-declare-modifiers": false, + "no-single-declare-module": false, + "member-access": true, + "no-unnecessary-class": false, + "array-type": [true, "array"], + + "no-any-union": false } } diff --git a/typings/index.d.ts b/typings/index.d.ts index a924fb4f..d16180e6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -100,9 +100,9 @@ declare module 'discord.js' { public clearTimeout(timeout: NodeJS.Timer): void; public clearImmediate(timeout: NodeJS.Immediate): void; public destroy(): void; - public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; - public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; - public setImmediate(fn: Function, ...args: any[]): NodeJS.Immediate; + public setInterval(fn: (...args: any[]) => void, delay: number, ...args: any[]): NodeJS.Timer; + public setTimeout(fn: (...args: any[]) => void, delay: number, ...args: any[]): NodeJS.Timer; + public setImmediate(fn: (...args: any[]) => void, ...args: any[]): NodeJS.Immediate; public toJSON(...props: { [key: string]: boolean | string; }[]): object; } @@ -182,14 +182,13 @@ declare module 'discord.js' { public on(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public on(event: 'error', listener: (error: Error) => void): this; public on(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User | PartialUser) => void): this; - public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; + public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable' | 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember | PartialGuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public on(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'inviteCreate' | 'inviteDelete', listener: (invite: Invite) => void): this; - public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; public on(event: 'messageReactionRemoveEmoji', listener: (reaction: MessageReaction) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; @@ -197,20 +196,18 @@ declare module 'discord.js' { public on(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public on(event: 'ready', listener: () => void): this; + public on(event: 'ready' | 'invalidated', listener: () => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; public on(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; - public on(event: 'invalidated', listener: () => void): this; public on(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; public on(event: 'shardError', listener: (error: Error, id: number) => void): this; - public on(event: 'shardReconnecting', listener: (id: number) => void): this; - public on(event: 'shardReady', listener: (id: number) => void): this; + public on(event: 'shardReady' | 'shardReconnecting', listener: (id: number) => void): this; public on(event: 'shardResume', listener: (id: number, replayed: number) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel | PartialChannel) => void): this; public once(event: 'channelPinsUpdate', listener: (channel: Channel | PartialChannel, time: Date) => void): this; @@ -221,33 +218,30 @@ declare module 'discord.js' { public once(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public once(event: 'error', listener: (error: Error) => void): this; public once(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User | PartialUser) => void): this; - public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; + public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable' | 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember | PartialGuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public once(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; - public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User | PartialUser) => void): this; public once(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public once(event: 'ready', listener: () => void): this; + public once(event: 'ready' | 'invalidated', listener: () => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; public once(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; - public once(event: 'invalidated', listener: () => void): this; public once(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; public once(event: 'shardError', listener: (error: Error, id: number) => void): this; - public once(event: 'shardReconnecting', listener: (id: number) => void): this; - public once(event: 'shardReady', listener: (id: number) => void): this; + public once(event: 'shardReady' | 'shardReconnecting', listener: (id: number) => void): this; public once(event: 'shardResume', listener: (id: number, replayed: number) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class ClientVoiceManager { @@ -346,17 +340,15 @@ declare module 'discord.js' { public [Symbol.asyncIterator](): AsyncIterableIterator; public toJSON(): object; - protected listener: Function; + protected listener: (...args: any[]) => void; public abstract collect(...args: any[]): K; public abstract dispose(...args: any[]): K; public abstract endReason(): void; - public on(event: 'collect', listener: (...args: any[]) => void): this; - public on(event: 'dispose', listener: (...args: any[]) => void): this; + public on(event: 'collect' | 'dispose', listener: (...args: any[]) => void): this; public on(event: 'end', listener: (collected: Collection, reason: string) => void): this; - public once(event: 'collect', listener: (...args: any[]) => void): this; - public once(event: 'dispose', listener: (...args: any[]) => void): this; + public once(event: 'collect' | 'dispose', listener: (...args: any[]) => void): this; public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; } @@ -1006,11 +998,9 @@ declare module 'discord.js' { public fetch(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; - public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - public reply(content?: StringResolvable, options?: MessageOptions & { split?: false; } | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions | MessageOptions & { split?: false; } | MessageAdditions): Promise; public reply(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; - public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - public reply(options?: MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions | MessageAdditions | APIMessage | MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; public reply(options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; public suppressEmbeds(suppress?: boolean): Promise; public toJSON(): object; @@ -1185,17 +1175,13 @@ declare module 'discord.js' { public empty(): void; public endReason(): string | null; - public on(event: 'collect', listener: (reaction: MessageReaction, user: User) => void): this; - public on(event: 'dispose', listener: (reaction: MessageReaction, user: User) => void): this; + public on(event: 'collect' | 'dispose' | 'remove', listener: (reaction: MessageReaction, user: User) => void): this; public on(event: 'end', listener: (collected: Collection, reason: string) => void): this; - public on(event: 'remove', listener: (reaction: MessageReaction, user: User) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'collect', listener: (reaction: MessageReaction, user: User) => void): this; - public once(event: 'dispose', listener: (reaction: MessageReaction, user: User) => void): this; + public once(event: 'collect' | 'dispose' | 'remove', listener: (reaction: MessageReaction, user: User) => void): this; public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; - public once(event: 'remove', listener: (reaction: MessageReaction, user: User) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class ReactionEmoji extends Emoji { @@ -1252,7 +1238,7 @@ declare module 'discord.js' { export class Shard extends EventEmitter { constructor(manager: ShardingManager, id: number); private _evals: Map>; - private _exitListener: Function; + private _exitListener: (...args: any[]) => void; private _fetches: Map>; private _handleExit(respawn?: boolean): void; private _handleMessage(message: any): void; @@ -1273,19 +1259,17 @@ declare module 'discord.js' { public send(message: any): Promise; public spawn(spawnTimeout?: number): Promise; - public on(event: 'death', listener: (child: ChildProcess) => void): this; + public on(event: 'spawn' | 'death', listener: (child: ChildProcess) => void): this; public on(event: 'disconnect' | 'ready' | 'reconnecting', listener: () => void): this; public on(event: 'error', listener: (error: Error) => void): this; public on(event: 'message', listener: (message: any) => void): this; - public on(event: 'spawn', listener: (child: ChildProcess) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'death', listener: (child: ChildProcess) => void): this; + public once(event: 'spawn' | 'death', listener: (child: ChildProcess) => void): this; public once(event: 'disconnect' | 'ready' | 'reconnecting', listener: () => void): this; public once(event: 'error', listener: (error: Error) => void): this; public once(event: 'message', listener: (message: any) => void): this; - public once(event: 'spawn', listener: (child: ChildProcess) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class ShardClientUtil { @@ -1341,7 +1325,7 @@ declare module 'discord.js' { public static generate(timestamp?: number | Date): Snowflake; } - const VolumeMixin: (base: Constructable) => Constructable; + function VolumeMixin(base: Constructable): Constructable; class StreamDispatcher extends VolumeMixin(Writable) { constructor(player: object, options?: StreamOptions, streams?: object); @@ -1360,31 +1344,21 @@ declare module 'discord.js' { public pause(silence?: boolean): void; public resume(): void; - public on(event: 'close', listener: () => void): this; + public on(event: 'close' | 'drain' | 'end' | 'finish' | 'start', listener: () => void): this; public on(event: 'debug', listener: (info: string) => void): this; - public on(event: 'drain', listener: () => void): this; - public on(event: 'end', listener: () => void): this; public on(event: 'error', listener: (err: Error) => void): this; - public on(event: 'finish', listener: () => void): this; - public on(event: 'pipe', listener: (src: Readable) => void): this; - public on(event: 'start', listener: () => void): this; + public on(event: 'pipe' | 'unpipe', listener: (src: Readable) => void): this; public on(event: 'speaking', listener: (speaking: boolean) => void): this; - public on(event: 'unpipe', listener: (src: Readable) => void): this; public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'close', listener: () => void): this; + public once(event: 'close' | 'drain' | 'end' | 'finish' | 'start', listener: () => void): this; public once(event: 'debug', listener: (info: string) => void): this; - public once(event: 'drain', listener: () => void): this; - public once(event: 'end', listener: () => void): this; public once(event: 'error', listener: (err: Error) => void): this; - public once(event: 'finish', listener: () => void): this; - public once(event: 'pipe', listener: (src: Readable) => void): this; - public once(event: 'start', listener: () => void): this; + public once(event: 'pipe' | 'unpipe', listener: (src: Readable) => void): this; public once(event: 'speaking', listener: (speaking: boolean) => void): this; - public once(event: 'unpipe', listener: (src: Readable) => void): this; - public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; - public once(event: string, listener: Function): this; + public once(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class Speaking extends BitField { @@ -1393,10 +1367,10 @@ declare module 'discord.js' { } export class Structures { - static get(structure: K): Extendable[K]; - static get(structure: string): Function; - static extend(structure: K, extender: (baseClass: Extendable[K]) => T): T; - static extend(structure: string, extender: (baseClass: typeof Function) => T): T; + public static get(structure: K): Extendable[K]; + public static get(structure: string): (...args: any[]) => void; + public static extend(structure: K, extender: (baseClass: Extendable[K]) => T): T; + public static extend void>(structure: string, extender: (baseClass: typeof Function) => T): T; } export class SystemChannelFlags extends BitField { @@ -1462,7 +1436,7 @@ declare module 'discord.js' { public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; public static discordSort(collection: Collection): Collection; - public static escapeMarkdown(text: string, options?: { codeBlock?: boolean; inlineCode?: boolean; bold?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean; spoiler?: boolean; inlineCodeContent?: boolean; codeBlockContent?: boolean; }): string; + public static escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string; public static escapeCodeBlock(text: string): string; public static escapeInlineCode(text: string): string; public static escapeBold(text: string): string; @@ -1502,17 +1476,15 @@ declare module 'discord.js' { public on(event: 'end', listener: () => void): this; public on(event: 'error', listener: (error: Error) => void): this; - public on(event: 'subscribe', listener: (dispatcher: StreamDispatcher) => void): this; - public on(event: 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public on(event: 'subscribe' | 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; public on(event: 'warn', listener: (warning: string | Error) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; public once(event: 'end', listener: () => void): this; public once(event: 'error', listener: (error: Error) => void): this; - public once(event: 'subscribe', listener: (dispatcher: StreamDispatcher) => void): this; - public once(event: 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public once(event: 'subscribe' | 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; public once(event: 'warn', listener: (warning: string | Error) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class VoiceChannel extends GuildChannel { @@ -1563,31 +1535,19 @@ declare module 'discord.js' { public disconnect(): void; public play(input: VoiceBroadcast | Readable | string, options?: StreamOptions): StreamDispatcher; - public on(event: 'authenticated', listener: () => void): this; - public on(event: 'closing', listener: () => void): this; + public on(event: 'authenticated' | 'closing' | 'newSession' | 'ready' | 'reconnecting', listener: () => void): this; public on(event: 'debug', listener: (message: string) => void): this; - public on(event: 'disconnect', listener: (error: Error) => void): this; - public on(event: 'error', listener: (error: Error) => void): this; - public on(event: 'failed', listener: (error: Error) => void): this; - public on(event: 'newSession', listener: () => void): this; - public on(event: 'ready', listener: () => void): this; - public on(event: 'reconnecting', listener: () => void): this; + public on(event: 'error' | 'failed' | 'disconnect', listener: (error: Error) => void): this; public on(event: 'speaking', listener: (user: User, speaking: Readonly) => void): this; public on(event: 'warn', listener: (warning: string | Error) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'authenticated', listener: () => void): this; - public once(event: 'closing', listener: () => void): this; + public once(event: 'authenticated' | 'closing' | 'newSession' | 'ready' | 'reconnecting', listener: () => void): this; public once(event: 'debug', listener: (message: string) => void): this; - public once(event: 'disconnect', listener: (error: Error) => void): this; - public once(event: 'error', listener: (error: Error) => void): this; - public once(event: 'failed', listener: (error: Error) => void): this; - public once(event: 'newSession', listener: () => void): this; - public once(event: 'ready', listener: () => void): this; - public once(event: 'reconnecting', listener: () => void): this; + public once(event: 'error' | 'failed' | 'disconnect', listener: (error: Error) => void): this; public once(event: 'speaking', listener: (user: User, speaking: Readonly) => void): this; public once(event: 'warn', listener: (warning: string | Error) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } class VoiceReceiver extends EventEmitter { @@ -1595,10 +1555,10 @@ declare module 'discord.js' { public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm'; end?: 'silence' | 'manual'; }): Readable; public on(event: 'debug', listener: (error: Error | string) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; public once(event: 'debug', listener: (error: Error | string) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class VoiceRegion { @@ -1741,19 +1701,15 @@ declare module 'discord.js' { private _emitDestroyed(): void; public send(data: object): void; - public on(event: 'ready', listener: () => void): this; - public on(event: 'resumed', listener: () => void): this; + public on(event: 'ready' | 'resumed' | 'invalidSession', listener: () => void): this; public on(event: 'close', listener: (event: CloseEvent) => void): this; - public on(event: 'invalidSession', listener: () => void): this; public on(event: 'allReady', listener: (unavailableGuilds?: Set) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'ready', listener: () => void): this; - public once(event: 'resumed', listener: () => void): this; + public once(event: 'ready' | 'resumed' | 'invalidSession', listener: () => void): this; public once(event: 'close', listener: (event: CloseEvent) => void): this; - public once(event: 'invalidSession', listener: () => void): this; public once(event: 'allReady', listener: (unavailableGuilds?: Set) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } //#endregion @@ -1761,12 +1717,10 @@ declare module 'discord.js' { //#region Collections export class Collection extends BaseCollection { - public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; - public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; - public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; - public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; + public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; + public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; public toJSON(): object; } @@ -1914,8 +1868,8 @@ declare module 'discord.js' { // to each of those classes type Constructable = new (...args: any[]) => T; - const PartialTextBasedChannel: (Base?: Constructable) => Constructable; - const TextBasedChannel: (Base?: Constructable) => Constructable; + function PartialTextBasedChannel(Base?: Constructable): Constructable; + function TextBasedChannel(Base?: Constructable): Constructable; interface PartialTextBasedChannelFields { lastMessageID: Snowflake | null; @@ -1923,11 +1877,9 @@ declare module 'discord.js' { readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date; - send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - send(content?: StringResolvable, options?: MessageOptions & { split?: false; } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions | MessageAdditions | MessageOptions & { split?: false; } | MessageAdditions): Promise; send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; - send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - send(options?: MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions | MessageAdditions | APIMessage | MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; send(options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; } @@ -1941,7 +1893,7 @@ declare module 'discord.js' { stopTyping(force?: boolean): void; } - const WebhookMixin: (Base?: Constructable) => Constructable; + function WebhookMixin(Base?: Constructable): Constructable; interface WebhookFields { readonly client: Client; @@ -2787,9 +2739,25 @@ declare module 'discord.js' { | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE'; - type MessageEvent = { data: WebSocket.Data; type: string; target: WebSocket; }; - type CloseEvent = { wasClean: boolean; code: number; reason: string; target: WebSocket; }; - type ErrorEvent = { error: any, message: string, type: string, target: WebSocket; }; + interface MessageEvent { + data: WebSocket.Data; + type: string; + target: WebSocket; + } + + interface CloseEvent { + wasClean: boolean; + code: number; + reason: string; + target: WebSocket; + } + + interface ErrorEvent { + error: any; + message: string; + type: string; + target: WebSocket; + } interface CrosspostedChannel { channelID: Snowflake; @@ -2798,5 +2766,17 @@ declare module 'discord.js' { name: string; } + interface EscapeMarkdownOptions { + codeBlock?: boolean; + inlineCode?: boolean; + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + spoiler?: boolean; + inlineCodeContent?: boolean; + codeBlockContent?: boolean; + } + //#endregion } From 0a1b9a52853213fe9e6dc14a4a8f11b1ec5f9056 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:15:38 +0000 Subject: [PATCH 402/428] refactor: remove unused error in catch statements (#3820) * refactor(handlers): remove unused error in catch * refactor(PacketHandler): remove unused error * refactor(SecretBox): remove unused error * refactor(ClientPresence): remove unused error * style: remove space Co-Authored-By: Crawl Co-authored-by: Crawl --- src/client/voice/receiver/PacketHandler.js | 2 +- src/client/voice/util/Secretbox.js | 2 +- src/client/websocket/handlers/index.js | 2 +- src/structures/ClientPresence.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index d468abab..3825b2ac 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -90,7 +90,7 @@ class PacketHandler extends EventEmitter { this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: 0 }); this.receiver.connection.client.clearTimeout(speakingTimeout); this.speakingTimeouts.delete(ssrc); - } catch (ex) { + } catch { // Connection already closed, ignore } }, DISCORD_SPEAKING_DELAY); diff --git a/src/client/voice/util/Secretbox.js b/src/client/voice/util/Secretbox.js index 1b30eeb6..c16a4353 100644 --- a/src/client/voice/util/Secretbox.js +++ b/src/client/voice/util/Secretbox.js @@ -27,6 +27,6 @@ exports.methods = {}; if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; // eslint-disable-line no-await-in-loop exports.methods = libs[libName](lib); break; - } catch (err) {} // eslint-disable-line no-empty + } catch {} // eslint-disable-line no-empty } })(); diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js index b253cefe..d69c105f 100644 --- a/src/client/websocket/handlers/index.js +++ b/src/client/websocket/handlers/index.js @@ -7,7 +7,7 @@ const handlers = {}; for (const name of Object.keys(WSEvents)) { try { handlers[name] = require(`./${name}.js`); - } catch (err) {} // eslint-disable-line no-empty + } catch {} // eslint-disable-line no-empty } module.exports = handlers; diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 213b46f2..3fdfd5b2 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -39,7 +39,7 @@ class ClientPresence extends Presence { try { const a = await this.client.api.oauth2.applications(applicationID).assets.get(); for (const asset of a) assets.set(asset.name, asset.id); - } catch (err) { } // eslint-disable-line no-empty + } catch {} // eslint-disable-line no-empty } } From a69ebbe9d957a21d165caf0582c0640e501908e9 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 24 Feb 2020 18:16:20 +0100 Subject: [PATCH 403/428] feat/fix(GuildAuditLogs): handle new event types (#3602) * feat/fix(GuildAuditLogs): handle new event types * fix(GuildAuditLogsEntry): coerce to numbers, simplify extra for deleted entities * fix(GuildAuditLogsEntry): do not revert 'type' extra --- src/structures/GuildAuditLogs.js | 109 ++++++++++++++++++++++++------- typings/index.d.ts | 5 +- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index a6327f35..7a83d495 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -1,10 +1,11 @@ 'use strict'; const Collection = require('../util/Collection'); +const Integration = require('./Integration'); const Snowflake = require('../util/Snowflake'); const Webhook = require('./Webhook'); const Util = require('../util/Util'); -const PartialTypes = require('../util/Constants'); +const { PartialTypes } = require('../util/Constants'); /** * The target type of an entry, e.g. `GUILD`. Here are the available types: @@ -16,6 +17,7 @@ const PartialTypes = require('../util/Constants'); * * WEBHOOK * * EMOJI * * MESSAGE + * * INTEGRATION * @typedef {string} AuditLogTargetType */ @@ -34,6 +36,7 @@ const Targets = { WEBHOOK: 'WEBHOOK', EMOJI: 'EMOJI', MESSAGE: 'MESSAGE', + INTEGRATION: 'INTEGRATION', UNKNOWN: 'UNKNOWN', }; @@ -141,6 +144,18 @@ class GuildAuditLogs { } } + /** + * Cached integrations + * @type {Collection} + * @private + */ + this.integrations = new Collection(); + if (data.integrations) { + for (const integration of data.integrations) { + this.integrations.set(integration.id, new Integration(guild.client, integration, guild)); + } + } + /** * The entries for this guild's audit logs * @type {Collection} @@ -169,9 +184,10 @@ class GuildAuditLogs { * * An emoji * * An invite * * A webhook + * * An integration * * An object with an id key if target was deleted * * An object where the keys represent either the new value or the old value - * @typedef {?Object|Guild|User|Role|GuildEmoji|Invite|Webhook} AuditLogEntryTarget + * @typedef {?Object|Guild|User|Role|GuildEmoji|Invite|Webhook|Integration} AuditLogEntryTarget */ /** @@ -188,6 +204,7 @@ class GuildAuditLogs { if (target < 60) return Targets.WEBHOOK; if (target < 70) return Targets.EMOJI; if (target < 80) return Targets.MESSAGE; + if (target < 90) return Targets.INTEGRATION; return Targets.UNKNOWN; } @@ -210,10 +227,13 @@ class GuildAuditLogs { Actions.CHANNEL_CREATE, Actions.CHANNEL_OVERWRITE_CREATE, Actions.MEMBER_BAN_REMOVE, + Actions.BOT_ADD, Actions.ROLE_CREATE, Actions.INVITE_CREATE, Actions.WEBHOOK_CREATE, Actions.EMOJI_CREATE, + Actions.MESSAGE_PIN, + Actions.INTEGRATION_CREATE, ].includes(action)) return 'CREATE'; if ([ @@ -222,11 +242,15 @@ class GuildAuditLogs { Actions.MEMBER_KICK, Actions.MEMBER_PRUNE, Actions.MEMBER_BAN_ADD, + Actions.MEMBER_DISCONNECT, Actions.ROLE_DELETE, Actions.INVITE_DELETE, Actions.WEBHOOK_DELETE, Actions.EMOJI_DELETE, Actions.MESSAGE_DELETE, + Actions.MESSAGE_BULK_DELETE, + Actions.MESSAGE_UNPIN, + Actions.INTEGRATION_DELETE, ].includes(action)) return 'DELETE'; if ([ @@ -235,10 +259,12 @@ class GuildAuditLogs { Actions.CHANNEL_OVERWRITE_UPDATE, Actions.MEMBER_UPDATE, Actions.MEMBER_ROLE_UPDATE, + Actions.MEMBER_MOVE, Actions.ROLE_UPDATE, Actions.INVITE_UPDATE, Actions.WEBHOOK_UPDATE, Actions.EMOJI_UPDATE, + Actions.INTEGRATION_UPDATE, ].includes(action)) return 'UPDATE'; return 'ALL'; @@ -312,49 +338,73 @@ class GuildAuditLogsEntry { * @type {?Object|Role|GuildMember} */ this.extra = null; - if (data.options) { - if (data.action_type === Actions.MEMBER_PRUNE) { + switch (data.action_type) { + case Actions.MEMBER_PRUNE: this.extra = { - removed: data.options.members_removed, - days: data.options.delete_member_days, + removed: Number(data.options.members_removed), + days: Number(data.options.delete_member_days), }; - } else if (data.action_type === Actions.MESSAGE_DELETE) { + break; + + case Actions.MEMBER_MOVE: + case Actions.MESSAGE_DELETE: + case Actions.MESSAGE_BULK_DELETE: this.extra = { - count: data.options.count, - channel: guild.channels.cache.get(data.options.channel_id), + channel: guild.channels.cache.get(data.options.channel_id) || { id: data.options.channel_id }, + count: Number(data.options.count), }; - } else if (data.action_type === Actions.MESSAGE_BULK_DELETE) { + break; + + case Actions.MESSAGE_PIN: + case Actions.MESSAGE_UNPIN: this.extra = { - count: data.options.count, + channel: guild.client.channels.cache.get(data.options.channel_id) || { id: data.options.channel_id }, + messageID: data.options.message_id, }; - } else { + break; + + case Actions.MEMBER_DISCONNECT: + this.extra = { + count: Number(data.options.count), + }; + break; + + case Actions.CHANNEL_OVERWRITE_CREATE: + case Actions.CHANNEL_OVERWRITE_UPDATE: + case Actions.CHANNEL_OVERWRITE_DELETE: switch (data.options.type) { case 'member': - this.extra = guild.members.cache.get(data.options.id); - if (!this.extra) this.extra = { id: data.options.id }; + this.extra = guild.members.cache.get(data.options.id) || + { id: data.options.id, type: 'member' }; break; + case 'role': - this.extra = guild.roles.cache.get(data.options.id); - if (!this.extra) this.extra = { id: data.options.id, name: data.options.role_name }; + this.extra = guild.roles.cache.get(data.options.id) || + { id: data.options.id, name: data.options.role_name, type: 'role' }; break; + default: break; } - } + break; + + default: + break; } - + /** + * The target of this entry + * @type {?AuditLogEntryTarget} + */ + this.target = null; if (targetType === Targets.UNKNOWN) { - /** - * The target of this entry - * @type {AuditLogEntryTarget} - */ this.target = this.changes.reduce((o, c) => { o[c.key] = c.new || c.old; return o; }, {}); this.target.id = data.target_id; - } else if (targetType === Targets.USER) { + // MEMBER_DISCONNECT and similar types do not provide a target_id. + } else if (targetType === Targets.USER && data.target_id) { this.target = guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users.add({ id: data.target_id }) : guild.client.users.cache.get(data.target_id); @@ -386,8 +436,17 @@ class GuildAuditLogsEntry { } }); } else if (targetType === Targets.MESSAGE) { - this.target = guild.client.users.cache.get(data.target_id); - } else { + // Discord sends a channel id for the MESSAGE_BULK_DELETE action type. + this.target = data.action_type === Actions.MESSAGE_BULK_DELETE ? + guild.channels.cache.get(data.target_id) || { id: data.target_id } : + guild.client.users.cache.get(data.target_id); + } else if (targetType === Targets.INTEGRATION) { + this.target = logs.integrations.get(data.target_id) || + new Integration(guild.client, this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, { id: data.target_id }), guild); + } else if (data.target_id) { this.target = guild[`${targetType.toLowerCase()}s`].cache.get(data.target_id) || { id: data.target_id }; } } diff --git a/typings/index.d.ts b/typings/index.d.ts index d16180e6..0788fd05 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -765,6 +765,7 @@ declare module 'discord.js' { export class GuildAuditLogs { constructor(guild: Guild, data: object); private webhooks: Collection; + private integrations: Collection; public entries: Collection; @@ -788,7 +789,7 @@ declare module 'discord.js' { public extra: object | Role | GuildMember | null; public id: Snowflake; public reason: string | null; - public target: Guild | User | Role | GuildEmoji | Invite | Webhook; + public target: Guild | User | Role | GuildEmoji | Invite | Webhook | Integration | null; public targetType: GuildAuditLogsTarget; public toJSON(): object; } @@ -2258,6 +2259,8 @@ declare module 'discord.js' { WEBHOOK?: string; EMOJI?: string; MESSAGE?: string; + INTEGRATION?: string; + UNKNOWN?: string; } type GuildChannelResolvable = Snowflake | GuildChannel; From acf724e691678998cb6774846454913a183c9614 Mon Sep 17 00:00:00 2001 From: matthewfripp <50251454+matthewfripp@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:17:24 +0000 Subject: [PATCH 404/428] feat(Collector): Addition of resetTimer() (#3825) * feat(Collector): Addition of resetTimer() * typings --- src/structures/interfaces/Collector.js | 17 +++++++++++++++++ typings/index.d.ts | 1 + 2 files changed, 18 insertions(+) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 3cd5d387..6231605e 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -188,6 +188,23 @@ class Collector extends EventEmitter { this.emit('end', this.collected, reason); } + /** + * Resets the collectors timeout and idle timer. + * @param {Object} [options] Options + * @param {number} [options.time] How long to run the collector for in milliseconds + * @param {number} [options.idle] How long to stop the collector after inactivity in milliseconds + */ + resetTimer({ time, idle } = {}) { + if (this._timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = this.client.setTimeout(() => this.stop('time'), time || this.options.time); + } + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = this.client.setTimeout(() => this.stop('idle'), idle || this.options.idle); + } + } + /** * Checks whether the collector should end, and if so, ends it. */ diff --git a/typings/index.d.ts b/typings/index.d.ts index 0788fd05..1b12e82d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -337,6 +337,7 @@ declare module 'discord.js' { public handleCollect(...args: any[]): void; public handleDispose(...args: any[]): void; public stop(reason?: string): void; + public resetTimer(options?: { time?: number, idle?: number }): void; public [Symbol.asyncIterator](): AsyncIterableIterator; public toJSON(): object; From 52c0a4067b4950aaa655488b112ddfd51c06264b Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:21:29 -0500 Subject: [PATCH 405/428] fix(MessageEmbed): various typos and fixes (#3819) * fix: typo * fix: couple more typos * fix: grammar stuff * fix: EmbedField takes StringResolvable not string * Revert "fix: EmbedField takes StringResolvable not string" This reverts commit c1bdd78ad378a4c2e2f9772753c24aaab0c2d910. Co-authored-by: Crawl --- src/structures/MessageEmbed.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 7cfb6d1a..b5ce08b2 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -215,7 +215,7 @@ class MessageEmbed { } /** - * Adds a fields to the embed (max 25). + * Adds fields to the embed (max 25). * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add * @returns {MessageEmbed} */ @@ -372,7 +372,7 @@ class MessageEmbed { } /** - * Checks for valid field input and resolves strings + * Normalizes field input and resolves strings. * @param {StringResolvable} name The name of the field * @param {StringResolvable} value The value of the field * @param {boolean} [inline=false] Set the field to display inline @@ -394,7 +394,7 @@ class MessageEmbed { */ /** - * Check for valid field input and resolves strings + * Normalizes field input and resolves strings. * @param {...EmbedFieldData|EmbedFieldData[]} fields Fields to normalize * @returns {EmbedField[]} */ From 02807347e7e899f476780d8632ddd62bffae11fa Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:27:15 +0000 Subject: [PATCH 406/428] fix: Client#sweepMessages should throw an INVALID_TYPE error (#3828) * fix(Client): sweepMessages shouldn't shrow an invalid client option error * style: trailing commas --- src/client/Client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index ae7bbf8b..ec512886 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -64,7 +64,7 @@ class Client extends BaseClient { if (Array.isArray(this.options.shards)) { this.options.shards = [...new Set( - this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)) + this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)), )]; } @@ -199,7 +199,7 @@ class Client extends BaseClient { if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); this.emit(Events.DEBUG, - `Provided token: ${token.split('.').map((val, i) => i > 1 ? val.replace(/./g, '*') : val).join('.')}` + `Provided token: ${token.split('.').map((val, i) => i > 1 ? val.replace(/./g, '*') : val).join('.')}`, ); if (this.options.presence) { @@ -286,7 +286,7 @@ class Client extends BaseClient { */ sweepMessages(lifetime = this.options.messageCacheLifetime) { if (typeof lifetime !== 'number' || isNaN(lifetime)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'Lifetime', 'a number'); + throw new TypeError('INVALID_TYPE', 'lifetime', 'number'); } if (lifetime <= 0) { this.emit(Events.DEBUG, 'Didn\'t sweep messages - lifetime is unlimited'); @@ -303,7 +303,7 @@ class Client extends BaseClient { channels++; messages += channel.messages.cache.sweep( - message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs + message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs, ); } From 91a025caaaadb49a4f6610ef1ff2f06d69784987 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:27:34 +0000 Subject: [PATCH 407/428] feat: GuildEmoji & Invite to GuildResolvable (#3637) * Add GuildEmoji to GuildResolvable * Add GuildEmoji to GuildResolvable * Add Invite to GuildResolvable * Add Invite to GuildResolvable * oops * oops x2 * Add Guild#fetchBan and an error for not resolving the ID * typings * Revert "Add Guild#fetchBan and an error for not resolving the ID" This reverts commit a4d0ed16e788beb18074cfc6f0cc72f27c325d56. * Revert "typings" This reverts commit 5a54e88785f5284b49ab96263134bfebb05df4e0. * fix jsdoc * add trailing comma --- src/managers/GuildManager.js | 16 ++++++++++++---- typings/index.d.ts | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js index c2747580..8a28e06b 100644 --- a/src/managers/GuildManager.js +++ b/src/managers/GuildManager.js @@ -6,6 +6,8 @@ const { Events } = require('../util/Constants'); const Guild = require('../structures/Guild'); const GuildChannel = require('../structures/GuildChannel'); const GuildMember = require('../structures/GuildMember'); +const GuildEmoji = require('../structures/GuildEmoji'); +const Invite = require('../structures/Invite'); const Role = require('../structures/Role'); /** @@ -27,9 +29,11 @@ class GuildManager extends BaseManager { * Data that resolves to give a Guild object. This can be: * * A Guild object * * A GuildChannel object + * * A GuildEmoji object * * A Role object * * A Snowflake - * @typedef {Guild|GuildChannel|GuildMember|Role|Snowflake} GuildResolvable + * * An Invite object + * @typedef {Guild|GuildChannel|GuildMember|GuildEmoji|Role|Snowflake|Invite} GuildResolvable */ /** @@ -43,7 +47,9 @@ class GuildManager extends BaseManager { resolve(guild) { if (guild instanceof GuildChannel || guild instanceof GuildMember || - guild instanceof Role) return super.resolve(guild.guild); + guild instanceof GuildEmoji || + guild instanceof Role || + (guild instanceof Invite && guild.guild)) return super.resolve(guild.guild); return super.resolve(guild); } @@ -58,7 +64,9 @@ class GuildManager extends BaseManager { resolveID(guild) { if (guild instanceof GuildChannel || guild instanceof GuildMember || - guild instanceof Role) return super.resolveID(guild.guild.id); + guild instanceof GuildEmoji || + guild instanceof Role || + (guild instanceof Invite && guild.guild)) return super.resolveID(guild.guild.id); return super.resolveID(guild); } @@ -92,7 +100,7 @@ class GuildManager extends BaseManager { resolve(this.client.guilds.add(data)); }, 10000); return undefined; - }, reject) + }, reject), ); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 1b12e82d..ecfa6f1d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2332,7 +2332,7 @@ declare module 'discord.js' { type GuildMemberResolvable = GuildMember | UserResolvable; - type GuildResolvable = Guild | GuildChannel | GuildMember | Role | Snowflake; + type GuildResolvable = Guild | GuildChannel | GuildMember | GuildEmoji | Invite | Role | Snowflake; interface GuildPruneMembersOptions { count?: boolean; From 44ff67dc1192e4ca9aa1cf69390d0453c5302f0c Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 21:01:41 +0000 Subject: [PATCH 408/428] typings(WebhookClient): client is not a Client (#3829) * typings(WebhookClient): client is not a Client * style: use tabs --- typings/index.d.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index ecfa6f1d..605969f5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -303,13 +303,6 @@ declare module 'discord.js' { public toString(): string; } - export interface ActivityOptions { - name?: string; - url?: string; - type?: ActivityType | number; - shardID?: number | number[]; - } - export class ClientUser extends User { public mfaEnabled: boolean; public verified: boolean; @@ -1630,6 +1623,7 @@ declare module 'discord.js' { export class WebhookClient extends WebhookMixin(BaseClient) { constructor(id: string, token: string, options?: ClientOptions); public token: string; + public readonly client: this; } export class WebSocketManager extends EventEmitter { @@ -1923,6 +1917,13 @@ declare module 'discord.js' { | 'SYNC' | 'PLAY'; + interface ActivityOptions { + name?: string; + url?: string; + type?: ActivityType | number; + shardID?: number | number[]; + } + type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' From c1d396a6c4c063a70f28bda6498b1114837af6b0 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 22:44:46 +0100 Subject: [PATCH 409/428] Partial-revert "typings(WebhookClient): client is not a Client" (#3831) * Revert "typings(WebhookClient): client is not a Client (#3829)" This reverts commit 44ff67dc1192e4ca9aa1cf69390d0453c5302f0c. * Update index.d.ts * Update index.d.ts --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 605969f5..95b0790c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1623,7 +1623,6 @@ declare module 'discord.js' { export class WebhookClient extends WebhookMixin(BaseClient) { constructor(id: string, token: string, options?: ClientOptions); public token: string; - public readonly client: this; } export class WebSocketManager extends EventEmitter { From 3a0470b45c989a565e2dd3d827282f1a07cee6e1 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 23:14:31 +0100 Subject: [PATCH 410/428] chore(deps): update deps and fix lint (#3833) --- package.json | 30 +++++++++---------- src/client/voice/util/VolumeInterface.js | 2 +- src/client/websocket/handlers/GUILD_CREATE.js | 4 +-- src/rest/RequestHandler.js | 6 ++-- src/sharding/Shard.js | 4 +-- src/structures/Guild.js | 10 +++---- src/structures/GuildChannel.js | 1 + src/structures/Message.js | 8 ++--- src/structures/interfaces/TextBasedChannel.js | 4 +-- src/util/Snowflake.js | 2 +- src/util/Structures.js | 4 +-- src/util/Util.js | 2 +- 12 files changed, 39 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index f693c27f..721dc662 100644 --- a/package.json +++ b/package.json @@ -35,19 +35,19 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { - "@discordjs/collection": "^0.1.1", + "@discordjs/collection": "^0.1.5", "abort-controller": "^3.0.0", - "form-data": "^2.3.3", - "node-fetch": "^2.3.0", - "prism-media": "^1.0.0", + "form-data": "^3.0.0", + "node-fetch": "^2.6.0", + "prism-media": "^1.2.0", "setimmediate": "^1.0.5", - "tweetnacl": "^1.0.1", - "ws": "^7.2.0" + "tweetnacl": "^1.0.3", + "ws": "^7.2.1" }, "peerDependencies": { "bufferutil": "^4.0.1", "erlpack": "discordapp/erlpack", - "libsodium-wrappers": "^0.7.4", + "libsodium-wrappers": "^0.7.6", "sodium": "^3.0.2", "utf-8-validate": "^5.0.2", "zlib-sync": "^0.1.6" @@ -74,20 +74,20 @@ }, "devDependencies": { "@types/node": "^10.12.24", - "@types/ws": "^6.0.1", + "@types/ws": "^7.2.1", "discord.js-docgen": "discordjs/docgen", "dtslint": "^3.0.0", - "eslint": "^5.13.0", - "jest": "^24.7.1", + "eslint": "^6.8.0", + "jest": "^25.1.0", "json-filter-loader": "^1.0.0", "terser-webpack-plugin": "^1.2.2", - "tslint": "^5.12.1", - "typescript": "^3.3.3", - "webpack": "^4.29.3", - "webpack-cli": "^3.2.3" + "tslint": "^6.0.0", + "typescript": "^3.8.2", + "webpack": "^4.41.6", + "webpack-cli": "^3.3.11" }, "engines": { - "node": ">=11.0.0" + "node": ">=12.0.0" }, "browser": { "@discordjs/opus": false, diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index ba162a94..179d4fb7 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -106,7 +106,7 @@ exports.applyToClass = function applyToClass(structure) { Object.defineProperty( structure.prototype, prop, - Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop) + Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop), ); } }; diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index eb897443..9994d9f8 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -11,7 +11,7 @@ module.exports = async (client, { d: data }, shard) => { // If the client was ready before and we had unavailable guilds, fetch them if (client.ws.status === Status.READY && client.options.fetchAllMembers) { await guild.members.fetch().catch(err => - client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`) + client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`), ); } } @@ -27,7 +27,7 @@ module.exports = async (client, { d: data }, shard) => { */ if (client.options.fetchAllMembers) { await guild.members.fetch().catch(err => - client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`) + client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`), ); } client.emit(Events.GUILD_CREATE, guild); diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 8f650b42..8a8ecf9a 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -103,7 +103,7 @@ class RequestHandler { // NodeFetch error expected for all "operational" errors, such as 500 status code this.busy = false; return reject( - new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path) + new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path), ); } @@ -155,7 +155,7 @@ class RequestHandler { // Retry the specified number of times for possible serverside issues if (item.retries === this.manager.client.options.retryLimit) { return reject( - new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path) + new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path), ); } else { item.retries++; @@ -172,7 +172,7 @@ class RequestHandler { return null; } catch (err) { return reject( - new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path) + new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path), ); } } diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 04d224db..7e016fbe 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -294,7 +294,7 @@ class Shard extends EventEmitter { if (message._sFetchProp) { this.manager.fetchClientValues(message._sFetchProp).then( results => this.send({ _sFetchProp: message._sFetchProp, _result: results }), - err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }) + err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }), ); return; } @@ -303,7 +303,7 @@ class Shard extends EventEmitter { if (message._sEval) { this.manager.broadcastEval(message._sEval).then( results => this.send({ _sEval: message._sEval, _result: results }), - err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }) + err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }), ); return; } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index fc04530c..9353f4de 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -616,7 +616,7 @@ class Guild extends Base { user: this.client.users.add(ban.user), }); return collection; - }, new Collection()) + }, new Collection()), ); } @@ -634,7 +634,7 @@ class Guild extends Base { return this.client.api.guilds(this.id).integrations.get().then(data => data.reduce((collection, integration) => collection.set(integration.id, new Integration(this.client, integration, this)), - new Collection()) + new Collection()), ); } @@ -1086,7 +1086,7 @@ class Guild extends Base { this.client.actions.GuildChannelsPositionUpdate.handle({ guild_id: this.id, channels: updatedChannels, - }).guild + }).guild, ); } @@ -1120,7 +1120,7 @@ class Guild extends Base { this.client.actions.GuildRolePositionUpdate.handle({ guild_id: this.id, roles: rolePositions, - }).guild + }).guild, ); } @@ -1250,7 +1250,7 @@ class Guild extends Base { _sortedChannels(channel) { const category = channel.type === ChannelTypes.CATEGORY; return Util.discordSort(this.channels.cache.filter(c => - c.type === channel.type && (category || c.parent === channel.parent) + c.type === channel.type && (category || c.parent === channel.parent), )); } } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 649e5b22..35a0bc2a 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -375,6 +375,7 @@ class GuildChannel extends Channel { */ setParent(channel, { lockPermissions = true, reason } = {}) { return this.edit({ + // eslint-disable-next-line no-prototype-builtins parentID: channel !== null ? channel.hasOwnProperty('id') ? channel.id : channel : null, lockPermissions, }, reason); diff --git a/src/structures/Message.js b/src/structures/Message.js index 0c856fda..d32e1c01 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -108,7 +108,7 @@ class Message extends Base { if (data.attachments) { for (const attachment of data.attachments) { this.attachments.set(attachment.id, new MessageAttachment( - attachment.url, attachment.filename, attachment + attachment.url, attachment.filename, attachment, )); } } @@ -230,7 +230,7 @@ class Message extends Base { this.attachments = new Collection(); for (const attachment of data.attachments) { this.attachments.set(attachment.id, new MessageAttachment( - attachment.url, attachment.filename, attachment + attachment.url, attachment.filename, attachment, )); } } else { @@ -242,7 +242,7 @@ class Message extends Base { 'mentions' in data ? data.mentions : this.mentions.users, 'mentions_roles' in data ? data.mentions_roles : this.mentions.roles, 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone, - 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels + 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels, ); this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); @@ -511,7 +511,7 @@ class Message extends Base { reply(content, options) { return this.channel.send(content instanceof APIMessage ? content : - APIMessage.transformOptions(content, options, { reply: this.member || this.author }) + APIMessage.transformOptions(content, options, { reply: this.member || this.author }), ); } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 4106e00d..0fe0e2dc 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -300,7 +300,7 @@ class TextBasedChannel { let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id || m); if (filterOld) { messageIDs = messageIDs.filter(id => - Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000 + Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000, ); } if (messageIDs.length === 0) return new Collection(); @@ -336,7 +336,7 @@ class TextBasedChannel { 'typing', 'typingCount', 'createMessageCollector', - 'awaitMessages' + 'awaitMessages', ); } for (const prop of props) { diff --git a/src/util/Snowflake.js b/src/util/Snowflake.js index 06612c30..71090adc 100644 --- a/src/util/Snowflake.js +++ b/src/util/Snowflake.js @@ -36,7 +36,7 @@ class SnowflakeUtil { if (timestamp instanceof Date) timestamp = timestamp.getTime(); if (typeof timestamp !== 'number' || isNaN(timestamp)) { throw new TypeError( - `"timestamp" argument must be a number (received ${isNaN(timestamp) ? 'NaN' : typeof timestamp})` + `"timestamp" argument must be a number (received ${isNaN(timestamp) ? 'NaN' : typeof timestamp})`, ); } if (INCREMENT >= 4095) INCREMENT = 0; diff --git a/src/util/Structures.js b/src/util/Structures.js index c4061f62..fdbab069 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -45,7 +45,7 @@ class Structures { 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}.` + `"extender" argument must be a function that returns the extended structure class/prototype ${received}.`, ); } @@ -60,7 +60,7 @@ class Structures { const received = `${extended.name || 'unnamed'}${prototype.name ? ` extends ${prototype.name}` : ''}`; throw new Error( 'The class/prototype returned from the extender function must extend the existing structure class/prototype' + - ` (received function ${received}; expected extension of ${structures[structure].name}).` + ` (received function ${received}; expected extension of ${structures[structure].name}).`, ); } diff --git a/src/util/Util.js b/src/util/Util.js index 4ab51877..6e878f34 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -437,7 +437,7 @@ class Util { return collection.sorted((a, b) => a.rawPosition - b.rawPosition || parseInt(b.id.slice(0, -10)) - parseInt(a.id.slice(0, -10)) || - parseInt(b.id.slice(10)) - parseInt(a.id.slice(10)) + parseInt(b.id.slice(10)) - parseInt(a.id.slice(10)), ); } From c4bda746c893638907e6fa37f3c8a3f3d390ca73 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 23:32:12 +0100 Subject: [PATCH 411/428] chore(githooks): husky (#3835) --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 721dc662..123f1d7b 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "discord.js-docgen": "discordjs/docgen", "dtslint": "^3.0.0", "eslint": "^6.8.0", + "husky": "^4.2.3", "jest": "^25.1.0", "json-filter-loader": "^1.0.0", "terser-webpack-plugin": "^1.2.2", @@ -120,5 +121,10 @@ "src/client/voice/util/Secretbox.js": false, "src/client/voice/util/Silence.js": false, "src/client/voice/util/VolumeInterface.js": false + }, + "husky": { + "hooks": { + "pre-commit": "npm test" + } } } From 9cb306c823216130e5bddc268ba3aee025a5b98f Mon Sep 17 00:00:00 2001 From: Timo <30553356+y21@users.noreply.github.com> Date: Wed, 26 Feb 2020 12:13:23 +0100 Subject: [PATCH 412/428] feat: replace disableEveryone with disableMentions (#3830) * add ClientOptions#disableMentions and MessageOptions#disableMentions * provide tests * don't sanitize controlled mentions * add @here mentions to tests * fix indents (6 spaces instead of 8) * add Util#cleanContent tests * add typings for removeMentions * replace @ with @\u200b AFTER cleaning content as suggested instead of using removeMentions * better explanation of this option * no newline in Util.removeMentions * fix long line * remove double space * remove comments (change has been reverted) * Use Util.removeMentions to remove mentions * use Util.removeMentions in Util.cleanContent --- src/client/Client.js | 4 +- src/structures/APIMessage.js | 14 +++--- src/structures/Webhook.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 4 +- src/util/Constants.js | 4 +- src/util/Util.js | 14 ++++-- test/disableMentions.js | 47 +++++++++++++++++++ typings/index.d.ts | 7 +-- 8 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 test/disableMentions.js diff --git a/src/client/Client.js b/src/client/Client.js index ec512886..e2abbfa7 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -386,8 +386,8 @@ class Client extends BaseClient { if (typeof options.fetchAllMembers !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean'); } - if (typeof options.disableEveryone !== 'boolean') { - throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean'); + if (typeof options.disableMentions !== 'boolean') { + throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a boolean'); } if (!Array.isArray(options.partials)) { throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index b05fb68f..3a52a460 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -88,6 +88,13 @@ class APIMessage { content = Util.resolveString(this.options.content); } + const disableMentions = typeof this.options.disableMentions === 'undefined' ? + this.target.client.options.disableMentions : + this.options.disableMentions; + if (disableMentions) { + content = Util.removeMentions(content || ''); + } + const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; const splitOptions = isSplit ? { ...this.options.split } : undefined; @@ -113,13 +120,6 @@ class APIMessage { content = `${mentionPart}${content || ''}`; } - const disableEveryone = typeof this.options.disableEveryone === 'undefined' ? - this.target.client.options.disableEveryone : - this.options.disableEveryone; - if (disableEveryone) { - content = (content || '').replace(/@(everyone|here)/g, '@\u200b$1'); - } - if (isSplit) { content = Util.splitMessage(content || '', splitOptions); } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index b2960704..c7604599 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -85,8 +85,8 @@ class Webhook { * @property {string} [nonce=''] The nonce for the message * @property {Object[]} [embeds] An array of embeds for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here - * should be replaced with plain-text + * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space + * should be placed after every @ character to prevent unexpected mentions * @property {FileOptions[]|string[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 0fe0e2dc..38a9dcb3 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -57,8 +57,8 @@ class TextBasedChannel { * @property {string} [content=''] The content for the message * @property {MessageEmbed|Object} [embed] An embed for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here - * should be replaced with plain-text + * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space + * should be placed after every @ character to prevent unexpected mentions * @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/util/Constants.js b/src/util/Constants.js index 2bbe580d..b38e0b58 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -21,7 +21,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * the message cache lifetime (in seconds, 0 for never) * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as * upon joining a guild (should be avoided whenever possible) - * @property {boolean} [disableEveryone=false] Default value for {@link MessageOptions#disableEveryone} + * @property {boolean} [disableMentions=false] Default value for {@link MessageOptions#disableMentions} * @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when * they're missing all the data for a particular structure. See the "Partials" topic listed in the sidebar for some * important usage information, as partials require you to put checks in place when handling data. @@ -47,7 +47,7 @@ exports.DefaultOptions = { messageCacheLifetime: 0, messageSweepInterval: 0, fetchAllMembers: false, - disableEveryone: false, + disableMentions: false, partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], diff --git a/src/util/Util.js b/src/util/Util.js index 6e878f34..bc47ada4 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -518,6 +518,15 @@ class Util { return dec; } + /** + * Breaks user, role and everyone/here mentions by adding a zero width space after every @ character + * @param {string} str The string to sanitize + * @returns {string} + */ + static removeMentions(str) { + return str.replace(/@/g, '@\u200b'); + } + /** * The content to have all mentions replaced by the equivalent text. * @param {string} str The string to be converted @@ -525,8 +534,7 @@ class Util { * @returns {string} */ static cleanContent(str, message) { - return str - .replace(/@(everyone|here)/g, '@\u200b$1') + return Util.removeMentions(str .replace(/<@!?[0-9]+>/g, input => { const id = input.replace(/<|!|>|@/g, ''); if (message.channel.type === 'dm') { @@ -550,7 +558,7 @@ class Util { if (message.channel.type === 'dm') return input; const role = message.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; - }); + })); } /** diff --git a/test/disableMentions.js b/test/disableMentions.js new file mode 100644 index 00000000..281633d6 --- /dev/null +++ b/test/disableMentions.js @@ -0,0 +1,47 @@ +const Discord = require('../src'); +const { Util } = Discord; +const { token, prefix } = require('./auth'); + +const client = new Discord.Client({ + // To see a difference, comment out disableMentions and run the same tests using disableEveryone + // You will notice that all messages will mention @everyone + //disableEveryone: true + disableMentions: true +}); + +const tests = [ + // Test 1 + // See https://github.com/discordapp/discord-api-docs/issues/1189 + '@\u202eeveryone @\u202ehere', + + // Test 2 + // See https://github.com/discordapp/discord-api-docs/issues/1241 + // TL;DR: Characters like \u0300 will only be stripped if more than 299 are present + '\u0300@'.repeat(150) + '@\u0300everyone @\u0300here', + + // Test 3 + // Normal @everyone/@here mention + '@everyone @here', +]; + + + +client.on('ready', () => console.log('Ready!')); + +client.on('message', message => { + // Check if message starts with prefix + if (!message.content.startsWith(prefix)) return; + const [command, ...args] = message.content.substr(prefix.length).split(' '); + + // Clean content and log each character + console.log(Util.cleanContent(args.join(' '), message).split('')); + + if (command === 'test1') + message.reply(tests[0]); + else if (command === 'test2') + message.reply(tests[1]); + else if (command === 'test3') + message.reply(tests[2]); +}); + +client.login(token).catch(console.error); \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index 95b0790c..1b659740 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1427,6 +1427,7 @@ declare module 'discord.js' { public static basename(path: string, ext?: string): string; public static binaryToID(num: string): Snowflake; public static cleanContent(str: string, message: Message): string; + public static removeMentions(str: string): string; public static cloneObject(obj: object): object; public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; @@ -2068,7 +2069,7 @@ declare module 'discord.js' { messageCacheLifetime?: number; messageSweepInterval?: number; fetchAllMembers?: boolean; - disableEveryone?: boolean; + disableMentions?: boolean; partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; @@ -2464,7 +2465,7 @@ declare module 'discord.js' { nonce?: string; content?: string; embed?: MessageEmbed | MessageEmbedOptions; - disableEveryone?: boolean; + disableMentions?: boolean; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; @@ -2693,7 +2694,7 @@ declare module 'discord.js' { tts?: boolean; nonce?: string; embeds?: (MessageEmbed | object)[]; - disableEveryone?: boolean; + disableMentions?: boolean; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; From 653784b56445b015a1584de1b693e57fc9069050 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 26 Feb 2020 20:00:48 +0000 Subject: [PATCH 413/428] chore(StreamDispatcher): remove end event use finish event instead --- src/client/voice/dispatcher/StreamDispatcher.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 20a63fd9..8901767e 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -68,8 +68,6 @@ class StreamDispatcher extends Writable { this.on('finish', () => { this._cleanup(); - // Still emitting end for backwards compatibility, probably remove it in the future! - this.emit('end'); this._setSpeaking(0); }); From 6109669c972726a1ee9f0d5ec97a66d3ef3cde60 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 28 Feb 2020 16:41:12 +0000 Subject: [PATCH 414/428] typings(WebhookClient): client is not a client (#3838) --- typings/index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 1b659740..97bfb052 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1614,6 +1614,7 @@ declare module 'discord.js' { public avatar: string; public avatarURL(options?: ImageURLOptions): string | null; public channelID: Snowflake; + public client: Client; public guildID: Snowflake; public name: string; public owner: User | object | null; @@ -1623,6 +1624,7 @@ declare module 'discord.js' { export class WebhookClient extends WebhookMixin(BaseClient) { constructor(id: string, token: string, options?: ClientOptions); + public client: this; public token: string; } @@ -1892,7 +1894,6 @@ declare module 'discord.js' { function WebhookMixin(Base?: Constructable): Constructable; interface WebhookFields { - readonly client: Client; id: Snowflake; readonly createdAt: Date; readonly createdTimestamp: number; From 31a3a86ebccf7db299588e9899d20c19dab80710 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 28 Feb 2020 16:43:11 +0000 Subject: [PATCH 415/428] docs(MessageEmbed): document `article` embed type (#3846) --- src/structures/MessageEmbed.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index b5ce08b2..cad60d75 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -14,11 +14,12 @@ class MessageEmbed { setup(data) { // eslint-disable-line complexity /** * The type of this embed, either: + * * `rich` - a rich embed * * `image` - an image embed * * `video` - a video embed * * `gifv` - a gifv embed + * * `article` - an article embed * * `link` - a link embed - * * `rich` - a rich embed * @type {string} */ this.type = data.type; From 261816dcf84d8d871bfe1359eeb2d2e3a28da582 Mon Sep 17 00:00:00 2001 From: Crawl Date: Fri, 28 Feb 2020 17:43:45 +0100 Subject: [PATCH 416/428] chore(githooks): commitlint (#3836) --- package.json | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 123f1d7b..ecfb0c85 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,8 @@ } }, "devDependencies": { + "@commitlint/cli": "^8.3.5", + "@commitlint/config-angular": "^8.3.4", "@types/node": "^10.12.24", "@types/ws": "^7.2.1", "discord.js-docgen": "discordjs/docgen", @@ -124,7 +126,30 @@ }, "husky": { "hooks": { - "pre-commit": "npm test" + "pre-commit": "npm test", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + }, + "commitlint": { + "extends": ["@commitlint/config-angular"], + "rules": { + "type-enum": [ + 2, + "always", + [ + "chore", + "build", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test" + ] + ] } } } From df88729c44135ff9fdf5b3eb79ee1d3e09ff1b02 Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 28 Feb 2020 17:58:52 +0100 Subject: [PATCH 417/428] feat(MessageEmbed): re-introduce MessageEmbed#addField (#3850) * feat(MessageEmbed): re-introduce MessageEmbed#addField * suggestion: sorting alphabetically * suggestion: document inline to default false for #addField --- src/structures/MessageEmbed.js | 13 ++++++++++++- typings/index.d.ts | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index cad60d75..42750c42 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -215,6 +215,17 @@ class MessageEmbed { (this.footer ? this.footer.text.length : 0)); } + /** + * Adds a field to the embed (max 25). + * @param {StringResolvable} name The name of this field + * @param {StringResolvable} value The value of this field + * @param {boolean} [inline=false] If this field will be displayed inline + * @returns {MessageEmbed} + */ + addField(name, value, inline) { + return this.addFields({ name, value, inline }); + } + /** * Adds fields to the embed (max 25). * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add @@ -391,7 +402,7 @@ class MessageEmbed { * @typedef {Object} EmbedFieldData * @property {StringResolvable} name The name of this field * @property {StringResolvable} value The value of this field - * @property {boolean} [inline] If this field will be displayed inline + * @property {boolean} [inline=false] If this field will be displayed inline */ /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 97bfb052..55dc94eb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1053,6 +1053,7 @@ declare module 'discord.js' { public type: string; public url?: string; public readonly video: MessageEmbedVideo | null; + public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; public addFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; From 1af1e0cbb8ef2f433054e2d16cb760e503cd5458 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 28 Feb 2020 17:02:51 +0000 Subject: [PATCH 418/428] refactor: add some more consistency (#3842) * cleanup(StreamDispatcher): remove old 'end' event * fix(StreamDispatcher): only listen to finish event once * refactor(VoiceWebSocket): use `connection.client` in favour of `connection.voiceManager.client` * fix(VoiceWebSocket): use `client.clearInterval` in favour of `clearInterval` * refactor: destructure EventEmitter * refactor: destructure EventEmitter from events * refactor: use EventEmitter.off in favour of EventEmitter.removeListener * style: order typings alphabetically * oops * fix indent * style: alphabetically organize imports * style: remove extra line * Revert "style: remove extra line" This reverts commit 96e182ed69cfba159ef69aba1d0b218002af67c6. * Revert "style: alphabetically organize imports" This reverts commit 02aee9b06d991731d08d552cf661c5e01343ec6a. * Revert "refactor: destructure EventEmitter from events" This reverts commit 9953b4d267b183e12dee52b284ce7188d67381f6. * Revert "refactor: destructure EventEmitter" This reverts commit 930d7751ab2ee902c8a80559ae9976f67ef6efb0. * Revert "fix(StreamDispatcher): only listen to finish event once" This reverts commit 485a6430a804aba7368e105e9f8bd0c093d7491d. * refactor: use .removeListener instead of .off --- src/client/voice/networking/VoiceWebSocket.js | 6 +- src/client/websocket/WebSocketShard.js | 10 +- src/structures/interfaces/Collector.js | 6 +- typings/index.d.ts | 464 +++++++++--------- 4 files changed, 243 insertions(+), 243 deletions(-) diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index ac6405e1..c6ad45e5 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -35,7 +35,7 @@ class VoiceWebSocket extends EventEmitter { * @readonly */ get client() { - return this.connection.voiceManager.client; + return this.connection.client; } shutdown() { @@ -232,7 +232,7 @@ class VoiceWebSocket extends EventEmitter { * @event VoiceWebSocket#warn */ this.emit('warn', 'A voice heartbeat interval is being overwritten'); - clearInterval(this.heartbeatInterval); + this.client.clearInterval(this.heartbeatInterval); } this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval); } @@ -245,7 +245,7 @@ class VoiceWebSocket extends EventEmitter { this.emit('warn', 'Tried to clear a heartbeat interval that does not exist'); return; } - clearInterval(this.heartbeatInterval); + this.client.clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index b313c751..6fd38a63 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -175,11 +175,11 @@ class WebSocketShard extends EventEmitter { return new Promise((resolve, reject) => { const cleanup = () => { - this.off(ShardEvents.CLOSE, onClose); - this.off(ShardEvents.READY, onReady); - this.off(ShardEvents.RESUMED, onResumed); - this.off(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); - this.off(ShardEvents.DESTROYED, onInvalidOrDestroyed); + this.removeListener(ShardEvents.CLOSE, onClose); + this.removeListener(ShardEvents.READY, onReady); + this.removeListener(ShardEvents.RESUMED, onResumed); + this.removeListener(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.removeListener(ShardEvents.DESTROYED, onInvalidOrDestroyed); }; const onReady = () => { diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 6231605e..2ca7fd09 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -230,8 +230,8 @@ class Collector extends EventEmitter { // eslint-disable-next-line no-await-in-loop await new Promise(resolve => { const tick = () => { - this.off('collect', tick); - this.off('end', tick); + this.removeListener('collect', tick); + this.removeListener('end', tick); return resolve(); }; this.on('collect', tick); @@ -240,7 +240,7 @@ class Collector extends EventEmitter { } } } finally { - this.off('collect', onCollect); + this.removeListener('collect', onCollect); } } diff --git a/typings/index.d.ts b/typings/index.d.ts index 55dc94eb..bed7fa21 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -244,17 +244,6 @@ declare module 'discord.js' { public once(event: string, listener: (...args: any[]) => void): this; } - export class ClientVoiceManager { - constructor(client: Client); - public readonly client: Client; - public connections: Collection; - public broadcasts: VoiceBroadcast[]; - - private joinChannel(channel: VoiceChannel): Promise; - - public createBroadcast(): VoiceBroadcast; - } - export class ClientApplication extends Base { constructor(client: Client, data: object); public botPublic: boolean | null; @@ -275,34 +264,6 @@ declare module 'discord.js' { public toString(): string; } - export class Team extends Base { - constructor(client: Client, data: object); - public id: Snowflake; - public name: string; - public icon: string | null; - public ownerID: Snowflake | null; - public members: Collection; - - public readonly owner: TeamMember; - public readonly createdAt: Date; - public readonly createdTimestamp: number; - - public iconURL(options?: ImageURLOptions): string; - public toJSON(): object; - public toString(): string; - } - - export class TeamMember extends Base { - constructor(team: Team, data: object); - public team: Team; - public readonly id: Snowflake; - public permissions: string[]; - public membershipState: MembershipStates; - public user: User; - - public toString(): string; - } - export class ClientUser extends User { public mfaEnabled: boolean; public verified: boolean; @@ -315,6 +276,17 @@ declare module 'discord.js' { public setUsername(username: string): Promise; } + export class ClientVoiceManager { + constructor(client: Client); + public readonly client: Client; + public connections: Collection; + public broadcasts: VoiceBroadcast[]; + + private joinChannel(channel: VoiceChannel): Promise; + + public createBroadcast(): VoiceBroadcast; + } + export abstract class Collector extends EventEmitter { constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); private _timeout: NodeJS.Timer | null; @@ -822,18 +794,6 @@ declare module 'discord.js' { public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } - export class StoreChannel extends GuildChannel { - constructor(guild: Guild, data?: object); - public nsfw: boolean; - } - - export class PartialGroupDMChannel extends Channel { - constructor(client: Client, data: object); - public name: string; - public icon: string | null; - public iconURL(options?: ImageURLOptions): string | null; - } - export class GuildEmoji extends Emoji { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; @@ -889,6 +849,14 @@ declare module 'discord.js' { public valueOf(): string; } + export class HTTPError extends Error { + constructor(message: string, name: string, code: number, method: string, path: string); + public code: number; + public method: string; + public name: string; + public path: string; + } + export class Integration extends Base { constructor(client: Client, data: object, guild: Guild); public account: IntegrationAccount; @@ -908,14 +876,6 @@ declare module 'discord.js' { public sync(): Promise; } - export class HTTPError extends Error { - constructor(message: string, name: string, code: number, method: string, path: string); - public code: number; - public method: string; - public name: string; - public path: string; - } - export class Invite extends Base { constructor(client: Client, data: object); public channel: GuildChannel | PartialGroupDMChannel; @@ -941,11 +901,6 @@ declare module 'discord.js' { public toString(): string; } - export class MessageFlags extends BitField { - public static FLAGS: Record; - public static resolve(bit?: BitFieldResolvable): number; - } - export class Message extends Base { constructor(client: Client, data: object, channel: TextChannel | DMChannel); private _edits: Message[]; @@ -1072,6 +1027,11 @@ declare module 'discord.js' { public static normalizeFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): Required[]; } + export class MessageFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + export class MessageMentions { constructor(message: Message, users: object[] | Collection, roles: Snowflake[] | Collection, everyone: boolean); private _channels: Collection | null; @@ -1114,6 +1074,23 @@ declare module 'discord.js' { public toJSON(): object; } + export class NewsChannel extends TextBasedChannel(GuildChannel) { + constructor(guild: Guild, data?: object); + public messages: MessageManager; + public nsfw: boolean; + public topic: string | null; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable; reason?: string; }): Promise; + public setNSFW(nsfw: boolean, reason?: string): Promise; + public fetchWebhooks(): Promise>; + } + + export class PartialGroupDMChannel extends Channel { + constructor(client: Client, data: object); + public name: string; + public icon: string | null; + public iconURL(options?: ImageURLOptions): string | null; + } + export class PermissionOverwrites { constructor(guildChannel: GuildChannel, data?: object); public allow: Readonly; @@ -1321,7 +1298,15 @@ declare module 'discord.js' { public static generate(timestamp?: number | Date): Snowflake; } - function VolumeMixin(base: Constructable): Constructable; + export class Speaking extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + + export class StoreChannel extends GuildChannel { + constructor(guild: Guild, data?: object); + public nsfw: boolean; + } class StreamDispatcher extends VolumeMixin(Writable) { constructor(player: object, options?: StreamOptions, streams?: object); @@ -1357,11 +1342,6 @@ declare module 'discord.js' { public once(event: string, listener: (...args: any[]) => void): this; } - export class Speaking extends BitField { - public static FLAGS: Record; - public static resolve(bit?: BitFieldResolvable): number; - } - export class Structures { public static get(structure: K): Extendable[K]; public static get(structure: string): (...args: any[]) => void; @@ -1374,6 +1354,34 @@ declare module 'discord.js' { public static resolve(bit?: BitFieldResolvable): number; } + export class Team extends Base { + constructor(client: Client, data: object); + public id: Snowflake; + public name: string; + public icon: string | null; + public ownerID: Snowflake | null; + public members: Collection; + + public readonly owner: TeamMember; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + + public iconURL(options?: ImageURLOptions): string; + public toJSON(): object; + public toString(): string; + } + + export class TeamMember extends Base { + constructor(team: Team, data: object); + public team: Team; + public readonly id: Snowflake; + public permissions: string[]; + public membershipState: MembershipStates; + public user: User; + + public toString(): string; + } + export class TextChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); public messages: MessageManager; @@ -1386,16 +1394,6 @@ declare module 'discord.js' { public fetchWebhooks(): Promise>; } - export class NewsChannel extends TextBasedChannel(GuildChannel) { - constructor(guild: Guild, data?: object); - public messages: MessageManager; - public nsfw: boolean; - public topic: string | null; - public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable; reason?: string; }): Promise; - public setNSFW(nsfw: boolean, reason?: string): Promise; - public fetchWebhooks(): Promise>; - } - export class User extends PartialTextBasedChannel(Base) { constructor(client: Client, data: object); public avatar: string | null; @@ -1749,6 +1747,22 @@ declare module 'discord.js' { public resolveID(resolvable: R): K | null; } + export class GuildChannelManager extends BaseManager { + constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; + public create(name: string, options: GuildCreateChannelOptions & { type: 'voice'; }): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'category'; }): Promise; + public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text'; }): Promise; + public create(name: string, options: GuildCreateChannelOptions): Promise; + } + + export class GuildEmojiManager extends BaseManager { + constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; + public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; + public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; + } + export class GuildEmojiRoleManager { constructor(emoji: GuildEmoji); public emoji: GuildEmoji; @@ -1759,26 +1773,20 @@ declare module 'discord.js' { public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection): Promise; } - export class GuildEmojiManager extends BaseManager { - constructor(guild: Guild, iterable?: Iterable); - public guild: Guild; - public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; - public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; + export class GuildManager extends BaseManager { + constructor(client: Client, iterable?: Iterable); + public create(name: string, options?: { region?: string; icon: BufferResolvable | Base64Resolvable | null; }): Promise; } - export class GuildChannelManager extends BaseManager { + export class GuildMemberManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; - public create(name: string, options: GuildCreateChannelOptions & { type: 'voice'; }): Promise; - public create(name: string, options: GuildCreateChannelOptions & { type: 'category'; }): Promise; - public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text'; }): Promise; - public create(name: string, options: GuildCreateChannelOptions): Promise; - } - - // Hacky workaround because changing the signature of an overridden method errors - class OverridableManager extends BaseManager { - public add(data: any, cache: any): any; - public set(key: any): any; + public ban(user: UserResolvable, options?: BanOptions): Promise; + public fetch(options: UserResolvable | FetchMemberOptions): Promise; + public fetch(options?: FetchMembersOptions): Promise>; + public prune(options: GuildPruneMembersOptions & { dry?: false; count: false; }): Promise; + public prune(options?: GuildPruneMembersOptions): Promise; + public unban(user: UserResolvable, reason?: string): Promise; } export class GuildMemberRoleManager extends OverridableManager { @@ -1794,22 +1802,6 @@ declare module 'discord.js' { public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; } - export class GuildMemberManager extends BaseManager { - constructor(guild: Guild, iterable?: Iterable); - public guild: Guild; - public ban(user: UserResolvable, options?: BanOptions): Promise; - public fetch(options: UserResolvable | FetchMemberOptions): Promise; - public fetch(options?: FetchMembersOptions): Promise>; - public prune(options: GuildPruneMembersOptions & { dry?: false; count: false; }): Promise; - public prune(options?: GuildPruneMembersOptions): Promise; - public unban(user: UserResolvable, reason?: string): Promise; - } - - export class GuildManager extends BaseManager { - constructor(client: Client, iterable?: Iterable); - public create(name: string, options?: { region?: string; icon: BufferResolvable | Base64Resolvable | null; }): Promise; - } - export class MessageManager extends BaseManager { constructor(channel: TextChannel | DMChannel, iterable?: Iterable); public channel: TextBasedChannelFields; @@ -1820,6 +1812,12 @@ declare module 'discord.js' { public delete(message: MessageResolvable, reason?: string): Promise; } + // Hacky workaround because changing the signature of an overridden method errors + class OverridableManager extends BaseManager { + public add(data: any, cache: any): any; + public set(key: any): any; + } + export class PresenceManager extends BaseManager { constructor(client: Client, iterable?: Iterable); } @@ -1894,6 +1892,8 @@ declare module 'discord.js' { function WebhookMixin(Base?: Constructable): Constructable; + function VolumeMixin(base: Constructable): Constructable; + interface WebhookFields { id: Snowflake; readonly createdAt: Date; @@ -1932,11 +1932,13 @@ declare module 'discord.js' { | 'WATCHING' | 'CUSTOM_STATUS'; - type MessageFlagsString = 'CROSSPOSTED' - | 'IS_CROSSPOST' - | 'SUPPRESS_EMBEDS' - | 'SOURCE_MESSAGE_DELETED' - | 'URGENT'; + interface AddGuildMemberOptions { + accessToken: string; + nick?: string; + roles?: Collection | RoleResolvable[]; + mute?: boolean; + deaf?: boolean; + } interface APIErrror { UNKNOWN_ACCOUNT: number; @@ -1985,25 +1987,12 @@ declare module 'discord.js' { REACTION_BLOCKED: number; } - interface AddGuildMemberOptions { - accessToken: string; - nick?: string; - roles?: Collection | RoleResolvable[]; - mute?: boolean; - deaf?: boolean; - } - interface AuditLogChange { key: string; old?: any; new?: any; } - interface ImageURLOptions { - format?: ImageExt; - size?: ImageSize; - } - interface AwaitMessagesOptions extends MessageCollectorOptions { errors?: string[]; } @@ -2084,6 +2073,21 @@ declare module 'discord.js' { http?: HTTPOptions; } + type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; + + interface ClientPresenceStatusData { + web?: ClientPresenceStatus; + mobile?: ClientPresenceStatus; + desktop?: ClientPresenceStatus; + } + + interface CloseEvent { + wasClean: boolean; + code: number; + reason: string; + target: WebSocket; + } + type CollectorFilter = (...args: any[]) => boolean; interface CollectorOptions { @@ -2122,6 +2126,13 @@ declare module 'discord.js' { | number | string; + interface CrosspostedChannel { + channelID: Snowflake; + guildID: Snowflake; + type: keyof typeof ChannelType; + name: string; + } + interface DeconstructedSnowflake { timestamp: number; readonly date: Date; @@ -2133,11 +2144,6 @@ declare module 'discord.js' { type DefaultMessageNotifications = 'ALL' | 'MENTIONS'; - interface GuildEmojiEditData { - name?: string; - roles?: Collection | RoleResolvable[]; - } - interface EmbedField { name: string; value: string; @@ -2154,6 +2160,25 @@ declare module 'discord.js' { type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; + interface ErrorEvent { + error: any; + message: string; + type: string; + target: WebSocket; + } + + interface EscapeMarkdownOptions { + codeBlock?: boolean; + inlineCode?: boolean; + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + spoiler?: boolean; + inlineCodeContent?: boolean; + codeBlockContent?: boolean; + } + interface Extendable { GuildEmoji: typeof GuildEmoji; DMChannel: typeof DMChannel; @@ -2187,17 +2212,6 @@ declare module 'discord.js' { name?: string; } - interface MessageActivity { - partyID: string; - type: number; - } - - interface MessageReference { - channelID: string; - guildID: string; - messageID: string | null; - } - type GuildAuditLogsAction = keyof GuildAuditLogsActions; interface GuildAuditLogsActions { @@ -2286,11 +2300,6 @@ declare module 'discord.js' { name?: string; } - interface GuildEmojiCreateOptions { - roles?: Collection | RoleResolvable[]; - reason?: string; - } - interface GuildEditData { name?: string; region?: string; @@ -2312,6 +2321,16 @@ declare module 'discord.js' { channel: GuildChannelResolvable | null; } + interface GuildEmojiCreateOptions { + roles?: Collection | RoleResolvable[]; + reason?: string; + } + + interface GuildEmojiEditData { + name?: string; + roles?: Collection | RoleResolvable[]; + } + type GuildFeatures = 'ANIMATED_ICON' | 'BANNER' | 'COMMERCE' @@ -2366,6 +2385,11 @@ declare module 'discord.js' { | 1024 | 2048; + interface ImageURLOptions { + format?: ImageExt; + size?: ImageSize; + } + interface IntegrationData { id: string; type: string; @@ -2394,13 +2418,18 @@ declare module 'discord.js' { type MembershipStates = 'INVITED' | 'ACCEPTED'; + type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; + + interface MessageActivity { + partyID: string; + type: number; + } + interface MessageCollectorOptions extends CollectorOptions { max?: number; maxProcessed?: number; } - type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; - interface MessageEditOptions { content?: string; embed?: MessageEmbedOptions | null; @@ -2408,6 +2437,26 @@ declare module 'discord.js' { flags?: BitFieldResolvable; } + interface MessageEmbedAuthor { + name?: string; + url?: string; + iconURL?: string; + proxyIconURL?: string; + } + + interface MessageEmbedFooter { + text?: string; + iconURL?: string; + proxyIconURL?: string; + } + + interface MessageEmbedImage { + url: string; + proxyURL?: string; + height?: number; + width?: number; + } + interface MessageEmbedOptions { title?: string; description?: string; @@ -2423,11 +2472,9 @@ declare module 'discord.js' { footer?: Partial & { icon_url?: string; proxy_icon_url?: string; }; } - interface MessageEmbedAuthor { - name?: string; - url?: string; - iconURL?: string; - proxyIconURL?: string; + interface MessageEmbedProvider { + name: string; + url: string; } interface MessageEmbedThumbnail { @@ -2437,24 +2484,6 @@ declare module 'discord.js' { width?: number; } - interface MessageEmbedFooter { - text?: string; - iconURL?: string; - proxyIconURL?: string; - } - - interface MessageEmbedImage { - url: string; - proxyURL?: string; - height?: number; - width?: number; - } - - interface MessageEmbedProvider { - name: string; - url: string; - } - interface MessageEmbedVideo { url?: string; proxyURL?: string; @@ -2462,6 +2491,18 @@ declare module 'discord.js' { width?: number; } + interface MessageEvent { + data: WebSocket.Data; + type: string; + target: WebSocket; + } + + type MessageFlagsString = 'CROSSPOSTED' + | 'IS_CROSSPOST' + | 'SUPPRESS_EMBEDS' + | 'SOURCE_MESSAGE_DELETED' + | 'URGENT'; + interface MessageOptions { tts?: boolean; nonce?: string; @@ -2476,6 +2517,12 @@ declare module 'discord.js' { type MessageReactionResolvable = MessageReaction | Snowflake; + interface MessageReference { + channelID: string; + guildID: string; + messageID: string | null; + } + type MessageResolvable = Message | Snowflake; type MessageTarget = TextChannel | DMChannel | User | GuildMember | Webhook | WebhookClient; @@ -2513,6 +2560,8 @@ declare module 'discord.js' { interface PermissionOverwriteOption extends Partial> { } + type PermissionResolvable = BitFieldResolvable; + type PermissionString = 'CREATE_INSTANT_INVITE' | 'KICK_MEMBERS' | 'BAN_MEMBERS' @@ -2546,8 +2595,6 @@ declare module 'discord.js' { interface RecursiveArray extends Array> { } - type PermissionResolvable = BitFieldResolvable; - interface PermissionOverwriteOptions { allow: PermissionResolvable; deny: PermissionResolvable; @@ -2569,20 +2616,6 @@ declare module 'discord.js' { type PresenceResolvable = Presence | UserResolvable | Snowflake; - type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; - - interface ClientPresenceStatusData { - web?: ClientPresenceStatus; - mobile?: ClientPresenceStatus; - desktop?: ClientPresenceStatus; - } - - type PartialTypes = 'USER' - | 'CHANNEL' - | 'GUILD_MEMBER' - | 'MESSAGE' - | 'REACTION'; - type Partialize = { id: string; partial: true; @@ -2591,11 +2624,17 @@ declare module 'discord.js' { [K in keyof Omit]: T[K] | null; }; - interface PartialMessage extends Partialize {} interface PartialChannel extends Partialize {} interface PartialGuildMember extends Partialize {} + interface PartialMessage extends Partialize {} interface PartialUser extends Partialize {} + type PartialTypes = 'USER' + | 'CHANNEL' + | 'GUILD_MEMBER' + | 'MESSAGE' + | 'REACTION'; + type PresenceStatus = ClientPresenceStatus | 'offline'; type PresenceStatusData = ClientPresenceStatus | 'invisible'; @@ -2746,44 +2785,5 @@ declare module 'discord.js' { | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE'; - interface MessageEvent { - data: WebSocket.Data; - type: string; - target: WebSocket; - } - - interface CloseEvent { - wasClean: boolean; - code: number; - reason: string; - target: WebSocket; - } - - interface ErrorEvent { - error: any; - message: string; - type: string; - target: WebSocket; - } - - interface CrosspostedChannel { - channelID: Snowflake; - guildID: Snowflake; - type: keyof typeof ChannelType; - name: string; - } - - interface EscapeMarkdownOptions { - codeBlock?: boolean; - inlineCode?: boolean; - bold?: boolean; - italic?: boolean; - underline?: boolean; - strikethrough?: boolean; - spoiler?: boolean; - inlineCodeContent?: boolean; - codeBlockContent?: boolean; - } - //#endregion } From 3d0c1df19d66bbb83e2fe73a9377bd6a8428f515 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 28 Feb 2020 17:26:37 +0000 Subject: [PATCH 419/428] =?UTF-8?q?refactor(Guild)/fix(Util):=20use=20reso?= =?UTF-8?q?lveID=20and=20regex=20for=20cleanCod=E2=80=A6=20(#3837)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(Guild): use resolveID instead of resolve(...).id * fix(Util): use regex for cleanCodeBlockContent --- src/structures/Guild.js | 2 +- src/util/Util.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9353f4de..2368ae62 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -861,7 +861,7 @@ class Guild extends Base { } if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); if (typeof data.icon !== 'undefined') _data.icon = data.icon; - if (data.owner) _data.owner_id = this.client.users.resolve(data.owner).id; + if (data.owner) _data.owner_id = this.client.users.resolveID(data.owner); if (data.splash) _data.splash = data.splash; if (data.banner) _data.banner = data.banner; if (typeof data.explicitContentFilter !== 'undefined') { diff --git a/src/util/Util.js b/src/util/Util.js index bc47ada4..bc2f0f81 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -567,7 +567,7 @@ class Util { * @returns {string} */ static cleanCodeBlockContent(text) { - return text.replace('```', '`\u200b``'); + return text.replace(/```/g, '`\u200b``'); } /** From 2ee0f1cdc69dcd0c8f870bcecc3d36aef0e44ad1 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 29 Feb 2020 06:43:42 +0000 Subject: [PATCH 420/428] =?UTF-8?q?feat(GuildManager):=20Allow=20for=20mor?= =?UTF-8?q?e=20options=20for=20GuildManager.cre=E2=80=A6=20(#3742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * typings: add GuildVerificationLevel and GuildExplicitContentFilter * implement new types * fix jsdoc on stores * typo * add more options for GuildStore#create * add channels and roles * update typings * fix typings and use snake case for permissionOverwrites * typings & jsdoc * fix tslint * remove trailing whitespace * fix jsdoc * fix jsdoc * fix oopsies * fix lint * fix lint * fix mr lint man * add typedefs and support for setting channel parents * fix tab indenation * update jsdoc * suggested changes * style: fix silly format * docs(PartialChannelData): name is not optional * style: remove silly format --- src/managers/GuildManager.js | 144 ++++++++++++++++++++----- src/structures/Guild.js | 36 ++++--- src/structures/GuildChannel.js | 4 +- src/structures/PermissionOverwrites.js | 6 +- src/util/Constants.js | 33 ++++-- typings/index.d.ts | 40 +++++-- 6 files changed, 202 insertions(+), 61 deletions(-) diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js index 8a28e06b..40711863 100644 --- a/src/managers/GuildManager.js +++ b/src/managers/GuildManager.js @@ -2,10 +2,17 @@ const BaseManager = require('./BaseManager'); const DataResolver = require('../util/DataResolver'); -const { Events } = require('../util/Constants'); +const { + Events, + VerificationLevels, + DefaultMessageNotifications, + ExplicitContentFilterLevels, +} = require('../util/Constants'); const Guild = require('../structures/Guild'); const GuildChannel = require('../structures/GuildChannel'); const GuildMember = require('../structures/GuildMember'); +const Permissions = require('../util/Permissions'); +const { resolveColor } = require('../util/Util'); const GuildEmoji = require('../structures/GuildEmoji'); const Invite = require('../structures/Invite'); const Role = require('../structures/Role'); @@ -36,6 +43,45 @@ class GuildManager extends BaseManager { * @typedef {Guild|GuildChannel|GuildMember|GuildEmoji|Role|Snowflake|Invite} GuildResolvable */ + /** + * Partial data for a Role. + * @typedef {Object} PartialRoleData + * @property {number} id The ID for this role, used to set channel overrides, + * this is a placeholder and will be replaced by the API after consumption + * @property {string} [name] The name of the role + * @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number + * @property {boolean} [hoist] Whether or not the role should be hoisted + * @property {number} [position] The position of the role + * @property {PermissionResolvable|number} [permissions] The permissions of the role + * @property {boolean} [mentionable] Whether or not the role should be mentionable + */ + + /** + * Partial overwrite data. + * @typedef {Object} PartialOverwriteData + * @property {number|Snowflake} id The Role or User ID for this overwrite + * @property {string} [type] The type of this overwrite + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny + */ + + /** + * Partial data for a Channel. + * @typedef {Object} PartialChannelData + * @property {number} [id] The ID for this channel, used to set its parent, + * this is a placeholder and will be replaced by the API after consumption + * @property {number} [parentID] The parent ID for this channel + * @property {string} [type] The type of the channel + * @property {string} name The name of the channel + * @property {string} [topic] The topic of the text channel + * @property {boolean} [nsfw] Whether the channel is NSFW + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the channel + * @property {PartialOverwriteData} [permissionOverwrites] + * Overwrites of the channel + * @property {number} [rateLimitPerUser] The rate limit per user of the channel in seconds + */ + /** * Resolves a GuildResolvable to a Guild object. * @method resolve @@ -75,37 +121,81 @@ class GuildManager extends BaseManager { * This is only available to bots in fewer than 10 guilds. * @param {string} name The name of the guild * @param {Object} [options] Options for the creating - * @param {string} [options.region] The region for the server, defaults to the closest one available + * @param {PartialChannelData[]} [options.channels] The channels for this guild + * @param {DefaultMessageNotifications} [options.defaultMessageNotifications] The default message notifications + * for the guild + * @param {ExplicitContentFilterLevel} [options.explicitContentFilter] The explicit content filter level for the guild * @param {BufferResolvable|Base64Resolvable} [options.icon=null] The icon for the guild + * @param {string} [options.region] The region for the server, defaults to the closest one available + * @param {PartialRoleData[]} [options.roles] The roles for this guild, + * the first element of this array is used to change properties of the guild's everyone role. + * @param {VerificationLevel} [options.verificationLevel] The verification level for the guild * @returns {Promise} The guild that was created */ - create(name, { region, icon = null } = {}) { - if (!icon || (typeof icon === 'string' && icon.startsWith('data:'))) { - return new Promise((resolve, reject) => - this.client.api.guilds.post({ data: { name, region, icon } }) - .then(data => { - if (this.client.guilds.cache.has(data.id)) return resolve(this.client.guilds.cache.get(data.id)); - - const handleGuild = guild => { - if (guild.id === data.id) { - this.client.removeListener(Events.GUILD_CREATE, handleGuild); - this.client.clearTimeout(timeout); - resolve(guild); - } - }; - this.client.on(Events.GUILD_CREATE, handleGuild); - - const timeout = this.client.setTimeout(() => { - this.client.removeListener(Events.GUILD_CREATE, handleGuild); - resolve(this.client.guilds.add(data)); - }, 10000); - return undefined; - }, reject), - ); + async create(name, { + channels = [], + defaultMessageNotifications, + explicitContentFilter, + icon = null, + region, + roles = [], + verificationLevel, + } = {}) { + icon = await DataResolver.resolveImage(icon); + if (typeof verificationLevel !== 'undefined' && typeof verificationLevel !== 'number') { + verificationLevel = VerificationLevels.indexOf(verificationLevel); } + if (typeof defaultMessageNotifications !== 'undefined' && typeof defaultMessageNotifications !== 'number') { + defaultMessageNotifications = DefaultMessageNotifications.indexOf(defaultMessageNotifications); + } + if (typeof explicitContentFilter !== 'undefined' && typeof explicitContentFilter !== 'number') { + explicitContentFilter = ExplicitContentFilterLevels.indexOf(explicitContentFilter); + } + for (const channel of channels) { + channel.parent_id = channel.parentID; + delete channel.parentID; + if (!channel.permissionOverwrites) continue; + for (const overwrite of channel.permissionOverwrites) { + if (overwrite.allow) overwrite.allow = Permissions.resolve(overwrite.allow); + if (overwrite.deny) overwrite.deny = Permissions.resolve(overwrite.deny); + } + channel.permission_overwrites = channel.permissionOverwrites; + delete channel.permissionOverwrites; + } + for (const role of roles) { + if (role.color) role.color = resolveColor(role.color); + if (role.permissions) role.permissions = Permissions.resolve(role.permissions); + } + return new Promise((resolve, reject) => + this.client.api.guilds.post({ data: { + name, + region, + icon, + verification_level: verificationLevel, + default_message_notifications: defaultMessageNotifications, + explicit_content_filter: explicitContentFilter, + channels, + roles, + } }) + .then(data => { + if (this.client.guilds.cache.has(data.id)) return resolve(this.client.guilds.cache.get(data.id)); - return DataResolver.resolveImage(icon) - .then(data => this.create(name, { region, icon: data || null })); + const handleGuild = guild => { + if (guild.id === data.id) { + this.client.removeListener(Events.GUILD_CREATE, handleGuild); + this.client.clearTimeout(timeout); + resolve(guild); + } + }; + this.client.on(Events.GUILD_CREATE, handleGuild); + + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Events.GUILD_CREATE, handleGuild); + resolve(this.client.guilds.add(data)); + }, 10000); + return undefined; + }, reject), + ); } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 2368ae62..57431e42 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -5,7 +5,13 @@ const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); -const { ChannelTypes, DefaultMessageNotifications, PartialTypes } = require('../util/Constants'); +const { + ChannelTypes, + DefaultMessageNotifications, + PartialTypes, + VerificationLevels, + ExplicitContentFilterLevels, +} = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); @@ -247,15 +253,15 @@ class Guild extends Base { /** * The verification level of the guild - * @type {number} + * @type {VerificationLevel} */ - this.verificationLevel = data.verification_level; + this.verificationLevel = VerificationLevels[data.verification_level]; /** * The explicit content filter level of the guild - * @type {number} + * @type {ExplicitContentFilterLevel} */ - this.explicitContentFilter = data.explicit_content_filter; + this.explicitContentFilter = ExplicitContentFilterLevels[data.explicit_content_filter]; /** * The required MFA level for the guild @@ -821,8 +827,8 @@ class Guild extends Base { * @typedef {Object} GuildEditData * @property {string} [name] The name of the guild * @property {string} [region] The region of the guild - * @property {number} [verificationLevel] The verification level of the guild - * @property {number} [explicitContentFilter] The level of the explicit content filter + * @property {VerificationLevel|number} [verificationLevel] The verification level of the guild + * @property {ExplicitContentFilterLevel|number} [explicitContentFilter] The level of the explicit content filter * @property {ChannelResolvable} [afkChannel] The AFK channel of the guild * @property {ChannelResolvable} [systemChannel] The system channel of the guild * @property {number} [afkTimeout] The AFK timeout of the guild @@ -852,7 +858,11 @@ class Guild extends Base { const _data = {}; if (data.name) _data.name = data.name; if (data.region) _data.region = data.region; - if (typeof data.verificationLevel !== 'undefined') _data.verification_level = Number(data.verificationLevel); + if (typeof data.verificationLevel !== 'undefined') { + _data.verification_level = typeof data.verificationLevel === 'number' ? + Number(data.verificationLevel) : + ExplicitContentFilterLevels.indexOf(data.verificationLevel); + } if (typeof data.afkChannel !== 'undefined') { _data.afk_channel_id = this.client.channels.resolveID(data.afkChannel); } @@ -865,12 +875,14 @@ class Guild extends Base { if (data.splash) _data.splash = data.splash; if (data.banner) _data.banner = data.banner; if (typeof data.explicitContentFilter !== 'undefined') { - _data.explicit_content_filter = Number(data.explicitContentFilter); + _data.explicit_content_filter = typeof data.explicitContentFilter === 'number' ? + data.explicitContentFilter : + ExplicitContentFilterLevels.indexOf(data.explicitContentFilter); } if (typeof data.defaultMessageNotifications !== 'undefined') { _data.default_message_notifications = typeof data.defaultMessageNotifications === 'string' ? DefaultMessageNotifications.indexOf(data.defaultMessageNotifications) : - Number(data.defaultMessageNotifications); + data.defaultMessageNotifications; } if (typeof data.systemChannelFlags !== 'undefined') { _data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); @@ -881,7 +893,7 @@ class Guild extends Base { /** * Edits the level of the explicit content filter. - * @param {number} explicitContentFilter The new level of the explicit content filter + * @param {ExplicitContentFilterLevel|number} explicitContentFilter The new level of the explicit content filter * @param {string} [reason] Reason for changing the level of the guild's explicit content filter * @returns {Promise} */ @@ -943,7 +955,7 @@ class Guild extends Base { /** * Edits the verification level of the guild. - * @param {number} verificationLevel The new verification level of the guild + * @param {VerificationLevel|number} verificationLevel The new verification level of the guild * @param {string} [reason] Reason for changing the guild's verification level * @returns {Promise} * @example diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 35a0bc2a..d80f5b24 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -211,7 +211,7 @@ class GuildChannel extends Channel { /** * Updates Overwrites for a user or role in this channel. (creates if non-existent) * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update - * @param {PermissionOverwriteOption} options The options for the update + * @param {PermissionOverwriteOptions} options The options for the update * @param {string} [reason] Reason for creating/editing this overwrite * @returns {Promise} * @example @@ -234,7 +234,7 @@ class GuildChannel extends Channel { /** * Overwrites the permissions for a user or role in this channel. (replaces if existent) * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update - * @param {PermissionOverwriteOption} options The options for the update + * @param {PermissionOverwriteOptions} options The options for the update * @param {string} [reason] Reason for creating/editing this overwrite * @returns {Promise} * @example diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index cf11eaa9..59a5d93f 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -56,7 +56,7 @@ class PermissionOverwrites { /** * Updates this permissionOverwrites. - * @param {PermissionOverwriteOption} options The options for the update + * @param {PermissionOverwriteOptions} options The options for the update * @param {string} [reason] Reason for creating/editing this overwrite * @returns {Promise} * @example @@ -99,7 +99,7 @@ class PermissionOverwrites { * 'ATTACH_FILES': false, * } * ``` - * @typedef {Object} PermissionOverwriteOption + * @typedef {Object} PermissionOverwriteOptions */ /** @@ -110,7 +110,7 @@ class PermissionOverwrites { /** * Deletes this Permission Overwrite. - * @param {PermissionOverwriteOption} options The options for the update + * @param {PermissionOverwriteOptions} options The options for the update * @param {Object} initialPermissions The initial permissions * @param {PermissionResolvable} initialPermissions.allow Initial allowed permissions * @param {PermissionResolvable} initialPermissions.deny Initial denied permissions diff --git a/src/util/Constants.js b/src/util/Constants.js index b38e0b58..547ffde1 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -493,21 +493,34 @@ exports.Colors = { NOT_QUITE_BLACK: 0x23272A, }; +/** + * The value set for the explicit content filter levels for a guild: + * * DISABLED + * * MEMBERS_WITHOUT_ROLES + * * ALL_MEMBERS + * @typedef {string} ExplicitContentFilterLevel + */ +exports.ExplicitContentFilterLevels = [ + 'DISABLED', + 'MEMBERS_WITHOUT_ROLES', + 'ALL_MEMBERS', +]; + /** * The value set for the verification levels for a guild: - * * None - * * Low - * * Medium - * * (╯°□°)╯︵ ┻━┻ - * * ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ + * * NONE + * * LOW + * * MEDIUM + * * HIGH + * * VERY_HIGH * @typedef {string} VerificationLevel */ exports.VerificationLevels = [ - 'None', - 'Low', - 'Medium', - '(╯°□°)╯︵ ┻━┻', - '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', + 'NONE', + 'LOW', + 'MEDIUM', + 'HIGH', + 'VERY_HIGH', ]; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index bed7fa21..1bc54ae4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -582,7 +582,9 @@ declare module 'discord.js' { }; MessageTypes: MessageType[]; ActivityTypes: ActivityType[]; + ExplicitContentFilterLevels: ExplicitContentFilterLevel[]; DefaultMessageNotifications: DefaultMessageNotifications[]; + VerificationLevels: VerificationLevel[]; MembershipStates: 'INVITED' | 'ACCEPTED'; }; @@ -647,7 +649,7 @@ declare module 'discord.js' { public embedChannelID: Snowflake | null; public embedEnabled: boolean; public emojis: GuildEmojiManager; - public explicitContentFilter: number; + public explicitContentFilter: ExplicitContentFilterLevel; public features: GuildFeatures[]; public icon: string | null; public id: Snowflake; @@ -681,7 +683,7 @@ declare module 'discord.js' { public systemChannelFlags: Readonly; public systemChannelID: Snowflake | null; public vanityURLCode: string | null; - public verificationLevel: number; + public verificationLevel: VerificationLevel; public readonly verified: boolean; public readonly voice: VoiceState | null; public readonly voiceStates: VoiceStateManager; @@ -713,7 +715,7 @@ declare module 'discord.js' { public setChannelPositions(channelPositions: ChannelPosition[]): Promise; public setDefaultMessageNotifications(defaultMessageNotifications: DefaultMessageNotifications | number, reason?: string): Promise; public setEmbed(embed: GuildEmbedData, reason?: string): Promise; - public setExplicitContentFilter(explicitContentFilter: number, reason?: string): Promise; + public setExplicitContentFilter(explicitContentFilter: ExplicitContentFilterLevel, reason?: string): Promise; public setIcon(icon: Base64Resolvable | null, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; @@ -722,7 +724,7 @@ declare module 'discord.js' { public setSplash(splash: Base64Resolvable | null, reason?: string): Promise; public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise; - public setVerificationLevel(verificationLevel: number, reason?: string): Promise; + public setVerificationLevel(verificationLevel: VerificationLevel, reason?: string): Promise; public splashURL(options?: ImageURLOptions): string | null; public toJSON(): object; public toString(): string; @@ -2179,6 +2181,8 @@ declare module 'discord.js' { codeBlockContent?: boolean; } + type ExplicitContentFilterLevel = 'DISABLED' | 'MEMBERS_WITHOUT_ROLES' | 'ALL_MEMBERS'; + interface Extendable { GuildEmoji: typeof GuildEmoji; DMChannel: typeof DMChannel; @@ -2303,8 +2307,8 @@ declare module 'discord.js' { interface GuildEditData { name?: string; region?: string; - verificationLevel?: number; - explicitContentFilter?: number; + verificationLevel?: VerificationLevel; + explicitContentFilter?: ExplicitContentFilterLevel; defaultMessageNotifications?: DefaultMessageNotifications | number; afkChannel?: ChannelResolvable; systemChannel?: ChannelResolvable; @@ -2625,9 +2629,27 @@ declare module 'discord.js' { }; interface PartialChannel extends Partialize {} + + interface PartialChannelData { + id?: number; + name: string; + topic?: string; + type?: ChannelType; + parentID?: number; + permissionOverwrites?: { + id: number | Snowflake; + type?: OverwriteType; + allow?: PermissionResolvable; + deny?: PermissionResolvable; + }[]; + } + interface PartialGuildMember extends Partialize {} interface PartialMessage extends Partialize {} - interface PartialUser extends Partialize {} + + interface PartialRoleData extends RoleData { + id?: number; + } type PartialTypes = 'USER' | 'CHANNEL' @@ -2635,6 +2657,8 @@ declare module 'discord.js' { | 'MESSAGE' | 'REACTION'; + interface PartialUser extends Partialize {} + type PresenceStatus = ClientPresenceStatus | 'offline'; type PresenceStatusData = ClientPresenceStatus | 'invisible'; @@ -2720,6 +2744,8 @@ declare module 'discord.js' { type UserResolvable = User | Snowflake | Message | GuildMember; + type VerificationLevel = 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'; + type VoiceStatus = number; interface WebhookEditData { From de4b4a0939e36c74a19019f5f1d0c47a7934956f Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sat, 29 Feb 2020 13:18:37 +0000 Subject: [PATCH 421/428] feat(GuildMemberStore): add options.withPresences to fetch() (#3562) * feat: add options.withPresences to fetch() feat: update presences if present on received data typings: add user & withPresences to FetchMembersOptions fix: checking for added options ref: qol changes to return type so that all members are fetched oopsie * fix: use Manager.cache * fix(typings): tslint error Co-authored-by: Crawl --- .../websocket/handlers/GUILD_MEMBERS_CHUNK.js | 3 ++ src/managers/GuildMemberManager.js | 34 ++++++++++++++----- typings/index.d.ts | 4 ++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js index 9a0a880a..59120dcd 100644 --- a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -9,6 +9,9 @@ module.exports = (client, { d: data }) => { const members = new Collection(); for (const member of data.members) members.set(member.user.id, guild.members.add(member)); + if (data.presences) { + for (const presence of data.presences) guild.presences.cache.add(Object.assign(presence, { guild })); + } /** * Emitted whenever a chunk of guild members is received (all members come from the same guild). * @event Client#guildMembersChunk diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index 7fae74c8..23639df5 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -72,8 +72,10 @@ class GuildMemberManager extends BaseManager { /** * Options used to fetch multiple members from a guild. * @typedef {Object} FetchMembersOptions - * @property {string} [query=''] Limit fetch to members with similar usernames + * @property {UserResolvable|UserResolvable[]} user The user(s) to fetch + * @property {?string} query Limit fetch to members with similar usernames * @property {number} [limit=0] Maximum number of members to request + * @property {boolean} [withPresences=false] Whether or not to include the presences */ /** @@ -98,6 +100,11 @@ class GuildMemberManager extends BaseManager { * .then(console.log) * .catch(console.error); * @example + * // Fetch by an array of users including their presences + * guild.members.fetch({ user: ['66564597481480192', '191615925336670208'], withPresences: true }) + * .then(console.log) + * .catch(console.error); + * @example * // Fetch by query * guild.members.fetch({ query: 'hydra', limit: 1 }) * .then(console.log) @@ -108,8 +115,13 @@ class GuildMemberManager extends BaseManager { const user = this.client.users.resolveID(options); if (user) return this._fetchSingle({ user, cache: true }); if (options.user) { - options.user = this.client.users.resolveID(options.user); - if (options.user) return this._fetchSingle(options); + if (Array.isArray(options.user)) { + options.user = options.user.map(u => this.client.users.resolveID(u)); + return this._fetchMany(options); + } else { + options.user = this.client.users.resolveID(options.user); + } + if (!options.limit && !options.withPresences) return this._fetchSingle(options); } return this._fetchMany(options); } @@ -200,32 +212,38 @@ class GuildMemberManager extends BaseManager { .then(data => this.add(data, cache)); } - _fetchMany({ query = '', limit = 0 } = {}) { + _fetchMany({ limit = 0, withPresences: presences = false, user: user_ids, query } = {}) { return new Promise((resolve, reject) => { - if (this.guild.memberCount === this.cache.size && !query && !limit) { + if (this.guild.memberCount === this.cache.size && (!query && !limit && !presences && !user_ids)) { resolve(this.cache); return; } + if (!query && !user_ids) query = ''; this.guild.shard.send({ op: OPCodes.REQUEST_GUILD_MEMBERS, d: { guild_id: this.guild.id, + presences, + user_ids, query, limit, }, }); const fetchedMembers = new Collection(); + const option = query || limit || presences || user_ids; const handler = (members, guild) => { if (guild.id !== this.guild.id) return; timeout.refresh(); for (const member of members.values()) { - if (query || limit) fetchedMembers.set(member.id, member); + if (option) fetchedMembers.set(member.id, member); } if (this.guild.memberCount <= this.cache.size || - ((query || limit) && members.size < 1000) || + (option && members.size < 1000) || (limit && fetchedMembers.size >= limit)) { this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); - resolve(query || limit ? fetchedMembers : this.cache); + let fetched = option ? fetchedMembers : this.cache; + if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first(); + resolve(fetched); } }; const timeout = this.guild.client.setTimeout(() => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 1bc54ae4..42b05a31 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1784,7 +1784,7 @@ declare module 'discord.js' { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; public ban(user: UserResolvable, options?: BanOptions): Promise; - public fetch(options: UserResolvable | FetchMemberOptions): Promise; + public fetch(options: UserResolvable | FetchMemberOptions | (FetchMembersOptions & { user: UserResolvable })): Promise; public fetch(options?: FetchMembersOptions): Promise>; public prune(options: GuildPruneMembersOptions & { dry?: false; count: false; }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; @@ -2207,8 +2207,10 @@ declare module 'discord.js' { } interface FetchMembersOptions { + user?: UserResolvable | UserResolvable[]; query?: string; limit?: number; + withPresences?: boolean; } interface FileOptions { From f95df6f7d7aa59f5b425a1da611deaea2d13b0e8 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 29 Feb 2020 13:18:57 +0000 Subject: [PATCH 422/428] fix(Shard): cleanup after settling spawn promise (#3799) * clear sharding ready timeout * fix oops * update typings * commited to the wrong branch * fix(Shard): cleanup after settling the spawn promise Co-authored-by: SpaceEEC --- src/sharding/Shard.js | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 7e016fbe..f394b974 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -131,10 +131,37 @@ class Shard extends EventEmitter { if (spawnTimeout === -1 || spawnTimeout === Infinity) return this.process || this.worker; 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)), spawnTimeout); + const cleanup = () => { + clearTimeout(spawnTimeoutTimer); + this.off('ready', onReady); + this.off('disconnect', onDisconnect); + this.off('death', onDeath); + }; + + const onReady = () => { + cleanup(); + resolve(); + }; + + const onDisconnect = () => { + cleanup(); + reject(new Error('SHARDING_READY_DISCONNECTED', this.id)); + }; + + const onDeath = () => { + cleanup(); + reject(new Error('SHARDING_READY_DIED', this.id)); + }; + + const onTimeout = () => { + cleanup(); + reject(new Error('SHARDING_READY_TIMEOUT', this.id)); + }; + + const spawnTimeoutTimer = setTimeout(onTimeout, spawnTimeout); + this.once('ready', onReady); + this.once('disconnect', onDisconnect); + this.once('death', onDeath); }); return this.process || this.worker; } From e4f567c65ef3dfd1f0ae7ddc40a862445549f9d4 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 29 Feb 2020 13:19:21 +0000 Subject: [PATCH 423/428] =?UTF-8?q?refactor(GuildChannel):=20change=20over?= =?UTF-8?q?writePermisions=20to=20accept=20an=E2=80=A6=20(#3853)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(GuildChannel): change overwritePermisions to no longer accept an object * fix: check for instanceof Collection too --- src/structures/GuildChannel.js | 24 ++++++++++++++---------- typings/index.d.ts | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index d80f5b24..349dcad1 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -188,24 +188,28 @@ class GuildChannel extends Channel { /** * Replaces the permission overwrites in this channel. - * @param {Object} [options] Options - * @param {OverwriteResolvable[]|Collection} [options.permissionOverwrites] + * @param {OverwriteResolvable[]|Collection} overwrites * Permission overwrites the channel gets updated with - * @param {string} [options.reason] Reason for updating the channel overwrites + * @param {string} [reason] Reason for updating the channel overwrites * @returns {Promise} * @example - * channel.overwritePermissions({ - * permissionOverwrites: [ + * channel.overwritePermissions([ * { * id: message.author.id, * deny: ['VIEW_CHANNEL'], * }, - * ], - * reason: 'Needed to change permissions' - * }); + * ], 'Needed to change permissions'); */ - overwritePermissions(options = {}) { - return this.edit(options).then(() => this); + overwritePermissions(overwrites, reason) { + if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) { + return Promise.reject(new TypeError( + 'INVALID_TYPE', + 'overwrites', + 'Array or Collection of Permission Overwrites', + true, + )); + } + return this.edit({ permissionOverwrites: overwrites, reason }).then(() => this); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 42b05a31..659a1b89 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -787,7 +787,7 @@ declare module 'discord.js' { public equals(channel: GuildChannel): boolean; public fetchInvites(): Promise>; public lockPermissions(): Promise; - public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string; }): Promise; + public overwritePermissions(overwrites: OverwriteResolvable[] | Collection, reason?: string): Promise; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; public setName(name: string, reason?: string): Promise; public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean; reason?: string; }): Promise; From bbe169deac64a65f283f76b65e56c59808584f82 Mon Sep 17 00:00:00 2001 From: Bence <56838314+1s3k3b@users.noreply.github.com> Date: Sat, 29 Feb 2020 14:19:56 +0100 Subject: [PATCH 424/428] fix(MessageEmbed): prevent possible destructuring error --- src/structures/MessageEmbed.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 42750c42..35e2ec2b 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -411,7 +411,13 @@ class MessageEmbed { * @returns {EmbedField[]} */ static normalizeFields(...fields) { - return fields.flat(2).map(({ name, value, inline }) => this.normalizeField(name, value, inline)); + return fields.flat(2).map(field => + this.normalizeField( + field && field.name, + field && field.value, + field && typeof field.inline === 'boolean' ? field.inline : false, + ), + ); } } From 9c8aaf1bbcc220299e65cadb6ac54e1a7f208576 Mon Sep 17 00:00:00 2001 From: Papa Date: Sat, 29 Feb 2020 06:20:39 -0700 Subject: [PATCH 425/428] feat: reimplement disableEveryone into disableMentions * User input sanitation: reimplement disableEveryone into disableMentions * Change default value of ClientOptions#disableMentions to 'none' * Update type declarations of disableMentions to include default * update for compliance with ESLint * Overlooked these files. Updated for complete compliance with ESLint --- src/client/Client.js | 4 +- src/structures/APIMessage.js | 10 ++++- src/structures/Webhook.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 4 +- src/util/Constants.js | 5 ++- src/util/Util.js | 43 ++++++++++++------- test/disableMentions.js | 4 +- typings/index.d.ts | 6 +-- 8 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index e2abbfa7..a23e5a69 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -386,8 +386,8 @@ class Client extends BaseClient { if (typeof options.fetchAllMembers !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean'); } - if (typeof options.disableMentions !== 'boolean') { - throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a boolean'); + if (typeof options.disableMentions !== 'string') { + throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a string'); } if (!Array.isArray(options.partials)) { throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 3a52a460..385640a8 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -91,8 +91,16 @@ class APIMessage { const disableMentions = typeof this.options.disableMentions === 'undefined' ? this.target.client.options.disableMentions : this.options.disableMentions; - if (disableMentions) { + if (disableMentions === 'all') { content = Util.removeMentions(content || ''); + } else if (disableMentions === 'everyone') { + content = (content || '').replace(/@([^<>@ ]*)/gsmu, (match, target) => { + if (target.match(/^[&!]?\d+$/)) { + return `@${target}`; + } else { + return `@\u200b${target}`; + } + }); } const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index c7604599..6a5be72b 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -85,8 +85,8 @@ class Webhook { * @property {string} [nonce=''] The nonce for the message * @property {Object[]} [embeds] An array of embeds for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space - * should be placed after every @ character to prevent unexpected mentions + * @property {'none' | 'all' | 'everyone'} [disableMentions=this.client.options.disableMentions] Whether or not + * all mentions or everyone/here mentions should be sanitized to prevent unexpected mentions * @property {FileOptions[]|string[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 38a9dcb3..b2abf50a 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -57,8 +57,8 @@ class TextBasedChannel { * @property {string} [content=''] The content for the message * @property {MessageEmbed|Object} [embed] An embed for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space - * should be placed after every @ character to prevent unexpected mentions + * @property {'none' | 'all' | 'everyone'} [disableMentions=this.client.options.disableMentions] Whether or not + * all mentionsor everyone/here mentions should be sanitized to prevent unexpected mentions * @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/util/Constants.js b/src/util/Constants.js index 547ffde1..604a13f9 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -21,7 +21,8 @@ const browser = exports.browser = typeof window !== 'undefined'; * the message cache lifetime (in seconds, 0 for never) * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as * upon joining a guild (should be avoided whenever possible) - * @property {boolean} [disableMentions=false] Default value for {@link MessageOptions#disableMentions} + * @property {'none' | 'all' | 'everyone'} [disableMentions='none'] Default value + * for {@link MessageOptions#disableMentions} * @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when * they're missing all the data for a particular structure. See the "Partials" topic listed in the sidebar for some * important usage information, as partials require you to put checks in place when handling data. @@ -47,7 +48,7 @@ exports.DefaultOptions = { messageCacheLifetime: 0, messageSweepInterval: 0, fetchAllMembers: false, - disableMentions: false, + disableMentions: 'none', partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], diff --git a/src/util/Util.js b/src/util/Util.js index bc2f0f81..a7c2a23d 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -534,22 +534,30 @@ class Util { * @returns {string} */ static cleanContent(str, message) { - return Util.removeMentions(str - .replace(/<@!?[0-9]+>/g, input => { - const id = input.replace(/<|!|>|@/g, ''); - if (message.channel.type === 'dm') { - const user = message.client.users.cache.get(id); - return user ? `@${user.username}` : input; - } - - const member = message.channel.guild.members.cache.get(id); - if (member) { - return `@${member.displayName}`; + if (message.client.options.disableMentions === 'everyone') { + str = str.replace(/@([^<>@ ]*)/gsmu, (match, target) => { + if (target.match(/^[&!]?\d+$/)) { + return `@${target}`; } else { - const user = message.client.users.cache.get(id); - return user ? `@${user.username}` : input; + return `@\u200b${target}`; } - }) + }); + } + str = str.replace(/<@!?[0-9]+>/g, input => { + const id = input.replace(/<|!|>|@/g, ''); + if (message.channel.type === 'dm') { + const user = message.client.users.cache.get(id); + return user ? `@${user.username}` : input; + } + + const member = message.channel.guild.members.cache.get(id); + if (member) { + return `@${member.displayName}`; + } else { + const user = message.client.users.cache.get(id); + return user ? `@${user.username}` : input; + } + }) .replace(/<#[0-9]+>/g, input => { const channel = message.client.channels.cache.get(input.replace(/<|#|>/g, '')); return channel ? `#${channel.name}` : input; @@ -558,7 +566,12 @@ class Util { if (message.channel.type === 'dm') return input; const role = message.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; - })); + }); + if (message.client.options.disableMentions === 'all') { + return Util.removeMentions(str); + } else { + return str; + } } /** diff --git a/test/disableMentions.js b/test/disableMentions.js index 281633d6..f227a0b4 100644 --- a/test/disableMentions.js +++ b/test/disableMentions.js @@ -6,7 +6,7 @@ const client = new Discord.Client({ // To see a difference, comment out disableMentions and run the same tests using disableEveryone // You will notice that all messages will mention @everyone //disableEveryone: true - disableMentions: true + disableMentions: 'everyone' }); const tests = [ @@ -44,4 +44,4 @@ client.on('message', message => { message.reply(tests[2]); }); -client.login(token).catch(console.error); \ No newline at end of file +client.login(token).catch(console.error); diff --git a/typings/index.d.ts b/typings/index.d.ts index 659a1b89..cac46ed0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2062,7 +2062,7 @@ declare module 'discord.js' { messageCacheLifetime?: number; messageSweepInterval?: number; fetchAllMembers?: boolean; - disableMentions?: boolean; + disableMentions?: 'none' | 'all' | 'everyone'; partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; @@ -2514,7 +2514,7 @@ declare module 'discord.js' { nonce?: string; content?: string; embed?: MessageEmbed | MessageEmbedOptions; - disableMentions?: boolean; + disableMentions?: 'none' | 'all' | 'everyone'; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; @@ -2763,7 +2763,7 @@ declare module 'discord.js' { tts?: boolean; nonce?: string; embeds?: (MessageEmbed | object)[]; - disableMentions?: boolean; + disableMentions?: 'none' | 'all' | 'everyone'; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; From 6650d50a56c42aca9657b627d2333349bd05b181 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Sun, 1 Mar 2020 00:21:43 +1100 Subject: [PATCH 426/428] =?UTF-8?q?feat(MessageEmbed):=20Support=20EmbedFi?= =?UTF-8?q?eldData[]=20instead=20of=20EmbedFi=E2=80=A6=20(#3845)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(typings): MessageEmbedOptions#fields should be EmbedFieldData[] * feat(MessageEmbed): Allow optional EmbedFieldData#inline in constructor * docs(MessageEmbed): revert type change of fields Co-authored-by: Crawl Co-authored-by: SpaceEEC --- src/structures/MessageEmbed.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 35e2ec2b..905433e2 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -65,7 +65,7 @@ class MessageEmbed { * The fields of this embed * @type {EmbedField[]} */ - this.fields = data.fields ? data.fields.map(Util.cloneObject) : []; + this.fields = data.fields ? this.constructor.normalizeFields(data.fields) : []; /** * @typedef {Object} MessageEmbedThumbnail diff --git a/typings/index.d.ts b/typings/index.d.ts index cac46ed0..ad02dc09 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2469,7 +2469,7 @@ declare module 'discord.js' { url?: string; timestamp?: Date | number; color?: ColorResolvable; - fields?: EmbedField[]; + fields?: EmbedFieldData[]; files?: (MessageAttachment | string | FileOptions)[]; author?: Partial & { icon_url?: string; proxy_icon_url?: string; }; thumbnail?: Partial & { proxy_url?: string; }; From c065156a8883d81e58f3603e468289344f8a20a5 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 29 Feb 2020 14:35:57 +0100 Subject: [PATCH 427/428] chore: consistency/prettier (#3852) * chore: consistency/prettier * chore: rebase * chore: rebase * chore: include typings * fix: include typings file in prettier lint-staged --- .eslintrc.json | 87 +- .github/CONTRIBUTING.md | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 8 +- .github/ISSUE_TEMPLATE/feature_request.md | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 5 +- .tern-project | 8 +- README.md | 42 +- docs/examples/attachments.md | 70 +- docs/examples/embed.js | 4 +- docs/examples/moderation.md | 68 +- docs/general/faq.md | 6 +- docs/general/updating.md | 78 +- docs/general/welcome.md | 32 +- docs/topics/partials.md | 2 +- docs/topics/voice.md | 28 +- docs/topics/web.md | 7 + jsdoc.json | 2 +- package.json | 16 +- src/client/BaseClient.js | 2 +- src/client/Client.js | 74 +- src/client/WebhookClient.js | 2 +- src/client/actions/Action.js | 65 +- src/client/actions/ChannelDelete.js | 2 +- src/client/actions/GuildIntegrationsUpdate.js | 1 - src/client/actions/GuildMemberRemove.js | 1 - src/client/actions/GuildRoleCreate.js | 1 - src/client/actions/GuildRoleDelete.js | 1 - src/client/actions/GuildRoleUpdate.js | 1 - src/client/actions/GuildUpdate.js | 1 - src/client/actions/MessageCreate.js | 1 - src/client/actions/MessageDelete.js | 1 - src/client/actions/MessageDeleteBulk.js | 13 +- src/client/actions/MessageReactionRemove.js | 1 - src/client/actions/VoiceStateUpdate.js | 9 +- src/client/actions/WebhooksUpdate.js | 1 - src/client/voice/ClientVoiceManager.js | 7 +- src/client/voice/VoiceBroadcast.js | 7 +- src/client/voice/VoiceConnection.js | 72 +- .../voice/dispatcher/StreamDispatcher.js | 38 +- src/client/voice/networking/VoiceUDPClient.js | 4 +- src/client/voice/networking/VoiceWebSocket.js | 7 +- src/client/voice/player/BasePlayer.js | 14 +- .../voice/player/BroadcastAudioPlayer.js | 4 +- src/client/voice/receiver/PacketHandler.js | 8 +- src/client/voice/util/PlayInterface.js | 4 +- src/client/voice/util/Silence.js | 2 +- src/client/voice/util/VolumeInterface.js | 13 +- src/client/websocket/WebSocketManager.js | 10 +- src/client/websocket/WebSocketShard.js | 26 +- .../websocket/handlers/CHANNEL_UPDATE.js | 1 - src/client/websocket/handlers/GUILD_CREATE.js | 12 +- .../websocket/handlers/GUILD_MEMBERS_CHUNK.js | 2 +- .../websocket/handlers/GUILD_MEMBER_ADD.js | 10 +- src/client/websocket/handlers/TYPING_START.js | 12 +- src/errors/Messages.js | 16 +- src/managers/BaseManager.js | 6 +- src/managers/ChannelManager.js | 10 +- src/managers/GuildChannelManager.js | 2 +- src/managers/GuildEmojiManager.js | 21 +- src/managers/GuildEmojiRoleManager.js | 8 +- src/managers/GuildManager.js | 74 +- src/managers/GuildMemberManager.js | 41 +- src/managers/GuildMemberRoleManager.js | 20 +- src/managers/MessageManager.js | 140 +- src/managers/PresenceManager.js | 26 +- src/managers/ReactionManager.js | 40 +- src/managers/ReactionUserManager.js | 13 +- src/managers/RoleManager.js | 21 +- src/managers/UserManager.js | 2 +- src/rest/APIRequest.js | 15 +- src/rest/APIRouter.js | 24 +- src/rest/DiscordAPIError.js | 2 +- src/rest/RESTManager.js | 18 +- src/rest/RequestHandler.js | 14 +- src/sharding/Shard.js | 16 +- src/sharding/ShardClientUtil.js | 41 +- src/sharding/ShardingManager.js | 39 +- src/structures/APIMessage.js | 22 +- src/structures/Base.js | 4 +- src/structures/Channel.js | 7 +- src/structures/ClientApplication.js | 30 +- src/structures/ClientPresence.js | 48 +- src/structures/ClientUser.js | 6 +- src/structures/Emoji.js | 3 +- src/structures/Guild.js | 277 +- src/structures/GuildAuditLogs.js | 174 +- src/structures/GuildChannel.js | 137 +- src/structures/GuildEmoji.js | 32 +- src/structures/GuildMember.js | 32 +- src/structures/Integration.js | 15 +- src/structures/Invite.js | 10 +- src/structures/Message.js | 136 +- src/structures/MessageEmbed.js | 138 +- src/structures/MessageMentions.js | 4 +- src/structures/MessageReaction.js | 7 +- src/structures/PermissionOverwrites.js | 19 +- src/structures/Presence.js | 51 +- src/structures/ReactionCollector.js | 3 +- src/structures/ReactionEmoji.js | 2 +- src/structures/Role.js | 82 +- src/structures/Team.js | 4 +- src/structures/TextChannel.js | 14 +- src/structures/User.js | 34 +- src/structures/VoiceChannel.js | 4 +- src/structures/VoiceState.js | 12 +- src/structures/Webhook.js | 54 +- src/structures/interfaces/Collector.js | 2 +- src/structures/interfaces/TextBasedChannel.js | 50 +- src/util/Collection.js | 2 +- src/util/Constants.js | 105 +- src/util/DataResolver.js | 6 +- src/util/Snowflake.js | 12 +- src/util/Structures.js | 2 +- src/util/Util.js | 148 +- test/disableMentions.js | 47 +- test/escapeMarkdown.test.js | 147 +- test/random.js | 121 +- test/sendtest.js | 81 +- test/shard.js | 14 +- test/tester1000.js | 9 +- test/voice.js | 36 +- test/webhooktest.js | 95 +- test/webpack.html | 47 +- tsconfig.json | 32 +- tslint.json | 54 +- typings/index.d.ts | 5828 +++++++++-------- webpack.config.js | 6 +- 127 files changed, 5198 insertions(+), 4513 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 54950034..f524f5f7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,6 @@ { - "extends": "eslint:recommended", + "extends": ["eslint:recommended", "plugin:prettier/recommended"], + "plugins": ["import"], "parserOptions": { "ecmaVersion": 2019 }, @@ -7,42 +8,58 @@ "es6": true, "node": true }, - "overrides": [ - { "files": ["*.browser.js"], "env": { "browser": true } } - ], + "overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }], "rules": { + "import/order": [ + "error", + { + "groups": ["builtin", "external", "internal", "index", "sibling", "parent"], + "alphabetize": { + "order": "asc" + } + } + ], + "prettier/prettier": [ + 2, + { + "printWidth": 120, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "all", + "endOfLine": "lf" + } + ], "strict": ["error", "global"], "no-await-in-loop": "warn", "no-compare-neg-zero": "error", - "no-extra-parens": ["warn", "all", { - "nestedBinaryExpressions": false - }], "no-template-curly-in-string": "error", "no-unsafe-negation": "error", - "valid-jsdoc": ["error", { - "requireReturn": false, - "requireReturnDescription": false, - "prefer": { - "return": "returns", - "arg": "param" - }, - "preferType": { - "String": "string", - "Number": "number", - "Boolean": "boolean", - "Symbol": "symbol", - "object": "Object", - "function": "Function", - "array": "Array", - "date": "Date", - "error": "Error", - "null": "void" + "valid-jsdoc": [ + "error", + { + "requireReturn": false, + "requireReturnDescription": false, + "prefer": { + "return": "returns", + "arg": "param" + }, + "preferType": { + "String": "string", + "Number": "number", + "Boolean": "boolean", + "Symbol": "symbol", + "object": "Object", + "function": "Function", + "array": "Array", + "date": "Date", + "error": "Error", + "null": "void" + } } - }], + ], "accessor-pairs": "warn", "array-callback-return": "error", - "complexity": "warn", "consistent-return": "error", "curly": ["error", "multi-line", "consistent"], "dot-location": ["error", "property"], @@ -100,7 +117,6 @@ "func-names": "error", "func-name-matching": "error", "func-style": ["error", "declaration", { "allowArrowFunctions": true }], - "indent": ["error", 2, { "SwitchCase": 1 }], "key-spacing": "error", "keyword-spacing": "error", "max-depth": "error", @@ -112,7 +128,6 @@ "no-array-constructor": "error", "no-inline-comments": "error", "no-lonely-if": "error", - "no-mixed-operators": "error", "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], "no-new-object": "error", "no-spaced-func": "error", @@ -122,18 +137,20 @@ "nonblock-statement-body-position": "error", "object-curly-spacing": ["error", "always"], "operator-assignment": "error", - "operator-linebreak": ["error", "after"], "padded-blocks": ["error", "never"], "quote-props": ["error", "as-needed"], "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], "semi-spacing": "error", "semi": "error", "space-before-blocks": "error", - "space-before-function-paren": ["error", { - "anonymous": "never", - "named": "never", - "asyncArrow": "always" - }], + "space-before-function-paren": [ + "error", + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + } + ], "space-in-parens": "error", "space-infix-ops": "error", "space-unary-ops": "error", diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fd6de724..84ecd2d4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,6 +7,7 @@ pull request. We use ESLint to enforce a consistent coding style, so having that is a great boon to your development process. ## Setup + To get ready to work on the codebase, please do the following: 1. Fork & clone the repository, and make sure you're on the **master** branch diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d5061af2..caa9425b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,12 +1,11 @@ --- + name: Bug report about: Report incorrect or unexpected behaviour of discord.js title: '' labels: 's: unverified, type: bug' assignees: '' - --- -