From cd08a3b5a4569e98aaa9c954d578ac9c12fba826 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 26 Oct 2017 09:52:03 +0200 Subject: [PATCH 01/75] refactor(Actions): remove obsolete user and guild member get actions (#2061) The UserGetAction was never used. The GuildMemberGetAction was only once used. Easily replaced with a shorter and more comprehensible line. (Also consistent with the rest of the library) --- src/client/actions/ActionsManager.js | 2 -- src/client/actions/GuildMemberGet.js | 10 ---------- src/client/actions/UserGet.js | 11 ----------- src/structures/Guild.js | 2 +- 4 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 src/client/actions/GuildMemberGet.js delete mode 100644 src/client/actions/UserGet.js diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 8341e7453..9708d17b2 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -14,13 +14,11 @@ class ActionsManager { this.register(require('./ChannelUpdate')); this.register(require('./GuildDelete')); this.register(require('./GuildUpdate')); - this.register(require('./GuildMemberGet')); this.register(require('./GuildMemberRemove')); this.register(require('./GuildBanRemove')); this.register(require('./GuildRoleCreate')); this.register(require('./GuildRoleDelete')); this.register(require('./GuildRoleUpdate')); - this.register(require('./UserGet')); this.register(require('./UserUpdate')); this.register(require('./UserNoteUpdate')); this.register(require('./GuildSync')); diff --git a/src/client/actions/GuildMemberGet.js b/src/client/actions/GuildMemberGet.js deleted file mode 100644 index 5bf2aafec..000000000 --- a/src/client/actions/GuildMemberGet.js +++ /dev/null @@ -1,10 +0,0 @@ -const Action = require('./Action'); - -class GuildMemberGetAction extends Action { - handle(guild, data) { - const member = guild.members.create(data); - return { member }; - } -} - -module.exports = GuildMemberGetAction; diff --git a/src/client/actions/UserGet.js b/src/client/actions/UserGet.js deleted file mode 100644 index 4a135dd58..000000000 --- a/src/client/actions/UserGet.js +++ /dev/null @@ -1,11 +0,0 @@ -const Action = require('./Action'); - -class UserGetAction extends Action { - handle(data) { - const client = this.client; - const user = client.users.create(data); - return { user }; - } -} - -module.exports = UserGetAction; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index d96329be0..b3ea8535d 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -542,7 +542,7 @@ class Guild extends Base { } } return this.client.api.guilds(this.id).members(user.id).put({ data: options }) - .then(data => this.client.actions.GuildMemberGet.handle(this, data).member); + .then(data => this.members.create(data)); } /** From bc30fdd8671d3a82ca90ede3c7df571ca3f0ecf2 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 26 Oct 2017 14:07:27 -0500 Subject: [PATCH 02/75] Fix Avatar URL generation bug (#2063) --- src/util/Constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index aed65b8ee..7a0b262d3 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -114,6 +114,7 @@ exports.Endpoints = { Asset: name => `${root}/assets/${name}`, DefaultAvatar: number => `${root}/embed/avatars/${number}.png`, Avatar: (userID, hash, format = 'default', size) => { + if (userID === '1') return hash; if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); }, From cd54e9317ff0396a34bafacce2ff2c056a4b0cee Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 27 Oct 2017 08:36:53 -0500 Subject: [PATCH 03/75] Time Difference in REST (#2057) --- src/rest/RESTManager.js | 10 ++++++++++ src/rest/handlers/RequestHandler.js | 7 +++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 48476cca6..d62423979 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -12,12 +12,22 @@ class RESTManager { this.globallyRateLimited = false; this.tokenPrefix = tokenPrefix; this.versioned = true; + this.timeDifferences = []; } get api() { return routeBuilder(this); } + get timeDifference() { + return Math.round(this.timeDifferences.reduce((a, b) => a + b, 0) / this.timeDifferences.length); + } + + set timeDifference(ms) { + this.timeDifferences.unshift(ms); + if (this.timeDifferences.length > 5) this.timeDifferences.length = 5; + } + getAuth() { const token = this.client.token || this.client.accessToken; const prefixed = !!this.client.application || (this.client.user && this.client.user.bot); diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js index aaeed9d69..64e9ea72b 100644 --- a/src/rest/handlers/RequestHandler.js +++ b/src/rest/handlers/RequestHandler.js @@ -9,7 +9,6 @@ class RequestHandler { this.limit = Infinity; this.resetTime = null; this.remaining = 1; - this.timeDifference = 0; this.queue = []; } @@ -32,7 +31,7 @@ class RequestHandler { const finish = timeout => { if (timeout || this.limited) { if (!timeout) { - timeout = this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset; + timeout = this.resetTime - Date.now() + this.manager.timeDifference + this.client.options.restTimeOffset; } // eslint-disable-next-line prefer-promise-reject-errors reject({ timeout }); @@ -50,7 +49,7 @@ class RequestHandler { this.client.emit(RATE_LIMIT, { timeout, limit: this.limit, - timeDifference: this.timeDifference, + timeDifference: this.manager.timeDifference, method: item.request.method, path: item.request.path, route: item.request.route, @@ -66,7 +65,7 @@ class RequestHandler { this.limit = Number(res.headers['x-ratelimit-limit']); this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; this.remaining = Number(res.headers['x-ratelimit-remaining']); - this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + this.manager.timeDifference = Date.now() - new Date(res.headers.date).getTime(); } if (err) { if (err.status === 429) { From 0fc9459450341ae894dcbf62939f43bbca0d6c79 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 27 Oct 2017 10:34:18 -0400 Subject: [PATCH 04/75] Add TextChannel#setNSFW method (#2050) * Add TextChannel#setNSFW method * Doesn't look like anything to me * butts --- src/structures/GuildChannel.js | 2 ++ src/structures/TextChannel.js | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 66efd8c2b..a1e8d53a0 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -248,6 +248,7 @@ class GuildChannel extends Channel { * @property {string} [name] The name of the channel * @property {number} [position] The position 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 voice channel * @property {Snowflake} [parentID] The parent ID of the channel @@ -290,6 +291,7 @@ class GuildChannel extends Channel { data: { name: (data.name || this.name).trim(), topic: data.topic, + nsfw: data.nsfw, bitrate: data.bitrate || (this.bitrate ? this.bitrate * 1000 : undefined), user_limit: data.userLimit != null ? data.userLimit : this.userLimit, // eslint-disable-line eqeqeq parent_id: data.parentID, diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 6529f46b9..5679f841a 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -38,6 +38,16 @@ class TextChannel extends GuildChannel { if (data.messages) for (const message of data.messages) this.messages.create(message); } + /** + * Sets whether this channel is flagged as NSFW. + * @param {boolean} nsfw Whether the channel should be considered NSFW + * @param {string} [reason] Reason for changing the channel's NSFW flag + * @returns {Promise} + */ + setNSFW(nsfw, reason) { + return this.edit({ nsfw }, reason); + } + /** * Fetches all webhooks for the channel. * @returns {Promise>} From 291af7e845126d10a5fd95c6a6576e0e40a9e0ac Mon Sep 17 00:00:00 2001 From: Johnson Chen Date: Sat, 28 Oct 2017 20:35:38 +1100 Subject: [PATCH 05/75] Change recent to timestamp because DiscordAPIError (#2065) are lovely arn't they? --- src/structures/shared/Search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js index 971f2483d..a852a5086 100644 --- a/src/structures/shared/Search.js +++ b/src/structures/shared/Search.js @@ -11,7 +11,7 @@ const { TypeError } = require('../../errors'); * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) * @property {UserResolvable} [author] Author to limit search * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) - * @property {string} [sortBy='recent'] `recent` or `relevant` + * @property {string} [sortBy='timestamp'] `timestamp` or `relevant` * @property {string} [sortOrder='descending'] `ascending` or `descending` * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) * @property {number} [limit=25] Maximum number of results to get (1 to 25) From 0a05761b49dbd3970234ddaf80484fc78a1afbda Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 28 Oct 2017 18:55:59 +0200 Subject: [PATCH 06/75] Add new exports and remove a deprecated one (#2068) * add new exports and remove a deprecated one * fix incorrect require path --- src/index.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 746aab001..7bea36f1b 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,6 @@ module.exports = { DataResolver: require('./util/DataResolver'), DataStore: require('./stores/DataStore'), DiscordAPIError: require('./rest/DiscordAPIError'), - EvaluatedPermissions: require('./util/Permissions'), Permissions: require('./util/Permissions'), Snowflake: require('./util/Snowflake'), SnowflakeUtil: require('./util/Snowflake'), @@ -23,15 +22,32 @@ module.exports = { util: Util, version: require('../package.json').version, + // Stores + ChannelStore: require('./stores/ChannelStore'), + ClientPresenceStore: require('./stores/ClientPresenceStore'), + EmojiStore: require('./stores/EmojiStore'), + GuildChannelStore: require('./stores/GuildChannelStore'), + GuildMemberStore: require('./stores/GuildMemberStore'), + GuildStore: require('./stores/GuildStore'), + MessageStore: require('./stores/MessageStore'), + PresenceStore: require('./stores/PresenceStore'), + RoleStore: require('./stores/RoleStore'), + UserStore: require('./stores/UserStore'), + // Shortcuts to Util methods escapeMarkdown: Util.escapeMarkdown, fetchRecommendedShards: Util.fetchRecommendedShards, splitMessage: Util.splitMessage, // Structures + Base: require('./structures/Base'), Activity: require('./structures/Presence').Activity, + CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), + ClientApplication: require('./structures/ClientApplication'), ClientUser: require('./structures/ClientUser'), + ClientUserChannelOverride: require('./structures/ClientUserChannelOverride'), + ClientUserGuildSettings: require('./structures/ClientUserGuildSettings'), ClientUserSettings: require('./structures/ClientUserSettings'), Collector: require('./structures/interfaces/Collector'), DMChannel: require('./structures/DMChannel'), @@ -48,15 +64,17 @@ module.exports = { MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), MessageReaction: require('./structures/MessageReaction'), - ClientApplication: require('./structures/ClientApplication'), PermissionOverwrites: require('./structures/PermissionOverwrites'), Presence: require('./structures/Presence').Presence, - ReactionEmoji: require('./structures/ReactionEmoji'), ReactionCollector: require('./structures/ReactionCollector'), + ReactionEmoji: require('./structures/ReactionEmoji'), + RichPresenceAssets: require('./structures/Presence').RichPresenceAssets, Role: require('./structures/Role'), TextChannel: require('./structures/TextChannel'), User: require('./structures/User'), + UserConnection: require('./structures/UserConnection'), VoiceChannel: require('./structures/VoiceChannel'), + VoiceRegion: require('./structures/VoiceRegion'), Webhook: require('./structures/Webhook'), WebSocket: require('./WebSocket'), From cd3d3344e8c34e0950584b20bbe984b411fc23c5 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 28 Oct 2017 18:57:16 +0200 Subject: [PATCH 07/75] fix(GuildMember#hasPermission): pass correct parameters to Permissions#has (#2070) Also removed deprecated parameter of the method itself. --- src/structures/GuildMember.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index c4c2d6a81..935ba1084 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -294,19 +294,13 @@ class GuildMember extends Base { /** * Checks if any of the member's roles have a permission. * @param {PermissionResolvable|PermissionResolvable[]} permission Permission(s) to check for - * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permission - * **(deprecated)** - * @param {boolean} [checkAdmin] Whether to allow the administrator permission to override - * (takes priority over `explicit`) - * @param {boolean} [checkOwner] Whether to allow being the guild's owner to override - * (takes priority over `explicit`) + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @param {boolean} [checkOwner=true] Whether to allow being the guild's owner to override * @returns {boolean} */ - hasPermission(permission, explicit = false, checkAdmin, checkOwner) { - if (typeof checkAdmin === 'undefined') checkAdmin = !explicit; - if (typeof checkOwner === 'undefined') checkOwner = !explicit; + 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, undefined, checkAdmin)); + return this.roles.some(r => r.permissions.has(permission, checkAdmin)); } /** From cda408534ada7f50b2d3a3a613b912b3548723d6 Mon Sep 17 00:00:00 2001 From: bdistin Date: Sat, 28 Oct 2017 11:57:50 -0500 Subject: [PATCH 08/75] user.patch shouldn't try to touch the token (#2072) --- src/structures/ClientUser.js | 2 ++ src/structures/User.js | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index cc19ad58b..ce71d9bcf 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -88,6 +88,8 @@ class ClientUser extends User { this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(this.client, settings)); } } + + if (data.token) this.client.token = data.token; } /** diff --git a/src/structures/User.js b/src/structures/User.js index 814d3a90c..f12d67ffa 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -63,8 +63,6 @@ class User extends Base { * @type {?Message} */ this.lastMessage = null; - - if (data.token) this.client.token = data.token; } /** From b255af0825776eed430bbba0f96c4b8546bab926 Mon Sep 17 00:00:00 2001 From: bdistin Date: Sat, 28 Oct 2017 11:58:27 -0500 Subject: [PATCH 09/75] Fix user.bot (#2073) * fix user.bot * user.avatar is nullable (docs) --- src/structures/User.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/structures/User.js b/src/structures/User.js index f12d67ffa..2a184b1d4 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -20,6 +20,13 @@ class User extends Base { */ this.id = data.id; + /** + * Whether or not the user is a bot + * @type {boolean} + * @name User#bot + */ + this.bot = Boolean(data.bot); + this._patch(data); } @@ -40,18 +47,11 @@ class User extends Base { /** * The ID of the user's avatar - * @type {string} + * @type {?string} * @name User#avatar */ if (typeof data.avatar !== 'undefined') this.avatar = data.avatar; - /** - * Whether or not the user is a bot - * @type {boolean} - * @name User#bot - */ - if (typeof this.bot === 'undefined' && typeof data.bot !== 'undefined') this.bot = Boolean(data.bot); - /** * The ID of the last message sent by the user, if one was sent * @type {?Snowflake} From c495ea025a37b0d6e1f645407719573a1e6b9083 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 28 Oct 2017 11:58:46 -0500 Subject: [PATCH 10/75] fix raw event (#2074) --- src/client/websocket/WebSocketConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index a30be35a8..4e3835ef0 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -272,7 +272,7 @@ class WebSocketConnection extends EventEmitter { try { const packet = WebSocket.unpack(this.inflate.result); this.onPacket(packet); - if (this.client.listenerCount('raw')) this.client.emit('raw', data); + if (this.client.listenerCount('raw')) this.client.emit('raw', packet); } catch (err) { this.client.emit('debug', err); } From 88719f0f4209fce2a4d08cecc90fa4c8e2bdde8d Mon Sep 17 00:00:00 2001 From: William Tran Date: Sat, 28 Oct 2017 10:01:17 -0700 Subject: [PATCH 11/75] Typos in docs (#2055) * Typo in Guild.createRole docs Added missing semicolon in example code. * consistent periods in docs --- src/client/voice/VoiceBroadcast.js | 2 +- src/structures/Guild.js | 4 ++-- src/structures/Presence.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 678045dca..5a2c54c73 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -236,7 +236,7 @@ class VoiceBroadcast extends VolumeInterface { } /** - * Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) + * Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description). * @param {string} input The arbitrary input * @param {StreamOptions} [options] Options for playing the stream * @returns {VoiceBroadcast} diff --git a/src/structures/Guild.js b/src/structures/Guild.js index b3ea8535d..f3edd46ab 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -979,7 +979,7 @@ class Guild extends Base { } /** - * Creates a new role in the guild with given information + * 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 @@ -1000,7 +1000,7 @@ class Guild extends Base { * reason: 'we needed a role for Super Cool People', * }) * .then(role => console.log(`Created role ${role}`)) - * .catch(console.error) + * .catch(console.error); */ createRole({ data = {}, reason } = {}) { if (data.color) data.color = Util.resolveColor(data.color); diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 1eba8170c..91bee3ac3 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -38,7 +38,7 @@ class Presence { } /** - * Whether this presence is equal to another + * Whether this presence is equal to another. * @param {Presence} presence The presence to compare with * @returns {boolean} */ From 50ad66f5139b52bf057de0d2745d7ae73e4fa8f8 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 28 Oct 2017 12:02:12 -0500 Subject: [PATCH 12/75] clean up readme a bit (#2054) * clean up readme a bit * Update README.md --- README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6b88f4370..989d2d652 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,7 @@ NPM downloads Build status Dependencies - Patreon + Patreon

NPM info @@ -31,9 +30,9 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to **Node.js 8.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 --save` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus --save` -With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript --save` +Without voice support: `npm i discord.js` +With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm i discord.js node-opus` +With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm i 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. @@ -41,13 +40,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`) -- [bufferutil](https://www.npmjs.com/package/bufferutil) to greatly speed up the WebSocket when *not* using uws (`npm install bufferutil --save`) -- [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack --save`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm i zlib-sync`) +- [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm i 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 --save`) - - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers --save`) -- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`) + - [sodium](https://www.npmjs.com/package/sodium) (`npm i sodium`) + - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm i libsodium-wrappers`) +- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm i uws`) +- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm i bufferutil`) ## Example usage ```js @@ -70,11 +69,14 @@ client.login('your token'); ## Links * [Website](https://discord.js.org/) ([source](https://github.com/hydrabolt/discord.js-site)) * [Documentation](https://discord.js.org/#/docs) -* [Discord.js server](https://discord.gg/bRCvFy9) -* [Discord API server](https://discord.gg/rV4BwdK) +* [Discord.js Discord server](https://discord.gg/bRCvFy9) +* [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/hydrabolt/discord.js) * [NPM](https://www.npmjs.com/package/discord.js) -* [Related libraries](https://discordapi.com/unofficial/libs.html) (see also [discord-rpc](https://www.npmjs.com/package/discord-rpc)) +* [Related libraries](https://discordapi.com/unofficial/libs.html) + +### Extensions +* [discord-rpc](https://www.npmjs.com/package/discord-rpc) ([github](https://github.com/devsnek/discord-rpc)) ## Contributing Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the From a62d1e954d9e8be0a8acf3802dd4f87c367ac2a5 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 28 Oct 2017 19:03:27 +0200 Subject: [PATCH 13/75] fix(Presence): pass client and default to offline (#2071) --- src/structures/GuildMember.js | 2 +- src/structures/Presence.js | 2 +- src/structures/User.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 935ba1084..8b83796cf 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -125,7 +125,7 @@ class GuildMember extends Base { * @readonly */ get presence() { - return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(); + return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(this.client); } /** diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 91bee3ac3..e788124fc 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -19,7 +19,7 @@ class Presence { * * **`dnd`** - user is in Do Not Disturb * @type {string} */ - this.status = data.status || this.status; + this.status = data.status || this.status || 'offline'; const activity = data.game || data.activity; /** diff --git a/src/structures/User.js b/src/structures/User.js index 2a184b1d4..859cdeeb2 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -93,7 +93,7 @@ class User extends Base { for (const guild of this.client.guilds.values()) { if (guild.presences.has(this.id)) return guild.presences.get(this.id); } - return new Presence(); + return new Presence(this.client); } /** From 0101392334e48d144dda3bc1a127d1e9feb2555d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 28 Oct 2017 19:04:03 +0200 Subject: [PATCH 14/75] Documentation improvements (#2069) * docs: fix documentation in various places All stores: resolveID returns a nullable Snowflake GuildAuditLogs: ActionType also can be ALL MessageEmbed: make files property show up in the docs ClientApplication: resetSecret and resetToken return a promise ClientManager: status is readonly Guild: features is an array of strings and ban no longer accepts a number or string Guild: ban method no longer accepts a string or number GuildMember: ^ RichPresenceAssets: small and large Image are Snowflakes, also fixed parameter documentation for small and large image url method WebhookMessageOptions: file property is no longer a thing * docs: improve GuildAuditLogs documentation Prefix types with AuditLog to avoid confusion Document GuildAuditLogs' static Targets and Actions properties and add necessary typedefs Use typdefs over primitives where possible. * fix documentation for Guild#defaultRole --- src/client/ClientManager.js | 1 + src/stores/ChannelStore.js | 2 +- src/stores/DataStore.js | 2 +- src/stores/EmojiStore.js | 2 +- src/stores/GuildChannelStore.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/RoleStore.js | 2 +- src/stores/UserStore.js | 2 +- src/structures/ClientApplication.js | 4 +- src/structures/Guild.js | 6 +- src/structures/GuildAuditLogs.js | 93 +++++++++++++++++++++-------- src/structures/GuildMember.js | 2 +- src/structures/MessageEmbed.js | 12 ++-- src/structures/Presence.js | 14 +++-- src/structures/Webhook.js | 1 - 19 files changed, 100 insertions(+), 55 deletions(-) diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 9e0d570d2..ad006b8fe 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -22,6 +22,7 @@ class ClientManager { /** * The status of the client + * @readonly * @type {number} */ get status() { diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 5491b8e59..2ceee8000 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -95,7 +95,7 @@ class ChannelStore extends DataStore { * @memberof ChannelStore * @instance * @param {ChannelResolvable} channel The channel resolvable to resolve - * @returns {?string} + * @returns {?Snowflake} */ } diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 85ce17c90..398910d50 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -37,7 +37,7 @@ class DataStore extends Collection { /** * Resolves a data entry to a instance ID. * @param {string|Instance} idOrInstance The id or instance of something in this DataStore - * @returns {?string} + * @returns {?Snowflake} */ resolveID(idOrInstance) { if (idOrInstance instanceof this.holds) return idOrInstance.id; diff --git a/src/stores/EmojiStore.js b/src/stores/EmojiStore.js index 83b4df812..035cc3d4d 100644 --- a/src/stores/EmojiStore.js +++ b/src/stores/EmojiStore.js @@ -38,7 +38,7 @@ class EmojiStore extends DataStore { /** * Resolves a EmojiResolvable to a Emoji ID string. * @param {EmojiResolvable} emoji The Emoji resolvable to identify - * @returns {?string} + * @returns {?Snowflake} */ resolveID(emoji) { if (emoji instanceof ReactionEmoji) return emoji.id; diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 3e03c8110..0bc3e8c4e 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -42,7 +42,7 @@ class GuildChannelStore extends DataStore { * @memberof GuildChannelStore * @instance * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve - * @returns {?string} + * @returns {?Snowflake} */ } diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index ba8cac903..ad0acbed9 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -41,7 +41,7 @@ class GuildMemberStore extends DataStore { /** * Resolves a GuildMemberResolvable to an member ID string. * @param {GuildMemberResolvable} member The user that is part of the guild - * @returns {?string} + * @returns {?Snowflake} */ resolveID(member) { const memberResolveable = super.resolveID(member); diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index 5e3e792a4..45e9c9cfc 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -33,7 +33,7 @@ class GuildStore extends DataStore { * @memberof GuildStore * @instance * @param {GuildResolvable} guild The guild resolvable to identify - * @returns {?string} + * @returns {?Snowflake} */ } diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index 75d01620c..cc0ff60b0 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -112,7 +112,7 @@ class MessageStore extends DataStore { * @memberof MessageStore * @instance * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?string} + * @returns {?Snowflake} */ } diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js index 1c2649712..8322c9c65 100644 --- a/src/stores/PresenceStore.js +++ b/src/stores/PresenceStore.js @@ -39,7 +39,7 @@ class PresenceStore extends DataStore { /** * Resolves a PresenceResolvable to a Presence ID string. * @param {PresenceResolvable} presence The presence resolvable to resolve - * @returns {?string} + * @returns {?Snowflake} */ resolveID(presence) { const presenceResolveable = super.resolveID(presence); diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index bcbca72ac..c11b4e176 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -38,7 +38,7 @@ class ReactionStore extends DataStore { * @memberof ReactionStore * @instance * @param {MessageReactionResolvable} role The role resolvable to resolve - * @returns {?string} + * @returns {?Snowflake} */ } diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 7501cb459..bb8cd749d 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -38,7 +38,7 @@ class RoleStore extends DataStore { * @memberof RoleStore * @instance * @param {RoleResolvable} role The role resolvable to resolve - * @returns {?string} + * @returns {?Snowflake} */ } diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 6ee4909a3..432a0768a 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -35,7 +35,7 @@ class UserStore extends DataStore { /** * Resolves a UserResolvable to a user ID string. * @param {UserResolvable} user The UserResolvable to identify - * @returns {?string} + * @returns {?Snowflake} */ resolveID(user) { if (user instanceof GuildMember) return user.user.id; diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 0d2646cee..494d8c1c3 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -177,7 +177,7 @@ class ClientApplication extends Base { /** * Resets the app's secret. * This is only available when using a user account. - * @returns {ClientApplication} + * @returns {Promise} */ resetSecret() { return this.client.api.oauth2.applications[this.id].reset.post() @@ -187,7 +187,7 @@ class ClientApplication extends Base { /** * Resets the app's bot token. * This is only available when using a user account. - * @returns {ClientApplication} + * @returns {Promise} */ resetToken() { return this.client.api.oauth2.applications[this.id].bot.reset.post() diff --git a/src/structures/Guild.js b/src/structures/Guild.js index f3edd46ab..1b9463b51 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -115,7 +115,7 @@ class Guild extends Base { /** * An array of guild features - * @type {Object[]} + * @type {string[]} */ this.features = data.features; @@ -401,7 +401,7 @@ class Guild extends Base { } } - /* + /** * The `@everyone` role of the guild * @type {Role} * @readonly @@ -802,7 +802,7 @@ class Guild extends Base { /** * Bans a user from the guild. * @param {UserResolvable} user The user to ban - * @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a + * @param {Object} [options] Ban options. If a number, the number of days to delete messages for, if a * string, the ban reason. Supplying an object allows you to do both. * @param {number} [options.days=0] Number of days of messages to delete * @param {string} [options.reason] Reason for banning diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index c48da00c0..d43a48b29 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -2,6 +2,24 @@ const Collection = require('../util/Collection'); const Snowflake = require('../util/Snowflake'); const Webhook = require('./Webhook'); +/** + * The target type of an entry, e.g. `GUILD`. Here are the available types: + * * GUILD + * * CHANNEL + * * USER + * * ROLE + * * INVITE + * * WEBHOOK + * * EMOJI + * * MESSAGE + * @typedef {string} AuditLogTargetType + */ + +/** + * Key mirror of all available audit log targets. + * @name GuildAuditLogs.Targets + * @type {AuditLogTargetType} + */ const Targets = { ALL: 'ALL', GUILD: 'GUILD', @@ -15,6 +33,43 @@ const Targets = { UNKNOWN: 'UNKNOWN', }; +/** + * The action of an entry. Here are the available actions: + * * ALL: null + * * GUILD_UPDATE: 1 + * * CHANNEL_CREATE: 10 + * * CHANNEL_UPDATE: 11 + * * CHANNEL_DELETE: 12 + * * CHANNEL_OVERWRITE_CREATE: 13 + * * CHANNEL_OVERWRITE_UPDATE: 14 + * * CHANNEL_OVERWRITE_DELETE: 15 + * * MEMBER_KICK: 20 + * * MEMBER_PRUNE: 21 + * * MEMBER_BAN_ADD: 22 + * * MEMBER_BAN_REMOVE: 23 + * * MEMBER_UPDATE: 24 + * * MEMBER_ROLE_UPDATE: 25 + * * ROLE_CREATE: 30 + * * ROLE_UPDATE: 31 + * * ROLE_DELETE: 32 + * * INVITE_CREATE: 40 + * * INVITE_UPDATE: 41 + * * INVITE_DELETE: 42 + * * WEBHOOK_CREATE: 50 + * * WEBHOOK_UPDATE: 51 + * * WEBHOOK_DELETE: 50 + * * EMOJI_CREATE: 60 + * * EMOJI_UPDATE: 61 + * * EMOJI_DELETE: 62 + * * MESSAGE_DELETE: 72 + * @typedef {?number|string} AuditLogAction + */ + +/** + * All available actions keyed under their names to their numeric values. + * @name GuildAuditLogs.Actions + * @type {AuditLogAction} + */ const Actions = { ALL: null, GUILD_UPDATE: 1, @@ -85,20 +140,7 @@ class GuildAuditLogs { } /** - * The target type of an entry, e.g. `GUILD`. Here are the available types: - * * GUILD - * * CHANNEL - * * USER - * * ROLE - * * INVITE - * * WEBHOOK - * * EMOJI - * * MESSAGE - * @typedef {string} TargetType - */ - - /** - * The target for an audit log entry. It can be one of: + * The target of an entry. It can be one of: * * A guild * * A user * * A role @@ -106,13 +148,13 @@ class GuildAuditLogs { * * An invite * * A webhook * * An object where the keys represent either the new value or the old value - * @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook} EntryTarget + * @typedef {?Object|Guild|User|Role|Emoji|Invite|Webhook} AuditLogEntryTarget */ /** * Finds the target type from the entry action. - * @param {number} target The action target - * @returns {?string} + * @param {AuditLogAction} target The action target + * @returns {AuditLogTargetType} */ static targetType(target) { if (target < 10) return Targets.GUILD; @@ -131,13 +173,14 @@ class GuildAuditLogs { * * CREATE * * DELETE * * UPDATE - * @typedef {string} ActionType + * * ALL + * @typedef {string} AuditLogActionType */ /** * Finds the action type from the entry action. - * @param {string} action The action target - * @returns {string} + * @param {AuditLogAction} action The action target + * @returns {AuditLogActionType} */ static actionType(action) { if ([ @@ -187,19 +230,19 @@ class GuildAuditLogsEntry { const targetType = GuildAuditLogs.targetType(data.action_type); /** * The target type of this entry - * @type {TargetType} + * @type {AuditLogTargetType} */ this.targetType = targetType; /** * The action type of this entry - * @type {ActionType} + * @type {AuditLogActionType} */ this.actionType = GuildAuditLogs.actionType(data.action_type); /** - * Specific action type of this entry - * @type {string} + * Specific action type of this entry in its string presentation + * @type {AuditLogAction} */ this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type); @@ -271,7 +314,7 @@ class GuildAuditLogsEntry { if (targetType === Targets.UNKNOWN) { /** * The target of this entry - * @type {EntryTarget} + * @type {AuditLogEntryTarget} */ this.target = this.changes.reduce((o, c) => { o[c.key] = c.new || c.old; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8b83796cf..010775e29 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -515,7 +515,7 @@ class GuildMember extends Base { /** * Bans this guild member. - * @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a + * @param {Object} [options] Ban options. If a number, the number of days to delete messages for, if a * string, the ban reason. Supplying an object allows you to do both. * @param {number} [options.days=0] Number of days of messages to delete * @param {string} [options.reason] Reason for banning diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index da55d6cee..b7f430b75 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -132,12 +132,12 @@ class MessageEmbed { proxyIconURL: data.footer.proxyIconURL || data.footer.proxy_icon_url, } : null; - /** - * The files of this embed - * @type {?Object} - * @property {Array} files Files to attach - */ if (data.files) { + /** + * The files of this embed + * @type {?Object} + * @property {Array} files Files to attach + */ this.files = data.files.map(file => { if (file instanceof MessageAttachment) { return typeof file.file === 'string' ? file.file : Util.cloneObject(file.file); @@ -158,7 +158,7 @@ class MessageEmbed { /** * The hexadecimal version of the embed color, with a leading hash - * @type {string} + * @type {?string} * @readonly */ get hexColor() { diff --git a/src/structures/Presence.js b/src/structures/Presence.js index e788124fc..535c7d11d 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -160,21 +160,22 @@ class RichPresenceAssets { /** * ID of the large image asset - * @type {?string} + * @type {?Snowflake} */ this.largeImage = assets.large_image || null; /** * ID of the small image asset - * @type {?string} + * @type {?Snowflake} */ this.smallImage = assets.small_image || null; } /** * Gets the URL of the small image asset - * @param {string} format Format of the image - * @param {number} size Size of the image + * @param {Object} [options] Options for the image url + * @param {string} [options.format] Format of the image + * @param {number} [options.size] Size of the image * @returns {?string} The small image URL */ smallImageURL({ format, size } = {}) { @@ -185,8 +186,9 @@ class RichPresenceAssets { /** * Gets the URL of the large image asset - * @param {string} format Format of the image - * @param {number} size Size of the image + * @param {Object} [options] Options for the image url + * @param {string} [options.format] Format of the image + * @param {number} [options.size] Size of the image * @returns {?string} The large image URL */ largeImageURL({ format, size } = {}) { diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index a781a25a8..f1a5c6a11 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -79,7 +79,6 @@ class Webhook { * (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 {FileOptions|BufferResolvable} [file] A file to send with the message * @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 From 1a8e8c7a67b59242537a6c97e383bc6aa4e8c8dd Mon Sep 17 00:00:00 2001 From: Sanctuary Date: Sat, 28 Oct 2017 14:06:26 -0300 Subject: [PATCH 15/75] docs: Add/normalize .toString() docs on all classes (#2042) * docs: Add/normalize .toString() examples on all classes * docs: Remove exclamation point on ClientApplication#toString example * docs: Normalize .toString() descriptions on all classes * Use "returns" instead of "concatenates" --- src/structures/ClientApplication.js | 6 +++++- src/structures/DMChannel.js | 7 +++++-- src/structures/Emoji.js | 2 +- src/structures/GroupDMChannel.js | 6 ++---- src/structures/Guild.js | 5 +---- src/structures/GuildChannel.js | 7 ++----- src/structures/GuildMember.js | 4 ++-- src/structures/ReactionEmoji.js | 7 ++++--- src/structures/Role.js | 5 ++++- src/structures/User.js | 4 ++-- 10 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 494d8c1c3..ccca3f7e0 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -195,8 +195,12 @@ class ClientApplication extends Base { } /** - * When concatenated with a string, this automatically concatenates the app name rather than the app object. + * When concatenated with a string, this automatically returns the application's name instead of the + * ClientApplication object. * @returns {string} + * @example + * // Logs: Application name: My App + * console.log(`Application name: ${application}`); */ toString() { return this.name; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index cdd4d2db8..fe6ba57c6 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -27,9 +27,12 @@ class DMChannel extends Channel { } /** - * When concatenated with a string, this automatically concatenates the recipient's mention instead of the - * DM channel object. + * When concatenated with a string, this automatically returns the recipient's mention instead of the + * DMChannel object. * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${channel}!`); */ toString() { return this.recipient.toString(); diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 4b1ea2680..d1cca2843 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -190,7 +190,7 @@ class Emoji extends Base { } /** - * When concatenated with a string, this automatically returns the emoji mention rather than the object. + * When concatenated with a string, this automatically concatenates the emoji's mention instead of the Emoji object. * @returns {string} * @example * // Send an emoji: diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index e142b070d..2ade06b06 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -203,14 +203,12 @@ class GroupDMChannel extends Channel { } /** - * When concatenated with a string, this automatically concatenates the channel's name instead of the Channel object. + * 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}!`); - * @example - * // Logs: Hello from My Group DM! - * console.log(`Hello from ' + channel + '!'); */ toString() { return this.name; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1b9463b51..73adbe1df 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1123,14 +1123,11 @@ class Guild extends Base { } /** - * When concatenated with a string, this automatically concatenates the guild's name instead of the guild object. + * When concatenated with a string, this automatically returns the guild's name instead of the Guild object. * @returns {string} * @example * // Logs: Hello from My Guild! * console.log(`Hello from ${guild}!`); - * @example - * // Logs: Hello from My Guild! - * console.log('Hello from ' + guild + '!'); */ toString() { return this.name; diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index a1e8d53a0..896e2bdf8 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -493,11 +493,8 @@ class GuildChannel extends Channel { * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. * @returns {string} * @example - * // Outputs: Hello from #general - * console.log(`Hello from ${channel}`); - * @example - * // Outputs: Hello from #general - * console.log('Hello from ' + channel); + * // Logs: Hello from <#123456789012345678>! + * console.log(`Hello from ${channel}!`); */ toString() { return `<#${this.id}>`; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 010775e29..7ab2597fd 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -529,10 +529,10 @@ class GuildMember extends Base { } /** - * When concatenated with a string, this automatically concatenates the user's mention instead of the Member object. + * When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object. * @returns {string} * @example - * // Logs: Hello from <@123456789>! + * // Logs: Hello from <@123456789012345678>! * console.log(`Hello from ${member}!`); */ toString() { diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js index f550544c6..94ea38930 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -35,11 +35,12 @@ class ReactionEmoji { } /** - * Creates the text required to form a graphical emoji on Discord. + * When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord + * instead of the ReactionEmoji object. + * @returns {string} * @example * // Send the emoji used in a reaction to the channel the reaction is part of - * reaction.message.channel.send(`The emoji used is ${reaction.emoji}`); - * @returns {string} + * reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`); */ toString() { return this.id ? `<:${this.name}:${this.id}>` : this.name; diff --git a/src/structures/Role.js b/src/structures/Role.js index eb60185db..39c28e11b 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -332,8 +332,11 @@ class Role extends Base { } /** - * When concatenated with a string, this automatically concatenates the role mention rather than the Role object. + * When concatenated with a string, this automatically returns the role's mention instead of the Role object. * @returns {string} + * @example + * // Logs: Role: <@&123456789012345678> + * console.log(`Role: ${role}`); */ toString() { if (this.id === this.guild.id) return '@everyone'; diff --git a/src/structures/User.js b/src/structures/User.js index 859cdeeb2..001a999be 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -248,10 +248,10 @@ class User extends Base { } /** - * When concatenated with a string, this automatically concatenates the user's mention instead of the User object. + * When concatenated with a string, this automatically returns the user's mention instead of the User object. * @returns {string} * @example - * // logs: Hello from <@123456789>! + * // Logs: Hello from <@123456789012345678>! * console.log(`Hello from ${user}!`); */ toString() { From 94a4a068b9400000a3b057a1ae781cb9eb585bc4 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 29 Oct 2017 14:53:34 +0100 Subject: [PATCH 16/75] fix(TextBasedChannel): return a promise in startTyping and clarify count parameter (#2047) * fix(TextBasedChannel): return a promise in startTyping This fixes #2040 Calling TextBasedChannel#startTyping now returns a promise. This promise resolves when the bot stops typing (TextBasedChannel#stopTyping) or rejects when an error occurs. Calling the method again returns the same promise as long the bot is still typing. * move code into the promise' executor * Clarify the purpose of the returned Promise * inverse if and clarify count parameter --- src/structures/interfaces/TextBasedChannel.js | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index ce997ae37..77bf65935 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -158,26 +158,45 @@ class TextBasedChannel { /** * Starts a typing indicator in the channel. - * @param {number} [count] The number of times startTyping should be considered to have been called + * @param {number} [count=1] The number of times startTyping should be considered to have been called + * @returns {Promise} Resolves once the bot stops typing gracefully, or rejects when an error occurs * @example - * // Start typing in a channel + * // Start typing in a channel, or increase the typing count by one * channel.startTyping(); + * @example + * // Start typing in a channel with a typing count of five, or set it to five + * channel.startTyping(5); */ startTyping(count) { if (typeof count !== 'undefined' && count < 1) throw new RangeError('TYPING_COUNT'); - if (!this.client.user._typing.has(this.id)) { - const endpoint = this.client.api.channels[this.id].typing; - this.client.user._typing.set(this.id, { - count: count || 1, - interval: this.client.setInterval(() => { - endpoint.post(); - }, 9000), - }); - endpoint.post(); - } else { + if (this.client.user._typing.has(this.id)) { const entry = this.client.user._typing.get(this.id); entry.count = count || entry.count + 1; + return entry.promise; } + + const entry = {}; + entry.promise = new Promise((resolve, reject) => { + const endpoint = this.client.api.channels[this.id].typing; + Object.assign(entry, { + count: count || 1, + interval: this.client.setInterval(() => { + endpoint.post().catch(error => { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + reject(error); + }); + }, 9000), + resolve, + }); + endpoint.post().catch(error => { + this.client.clearInterval(entry.interval); + this.client.user._typing.delete(this.id); + reject(error); + }); + this.client.user._typing.set(this.id, entry); + }); + return entry.promise; } /** @@ -186,10 +205,10 @@ class TextBasedChannel { * It can take a few seconds for the client user to stop typing. * @param {boolean} [force=false] Whether or not to reset the call count and force the indicator to stop * @example - * // Stop typing in a channel + * // Reduce the typing count by one and stop typing if it reached 0 * channel.stopTyping(); * @example - * // Force typing to fully stop in a channel + * // Force typing to fully stop regardless of typing count * channel.stopTyping(true); */ stopTyping(force = false) { @@ -199,6 +218,7 @@ class TextBasedChannel { if (entry.count <= 0 || force) { this.client.clearInterval(entry.interval); this.client.user._typing.delete(this.id); + entry.resolve(); } } } From 29a81eab733f91c2574979e5b2b3e62163abe9de Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 29 Oct 2017 08:54:00 -0500 Subject: [PATCH 17/75] standardize message object creation (#1986) * standardize message object creation so i don't flip out again * fix stuff * Update Message.js * Update index.js * Update SendMessage.js * Update Message.js --- src/structures/Message.js | 35 ++---- src/structures/Webhook.js | 113 ++---------------- src/structures/interfaces/TextBasedChannel.js | 56 +-------- src/structures/shared/CreateMessage.js | 111 +++++++++++++++++ src/structures/shared/SendMessage.js | 63 +--------- src/structures/shared/index.js | 1 + 6 files changed, 135 insertions(+), 244 deletions(-) create mode 100644 src/structures/shared/CreateMessage.js diff --git a/src/structures/Message.js b/src/structures/Message.js index ee4c9b662..a409e411a 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -8,9 +8,9 @@ const Collection = require('../util/Collection'); const ReactionStore = require('../stores/ReactionStore'); const { MessageTypes } = require('../util/Constants'); const Permissions = require('../util/Permissions'); -const GuildMember = require('./GuildMember'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); +const { createMessage } = require('./shared'); /** * Represents a message on Discord. @@ -368,41 +368,22 @@ class Message extends Base { * .then(msg => console.log(`Updated the content of a message from ${msg.author}`)) * .catch(console.error); */ - edit(content, options) { + async edit(content, options) { if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; - content = ''; + content = null; } else if (!options) { options = {}; } - if (options instanceof Embed) options = { embed: options }; + if (!options.content) options.content = content; - if (typeof options.content !== 'undefined') content = options.content; - - if (typeof content !== 'undefined') content = Util.resolveString(content); - - let { embed, code, reply } = options; - - if (embed) embed = new Embed(embed)._apiTransform(); - - // Wrap everything in a code block - if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { - content = Util.escapeMarkdown(Util.resolveString(content), true); - content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; - } - - // Add the reply prefix - if (reply && this.channel.type !== 'dm') { - const id = this.client.users.resolveID(reply); - const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; - content = `${mention}${content ? `, ${content}` : ''}`; - } + const { data, files } = await createMessage(this, options); return this.client.api.channels[this.channel.id].messages[this.id] - .patch({ data: { content, embed } }) - .then(data => { + .patch({ data, files }) + .then(d => { const clone = this._clone(); - clone._patch(data); + clone._patch(d); return clone; }); } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index f1a5c6a11..3601b9ca4 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,9 +1,5 @@ -const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); -const Embed = require('./MessageEmbed'); -const MessageAttachment = require('./MessageAttachment'); -const MessageEmbed = require('./MessageEmbed'); -const { browser } = require('../util/Constants'); +const { createMessage } = require('./shared'); /** * Represents a webhook. @@ -98,115 +94,24 @@ class Webhook { * .catch(console.error); */ /* eslint-enable max-len */ - send(content, options) { // eslint-disable-line complexity + async send(content, options) { // eslint-disable-line complexity if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; - content = ''; + content = null; } else if (!options) { options = {}; } + if (!options.content) options.content = content; - if (options instanceof MessageAttachment) options = { files: [options.file] }; - if (options instanceof MessageEmbed) options = { embeds: [options] }; - if (options.embed) options = { embeds: [options.embed] }; - - if (content instanceof Array || options instanceof Array) { - const which = content instanceof Array ? content : options; - const attachments = which.filter(item => item instanceof MessageAttachment); - const embeds = which.filter(item => item instanceof MessageEmbed); - if (attachments.length) options = { files: attachments }; - if (embeds.length) options = { embeds }; - if ((embeds.length || attachments.length) && content instanceof Array) content = ''; - } - - if (!options.username) options.username = this.name; - if (options.avatarURL) { - options.avatar_url = options.avatarURL; - options.avatarURL = null; - } - - if (content) { - content = Util.resolveString(content); - let { split, code, disableEveryone } = options; - if (split && typeof split !== 'object') split = {}; - if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { - content = Util.escapeMarkdown(content, true); - content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; - if (split) { - split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; - split.append = '\n```'; - } - } - if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { - content = content.replace(/@(everyone|here)/g, '@\u200b$1'); - } - - if (split) content = Util.splitMessage(content, split); - } - options.content = content; - - if (options.embeds) options.embeds = options.embeds.map(embed => new Embed(embed)._apiTransform()); - - if (options.files) { - for (let i = 0; i < options.files.length; i++) { - let file = options.files[i]; - if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file }; - if (!file.name) { - if (typeof file.attachment === 'string') { - file.name = Util.basename(file.attachment); - } else if (file.attachment && file.attachment.path) { - file.name = Util.basename(file.attachment.path); - } else if (file instanceof MessageAttachment) { - file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' }; - } else { - file.name = 'file.jpg'; - } - } else if (file instanceof MessageAttachment) { - file = file.file; - } - options.files[i] = file; - } - - return Promise.all(options.files.map(file => - DataResolver.resolveFile(file.attachment).then(resource => { - file.file = resource; - return file; - }) - )).then(files => this.client.api.webhooks(this.id, this.token).post({ - data: options, - query: { wait: true }, - files, - auth: false, - })); - } - - if (content instanceof Array) { - return new Promise((resolve, reject) => { - const messages = []; - (function sendChunk() { - const opt = content.length ? null : { embeds: options.embeds, files: options.files }; - this.client.api.webhooks(this.id, this.token).post({ - data: { content: content.shift(), opt }, - query: { wait: true }, - auth: false, - }) - .then(message => { - messages.push(message); - if (content.length === 0) return resolve(messages); - return sendChunk.call(this); - }) - .catch(reject); - }.call(this)); - }); - } + const { data, files } = await createMessage(this, options); return this.client.api.webhooks(this.id, this.token).post({ - data: options, + data, files, query: { wait: true }, auth: false, - }).then(data => { - if (!this.client.channels) return data; - return this.client.channels.get(data.channel_id).messages.create(data, false); + }).then(d => { + if (!this.client.channels) return d; + return this.client.channels.get(d.channel_id).messages.create(d, false); }); } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 77bf65935..39aac0937 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,12 +1,7 @@ const MessageCollector = require('../MessageCollector'); const Shared = require('../shared'); -const Util = require('../../util/Util'); -const { browser } = require('../../util/Constants'); const Snowflake = require('../../util/Snowflake'); const Collection = require('../../util/Collection'); -const DataResolver = require('../../util/DataResolver'); -const MessageAttachment = require('../../structures/MessageAttachment'); -const MessageEmbed = require('../../structures/MessageEmbed'); const { RangeError, TypeError } = require('../../errors'); /** @@ -80,61 +75,12 @@ class TextBasedChannel { send(content, options) { // eslint-disable-line complexity if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; - content = ''; + content = null; } else if (!options) { options = {}; } - - if (options instanceof MessageEmbed) options = { embed: options }; - if (options instanceof MessageAttachment) options = { files: [options.file] }; - - if (content instanceof Array || options instanceof Array) { - const which = content instanceof Array ? content : options; - const attachments = which.filter(item => item instanceof MessageAttachment); - if (attachments.length) { - options = { files: attachments }; - if (content instanceof Array) content = ''; - } - } - if (!options.content) options.content = content; - if (options.embed && options.embed.files) { - if (options.files) options.files = options.files.concat(options.embed.files); - else options.files = options.embed.files; - } - - if (options.files) { - for (let i = 0; i < options.files.length; i++) { - let file = options.files[i]; - if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file }; - if (!file.name) { - if (typeof file.attachment === 'string') { - file.name = Util.basename(file.attachment); - } else if (file.attachment && file.attachment.path) { - file.name = Util.basename(file.attachment.path); - } else if (file instanceof MessageAttachment) { - file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' }; - } else { - file.name = 'file.jpg'; - } - } else if (file instanceof MessageAttachment) { - file = file.file; - } - options.files[i] = file; - } - - return Promise.all(options.files.map(file => - DataResolver.resolveFile(file.attachment).then(resource => { - file.file = resource; - return file; - }) - )).then(files => { - options.files = files; - return Shared.sendMessage(this, options); - }); - } - return Shared.sendMessage(this, options); } diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js new file mode 100644 index 000000000..65c7c2ca3 --- /dev/null +++ b/src/structures/shared/CreateMessage.js @@ -0,0 +1,111 @@ +const Embed = require('../MessageEmbed'); +const DataResolver = require('../../util/DataResolver'); +const MessageEmbed = require('../MessageEmbed'); +const MessageAttachment = require('../MessageAttachment'); +const { browser } = require('../../util/Constants'); +const Util = require('../../util/Util'); + +// eslint-disable-next-line complexity +module.exports = async function createMessage(channel, options) { + const User = require('../User'); + const GuildMember = require('../GuildMember'); + const Webhook = require('../Webhook'); + + const webhook = channel instanceof Webhook; + + if (typeof options.nonce !== 'undefined') { + options.nonce = parseInt(options.nonce); + if (isNaN(options.nonce) || options.nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); + } + + if (options instanceof MessageEmbed) options = webhook ? { embeds: [options] } : { embed: options }; + if (options instanceof MessageAttachment) options = { files: [options.file] }; + + if (options.reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = channel.client.users.resolveID(options.reply); + const mention = `<@${options.reply instanceof GuildMember && options.reply.nickname ? '!' : ''}${id}>`; + if (options.split) options.split.prepend = `${mention}, ${options.split.prepend || ''}`; + options.content = `${mention}${typeof options.content !== 'undefined' ? `, ${options.content}` : ''}`; + } + + if (options.content) { + options.content = Util.resolveString(options.content); + if (options.split && typeof options.split !== 'object') options.split = {}; + // Wrap everything in a code block + if (typeof options.code !== 'undefined' && (typeof options.code !== 'boolean' || options.code === true)) { + options.content = Util.escapeMarkdown(options.content, true); + options.content = + `\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n${options.content}\n\`\`\``; + if (options.split) { + options.split.prepend = `\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n`; + options.split.append = '\n```'; + } + } + + // Add zero-width spaces to @everyone/@here + if (options.disableEveryone || + (typeof options.disableEveryone === 'undefined' && channel.client.options.disableEveryone)) { + options.content = options.content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + if (options.split) options.content = Util.splitMessage(options.content, options.split); + } + + if (options.embed && options.embed.files) { + if (options.files) options.files = options.files.concat(options.embed.files); + else options.files = options.embed.files; + } + + if (options.embed && webhook) options.embeds = [new Embed(options.embed)._apiTransform()]; + else if (options.embed) options.embed = new Embed(options.embed)._apiTransform(); + else if (options.embeds) options.embeds = options.embeds.map(e => new Embed(e)._apiTransform()); + + let files; + + if (options.files) { + for (let i = 0; i < options.files.length; i++) { + let file = options.files[i]; + if (typeof file === 'string' || (!browser && Buffer.isBuffer(file))) file = { attachment: file }; + if (!file.name) { + if (typeof file.attachment === 'string') { + file.name = Util.basename(file.attachment); + } else if (file.attachment && file.attachment.path) { + file.name = Util.basename(file.attachment.path); + } else if (file instanceof MessageAttachment) { + file = { attachment: file.file, name: Util.basename(file.file) || 'file.jpg' }; + } else { + file.name = 'file.jpg'; + } + } else if (file instanceof MessageAttachment) { + file = file.file; + } + options.files[i] = file; + } + + files = await Promise.all(options.files.map(file => + DataResolver.resolveFile(file.attachment).then(resource => { + file.file = resource; + return file; + }) + )); + delete options.files; + } + + if (webhook) { + if (!options.username) options.username = this.name; + if (options.avatarURL) { + options.avatar_url = options.avatarURL; + options.avatarURL = null; + } + } + + return { data: { + content: options.content, + tts: options.tts, + nonce: options.nonce, + embed: options.embed, + embeds: options.embeds, + username: options.username, + avatar_url: options.avatarURL, + }, files }; +}; diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index 560ece728..5007a0af5 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -1,65 +1,12 @@ -const Util = require('../../util/Util'); -const Embed = require('../MessageEmbed'); -const { RangeError } = require('../../errors'); +const createMessage = require('./CreateMessage'); -module.exports = function sendMessage(channel, options) { // eslint-disable-line complexity +module.exports = async function sendMessage(channel, options) { // eslint-disable-line complexity const User = require('../User'); const GuildMember = require('../GuildMember'); if (channel instanceof User || channel instanceof GuildMember) return channel.createDM().then(dm => dm.send(options)); - let { content, nonce, reply, code, disableEveryone, tts, embed, files, split } = options; - if (embed) embed = new Embed(embed)._apiTransform(); + const { data, files } = await createMessage(channel, options); - if (typeof nonce !== 'undefined') { - nonce = parseInt(nonce); - if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); - } - - // Add the reply prefix - if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { - const id = channel.client.users.resolveID(reply); - const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; - if (split) split.prepend = `${mention}, ${split.prepend || ''}`; - content = `${mention}${typeof content !== 'undefined' ? `, ${content}` : ''}`; - } - - if (content) { - content = Util.resolveString(content); - if (split && typeof split !== 'object') split = {}; - // Wrap everything in a code block - if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { - content = Util.escapeMarkdown(content, true); - content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; - if (split) { - split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; - split.append = '\n```'; - } - } - - // Add zero-width spaces to @everyone/@here - if (disableEveryone || (typeof disableEveryone === 'undefined' && channel.client.options.disableEveryone)) { - content = content.replace(/@(everyone|here)/g, '@\u200b$1'); - } - - if (split) content = Util.splitMessage(content, split); - } - - if (content instanceof Array) { - return new Promise((resolve, reject) => { - const messages = []; - (function sendChunk() { - const opt = content.length ? { tts } : { tts, embed, files }; - channel.send(content.shift(), opt).then(message => { - messages.push(message); - if (content.length === 0) return resolve(messages); - return sendChunk(); - }).catch(reject); - }()); - }); - } - - return channel.client.api.channels[channel.id].messages.post({ - data: { content, tts, nonce, embed }, - files, - }).then(data => channel.client.actions.MessageCreate.handle(data).message); + return channel.client.api.channels[channel.id].messages.post({ data, files }) + .then(d => channel.client.actions.MessageCreate.handle(d).message); }; diff --git a/src/structures/shared/index.js b/src/structures/shared/index.js index 67eed7f83..67a09646b 100644 --- a/src/structures/shared/index.js +++ b/src/structures/shared/index.js @@ -1,4 +1,5 @@ module.exports = { search: require('./Search'), sendMessage: require('./SendMessage'), + createMessage: require('./CreateMessage'), }; From 21d09f338e8dc2ad3cbc74d1e3fd56994b92de01 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 5 Nov 2017 18:52:33 +0100 Subject: [PATCH 18/75] fix(Guild): correctly resolve user in Guild#addMember (#2090) --- src/structures/Guild.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 73adbe1df..e00151f47 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -528,7 +528,9 @@ class Guild extends Base { * @returns {Promise} */ addMember(user, options) { - if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); + user = this.client.users.resolveID(user); + if (!user) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable')); + if (this.members.has(user)) return Promise.resolve(this.members.get(user)); options.access_token = options.accessToken; if (options.roles) { const roles = []; @@ -541,7 +543,7 @@ class Guild extends Base { roles.push(role.id); } } - return this.client.api.guilds(this.id).members(user.id).put({ data: options }) + return this.client.api.guilds(this.id).members(user).put({ data: options }) .then(data => this.members.create(data)); } From 05a41b5ca44cfa39db2413284a93bd34c402806d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 6 Nov 2017 02:42:24 +0100 Subject: [PATCH 19/75] fix(Split/Webhook): readd message chunk sending and fix webhook avatar/username (#2085) --- src/structures/Webhook.js | 13 +++++++++++++ src/structures/shared/CreateMessage.js | 11 ++++------- src/structures/shared/SendMessage.js | 11 +++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 3601b9ca4..7ee44745e 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -105,6 +105,19 @@ class Webhook { const { data, files } = await createMessage(this, options); + if (data.content instanceof Array) { + const messages = []; + for (let i = 0; i < data.content.length; i++) { + const opt = i === data.content.length - 1 ? { embeds: data.embeds, files } : {}; + Object.assign(opt, { avatarURL: data.avatar_url, content: data.content[i], username: data.username }); + // eslint-disable-next-line no-await-in-loop + const message = await this.send(data.content[i], opt); + messages.push(message); + } + return messages; + } + + return this.client.api.webhooks(this.id, this.token).post({ data, files, query: { wait: true }, diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js index 65c7c2ca3..5abf0799e 100644 --- a/src/structures/shared/CreateMessage.js +++ b/src/structures/shared/CreateMessage.js @@ -10,8 +10,9 @@ module.exports = async function createMessage(channel, options) { const User = require('../User'); const GuildMember = require('../GuildMember'); const Webhook = require('../Webhook'); + const WebhookClient = require('../../client/WebhookClient'); - const webhook = channel instanceof Webhook; + const webhook = channel instanceof Webhook || channel instanceof WebhookClient; if (typeof options.nonce !== 'undefined') { options.nonce = parseInt(options.nonce); @@ -88,15 +89,11 @@ module.exports = async function createMessage(channel, options) { return file; }) )); - delete options.files; } if (webhook) { if (!options.username) options.username = this.name; - if (options.avatarURL) { - options.avatar_url = options.avatarURL; - options.avatarURL = null; - } + if (options.avatarURL) options.avatar_url = options.avatarURL; } return { data: { @@ -106,6 +103,6 @@ module.exports = async function createMessage(channel, options) { embed: options.embed, embeds: options.embeds, username: options.username, - avatar_url: options.avatarURL, + avatar_url: options.avatar_url, }, files }; }; diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index 5007a0af5..95ea49ca3 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -7,6 +7,17 @@ module.exports = async function sendMessage(channel, options) { // eslint-disabl const { data, files } = await createMessage(channel, options); + if (data.content instanceof Array) { + const messages = []; + for (let i = 0; i < data.content.length; i++) { + const opt = i === data.content.length - 1 ? { tts: data.tts, embed: data.embed, files } : { tts: data.tts }; + // eslint-disable-next-line no-await-in-loop + const message = await channel.send(data.content[i], opt); + messages.push(message); + } + return messages; + } + return channel.client.api.channels[channel.id].messages.post({ data, files }) .then(d => channel.client.actions.MessageCreate.handle(d).message); }; From 62544905a06152ca769c4e6d5f16337b134ac67e Mon Sep 17 00:00:00 2001 From: Yukine Date: Fri, 10 Nov 2017 01:30:13 +0100 Subject: [PATCH 20/75] enhanced setUserLimit to reset when passing null to stay consistent with other methods (#2083) * added a new check to setUserLimit so it won't silently fail anymore if you put a wrong type in * adapt spaces idea of converting null to 0 * this way it looks cleaner * and i need to remove this * need to do it that way because like Gus said null will not change anyhting * space prooved me wrong and idk why ist working now --- 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 896e2bdf8..a84ad0847 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -293,7 +293,7 @@ class GuildChannel extends Channel { topic: data.topic, nsfw: data.nsfw, bitrate: data.bitrate || (this.bitrate ? this.bitrate * 1000 : undefined), - user_limit: data.userLimit != null ? data.userLimit : this.userLimit, // eslint-disable-line eqeqeq + user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit, parent_id: data.parentID, lock_permissions: data.lockPermissions, permission_overwrites: data.permissionOverwrites, From 5cd42695aeb974e998d4b220cba1c27b30a55628 Mon Sep 17 00:00:00 2001 From: Isabella Date: Tue, 14 Nov 2017 02:11:44 -0600 Subject: [PATCH 21/75] refactor(MessageReaction): ReactionUserStore (#2078) * refactor(MessageReactions): fetchUsers() is now users.fetch() made a lovely class for it and all happify linter stuff i know how to code i swear i lied * bdistin suggestions * space suggestions, rename store * fix count * documentation update --- src/index.js | 1 + src/stores/ReactionUserStore.js | 33 +++++++++++++++++++++++++++++++ src/structures/MessageReaction.js | 26 +++--------------------- 3 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 src/stores/ReactionUserStore.js diff --git a/src/index.js b/src/index.js index 7bea36f1b..de6a6e66c 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ module.exports = { GuildChannelStore: require('./stores/GuildChannelStore'), GuildMemberStore: require('./stores/GuildMemberStore'), GuildStore: require('./stores/GuildStore'), + ReactionUserStore: require('./stores/ReactionUserStore'), MessageStore: require('./stores/MessageStore'), PresenceStore: require('./stores/PresenceStore'), RoleStore: require('./stores/RoleStore'), diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js new file mode 100644 index 000000000..b3c3ec012 --- /dev/null +++ b/src/stores/ReactionUserStore.js @@ -0,0 +1,33 @@ +const DataStore = require('./DataStore'); +/** + * A data store to store User models who reacted to a MessageReaction. + * @extends {DataStore} + */ +class ReactionUserStore extends DataStore { + constructor(client, iterable, reaction) { + super(client, iterable, require('../structures/User')); + this.reaction = reaction; + } + + /** + * Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs. + * @param {Object} [options] Options for fetching the users + * @param {number} [options.limit=100] The maximum amount of users to fetch, defaults to 100 + * @param {Snowflake} [options.before] Limit fetching users to those with an id lower than the supplied id + * @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id + * @returns {Promise>} + */ + async fetch({ limit = 100, after, before } = {}) { + const message = this.reaction.message; + const users = await this.client.api.channels[message.channel.id].messages[message.id] + .reactions[this.reaction.emoji.identifier] + .get({ query: { limit, before, after } }); + for (const rawUser of users) { + const user = this.client.users.create(rawUser); + this.set(user.id, user); + } + return this; + } +} + +module.exports = ReactionUserStore; diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 2b273f84b..28a320a63 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,6 +1,6 @@ -const Collection = require('../util/Collection'); const Emoji = require('./Emoji'); const ReactionEmoji = require('./ReactionEmoji'); +const ReactionUserStore = require('../stores/ReactionUserStore'); const { Error } = require('../errors'); /** @@ -28,9 +28,9 @@ class MessageReaction { /** * The users that have given this reaction, mapped by their ID - * @type {Collection} + * @type {ReactionUserStore} */ - this.users = new Collection(); + this.users = new ReactionUserStore(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji.name, data.emoji.id); } @@ -77,26 +77,6 @@ class MessageReaction { ); } - /** - * Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs. - * @param {Object} [options] Options for fetching the users - * @param {number} [options.limit=100] The maximum amount of users to fetch, defaults to 100 - * @param {Snowflake} [options.after] Limit fetching users to those with an id greater than the supplied id - * @returns {Promise>} - */ - async fetchUsers({ limit = 100, after } = {}) { - const message = this.message; - const users = await message.client.api.channels[message.channel.id].messages[message.id] - .reactions[this.emoji.identifier] - .get({ query: { limit, after } }); - for (const rawUser of users) { - const user = message.client.users.create(rawUser); - this.users.set(user.id, user); - } - this.count = this.users.size; - return this.users; - } - _add(user) { if (!this.users.has(user.id)) { this.users.set(user.id, user); From 2d8e26c24c360148083cacb3468a58c99320198a Mon Sep 17 00:00:00 2001 From: Isabella Date: Thu, 16 Nov 2017 08:24:27 -0600 Subject: [PATCH 22/75] docs: add Guild#features type (#2105) * docs: add Guild#features type * fixed spacing * make it a list, and add MORE_EMOJI --- src/structures/Guild.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index e00151f47..b84117e91 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -114,8 +114,18 @@ class Guild extends Base { this.large = Boolean('large' in data ? data.large : this.large); /** - * An array of guild features - * @type {string[]} + * An array of enabled guild features, here are the possible values: + * * INVITE_SPLASH + * * MORE_EMOJI + * * VERIFIED + * * VIP_REGIONS + * * VANITY_URL + * @typedef {string} Features + */ + + /** + * An array of guild features partnered guilds have enabled + * @type {Features[]} */ this.features = data.features; From 09315ae9dbcef978721a10dde4d00971fba0561c Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Thu, 16 Nov 2017 06:24:53 -0800 Subject: [PATCH 23/75] emit ReactionCollector#remove on all unreactions (#2096) * emit ReactionCollector#remove on all unreactions this will emit an event when a user removes a collected reaction. this is in addition to Collector#dispose, which will only fire when all users have unreacted to the same emoji. * emit only collected removals --- src/structures/ReactionCollector.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index 1ebab746c..60bdabc48 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -83,7 +83,17 @@ class ReactionCollector extends Collector { * @returns {?Snowflake|string} */ dispose(reaction) { - return reaction.message.id === this.message.id && !reaction.count ? ReactionCollector.key(reaction) : null; + if (reaction.message.id !== this.message.id) return null; + + /** + * Emitted whenever a reaction is removed from a message. Will emit on all reaction removals, + * as opposed to {@link Collector#dispose} which will only be emitted when the entire reaction + * is removed. + * @event ReactionCollector#remove + * @param {MessageReaction} reaction The reaction that was removed + */ + if (this.collected.has(reaction)) this.emit('remove', reaction); + return reaction.count ? null : ReactionCollector.key(reaction); } /** From 196cf7652e1a9c9009578a0c532bf6f0ddd7451e Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Thu, 16 Nov 2017 22:49:38 -0500 Subject: [PATCH 24/75] Add Shard#ready property and related events --- src/sharding/Shard.js | 70 ++++++++++++++++++++++++++++----- src/sharding/ShardClientUtil.js | 9 +++-- src/sharding/ShardingManager.js | 4 +- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index e67bece78..76da23527 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -8,9 +8,9 @@ const { Error } = require('../errors'); */ class Shard { /** - * @param {ShardingManager} manager The sharding manager - * @param {number} id The ID of this shard - * @param {Array} [args=[]] Command line arguments to pass to the script + * @param {ShardingManager} manager Manager that is spawning this shard + * @param {number} id ID of this shard + * @param {string[]} [args=[]] Command line arguments to pass to the script */ constructor(manager, id, args = []) { /** @@ -26,7 +26,7 @@ class Shard { this.id = id; /** - * The environment variables for the shard + * Environment variables for the shard's process * @type {Object} */ this.env = Object.assign({}, process.env, { @@ -41,14 +41,33 @@ class Shard { */ this.process = childProcess.fork(path.resolve(this.manager.file), args, { env: this.env, - }); - this.process.on('message', this._handleMessage.bind(this)); - this.process.once('exit', () => { - if (this.manager.respawn) this.manager.createShard(this.id); - }); + }).on('message', this._handleMessage.bind(this)); + /** + * Whether the shard's {@link Client} is ready + * @type {boolean} + */ + this.ready = false; + + /** + * Ongoing promises for calls to {@link Shard#eval}, mapped by the `script` they were called with + * @type {Map} + * @private + */ this._evals = new Map(); + + /** + * Ongoing promises for calls to {@link Shard#fetchClientValue}, mapped by the `prop` they were called with + * @type {Map} + * @private + */ this._fetches = new Map(); + + // Handle the death of the process + this.process.once('exit', () => { + this.ready = false; + if (this.manager.respawn) this.manager.createShard(this.id).catch(err => { this.manager.emit('error', err); }); + }); } /** @@ -134,6 +153,39 @@ class Shard { */ _handleMessage(message) { if (message) { + // Shard is ready + if (message._ready) { + this.ready = true; + /** + * Emitted upon the shard's {@link Client#ready} event. + * @event Shard#ready + */ + this.emit('ready'); + return; + } + + // Shard has disconnected + if (message._disconnect) { + this.ready = false; + /** + * Emitted upon the shard's {@link Client#disconnect} event. + * @event Shard#disconnect + */ + this.emit('disconnect'); + return; + } + + // Shard is attempting to reconnect + if (message._reconnecting) { + this.ready = false; + /** + * Emitted upon the shard's {@link Client#reconnecting} event. + * @event Shard#reconnecting + */ + this.emit('reconnecting'); + return; + } + // Shard is requesting a property fetch if (message._sFetchProp) { this.manager.fetchClientValues(message._sFetchProp).then( diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index aceb85f2d..70e625824 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -7,11 +7,14 @@ const { Error } = require('../errors'); */ class ShardClientUtil { /** - * @param {Client} client The client of the current shard + * @param {Client} client Client of the current shard */ constructor(client) { this.client = client; process.on('message', this._handleMessage.bind(this)); + client.on('ready', () => { process.send({ _ready: true }); }); + client.on('disconnect', () => { process.send({ _disconnect: true }); }); + client.on('reconnecting', () => { process.send({ _reconnecting: true }); }); } /** @@ -49,7 +52,7 @@ class ShardClientUtil { /** * Fetches a client property value of each shard. * @param {string} prop Name of the client property to get, using periods for nesting - * @returns {Promise} + * @returns {Promise>} * @example * client.shard.fetchClientValues('guilds.size') * .then(results => { @@ -76,7 +79,7 @@ class ShardClientUtil { /** * Evaluates a script on all shards, in the context of the Clients. * @param {string} script JavaScript to run on each shard - * @returns {Promise} Results of the script execution + * @returns {Promise>} Results of the script execution */ broadcastEval(script) { return new Promise((resolve, reject) => { diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 7a18898be..117fca4f9 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -168,7 +168,7 @@ class ShardingManager extends EventEmitter { /** * Evaluates a script on all shards, in the context of the Clients. * @param {string} script JavaScript to run on each shard - * @returns {Promise} Results of the script execution + * @returns {Promise>} Results of the script execution */ broadcastEval(script) { const promises = []; @@ -179,7 +179,7 @@ class ShardingManager extends EventEmitter { /** * Fetches a client property value of each shard. * @param {string} prop Name of the client property to get, using periods for nesting - * @returns {Promise} + * @returns {Promise>} * @example * manager.fetchClientValues('guilds.size') * .then(results => { From 6fa4fc532cf9525838b55287a5565be4a4ec07d2 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 17 Nov 2017 08:49:57 +0100 Subject: [PATCH 25/75] fix(Shard): extend EventEmitter to be able to emit events (#2112) --- src/sharding/Shard.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 76da23527..7e2a51ac7 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -1,4 +1,5 @@ const childProcess = require('child_process'); +const EventEmitter = require('events'); const path = require('path'); const Util = require('../util/Util'); const { Error } = require('../errors'); @@ -6,13 +7,14 @@ const { Error } = require('../errors'); /** * Represents a Shard spawned by the ShardingManager. */ -class Shard { +class Shard extends EventEmitter { /** * @param {ShardingManager} manager Manager that is spawning this shard * @param {number} id ID of this shard * @param {string[]} [args=[]] Command line arguments to pass to the script */ constructor(manager, id, args = []) { + super(); /** * Manager that created the shard * @type {ShardingManager} From 0cd4a92fb8d7e041bbf600c5b7fdb3af55460680 Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Fri, 17 Nov 2017 15:20:57 +0200 Subject: [PATCH 26/75] docs: Fixed some missing docstrings or incorrect return types (#2093) * Fix some missing doc strings Mainly just readonly tags * Return an error when guild#allowDMs is ran from a bot account, and fix some return types * WebhookClient implements Webhook, doesn't extend it * Fix Client#rateLimit docs not showing what it returns Cause I wanted to handle this event only to see no return props :thinking: * Actually make Client#rateLimit show the right info Its an object with all the info --- src/client/BaseClient.js | 1 + src/client/Client.js | 1 + src/client/WebhookClient.js | 2 +- src/rest/handlers/RequestHandler.js | 13 +++++++------ src/structures/ClientUser.js | 2 +- src/structures/Guild.js | 5 +++-- src/structures/GuildMember.js | 6 ++++++ src/util/Constants.js | 2 +- 8 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index 365e9dcb7..f2c91ccdf 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -42,6 +42,7 @@ class BaseClient extends EventEmitter { /** * API shortcut * @type {Object} + * @readonly * @private */ get api() { diff --git a/src/client/Client.js b/src/client/Client.js index b26b73e2e..4b335e88a 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -163,6 +163,7 @@ class Client extends BaseClient { /** * Timestamp of the latest ping's start time * @type {number} + * @readonly * @private */ get _pingTimestamp() { diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js index bc413cef6..c4c297879 100644 --- a/src/client/WebhookClient.js +++ b/src/client/WebhookClient.js @@ -3,7 +3,7 @@ const BaseClient = require('./BaseClient'); /** * The webhook client. - * @extends {Webhook} + * @implements {Webhook} * @extends {BaseClient} */ class WebhookClient extends BaseClient { diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js index 64e9ea72b..c4226a45c 100644 --- a/src/rest/handlers/RequestHandler.js +++ b/src/rest/handlers/RequestHandler.js @@ -39,12 +39,13 @@ class RequestHandler { /** * Emitted when the client hits a rate limit while making a request * @event Client#rateLimit - * @prop {number} timeout Timeout in ms - * @prop {number} limit Number of requests that can be made to this endpoint - * @prop {number} timeDifference Delta-T in ms between your system and Discord servers - * @prop {string} method HTTP method used for request that triggered this event - * @prop {string} path Path used for request that triggered this event - * @prop {string} route Route used for request that triggered this event + * @param {Object} rateLimitInfo Object containing the rate limit info + * @param {number} rateLimitInfo.timeout Timeout in ms + * @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint + * @param {number} rateLimitInfo.timeDifference Delta-T in ms between your system and Discord servers + * @param {string} rateLimitInfo.method HTTP method used for request that triggered this event + * @param {string} rateLimitInfo.path Path used for request that triggered this event + * @param {string} rateLimitInfo.route Route used for request that triggered this event */ this.client.emit(RATE_LIMIT, { timeout, diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index ce71d9bcf..ef6433924 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -248,7 +248,7 @@ class ClientUser extends User { /** * Fetches messages that mentioned the client's user. * This is only available when using a user account. - * @param {Object} [options] Options for the fetch + * @param {Object} [options={}] Options for the fetch * @param {number} [options.limit=25] Maximum number of mentions to retrieve * @param {boolean} [options.roles=true] Whether to include role mentions * @param {boolean} [options.everyone=true] Whether to include everyone/here mentions diff --git a/src/structures/Guild.js b/src/structures/Guild.js index b84117e91..8c796d337 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -321,7 +321,7 @@ class Guild extends Base { /** * System channel for this guild - * @type {?GuildChannel} + * @type {?TextChannel} * @readonly */ get systemChannel() { @@ -806,6 +806,7 @@ class Guild extends Base { * @returns {Promise} */ allowDMs(allow) { + if (this.client.user.bot) return Promise.reject(new Error('FEATURE_USER_ONLY')); const settings = this.client.user.settings; if (allow) return settings.removeRestrictedGuild(this); else return settings.addRestrictedGuild(this); @@ -818,7 +819,7 @@ class Guild extends Base { * string, the ban reason. Supplying an object allows you to do both. * @param {number} [options.days=0] Number of days of messages to delete * @param {string} [options.reason] Reason for banning - * @returns {Promise} Result object will be resolved as specifically as possible. + * @returns {Promise} Result object will be resolved as specifically as possible. * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot * be resolved, the user ID will be the result. * @example diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 7ab2597fd..05266e1b8 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -77,36 +77,42 @@ class GuildMember extends Base { /** * Whether this member is deafened server-wide * @type {boolean} + * @readonly */ get serverDeaf() { return this.voiceState.deaf; } /** * Whether this member is muted server-wide * @type {boolean} + * @readonly */ get serverMute() { return this.voiceState.mute; } /** * Whether this member is self-muted * @type {boolean} + * @readonly */ get selfMute() { return this.voiceState.self_mute; } /** * Whether this member is self-deafened * @type {boolean} + * @readonly */ get selfDeaf() { return this.voiceState.self_deaf; } /** * The voice session ID of this member (if any) * @type {?Snowflake} + * @readonly */ get voiceSessionID() { return this.voiceState.session_id; } /** * The voice channel ID of this member, (if any) * @type {?Snowflake} + * @readonly */ get voiceChannelID() { return this.voiceState.channel_id; } diff --git a/src/util/Constants.js b/src/util/Constants.js index 7a0b262d3..efeeb29fa 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -52,7 +52,7 @@ exports.DefaultOptions = { * WebSocket options (these are left as snake_case to match the API) * @typedef {Object} WebsocketOptions * @property {number} [large_threshold=250] Number of members in a guild to be considered large - * @property {boolean} [compress=true] Whether to compress data sent on the connection + * @property {boolean} [compress=false] Whether to compress data sent on the connection * (defaults to `false` for browsers) */ ws: { From 8237bc054ce2d705ea5432dfbcf07c4fbd452d1a Mon Sep 17 00:00:00 2001 From: Drahcirius Date: Fri, 17 Nov 2017 08:37:07 -0500 Subject: [PATCH 27/75] So long, long (#1994) * refactor: remove long dep * fix linter issue * remove file extensions * optimize methods --- package.json | 1 - src/structures/shared/Search.js | 10 +++---- src/util/Snowflake.js | 13 ++++---- src/util/Util.js | 53 +++++++++++++++++++++++++++++++-- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 50fb6bb59..08e2b200d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "homepage": "https://github.com/hydrabolt/discord.js#readme", "runkitExampleFilename": "./docs/examples/ping.js", "dependencies": { - "long": "^3.0.0", "pako": "^1.0.0", "prism-media": "^0.0.2", "snekfetch": "^3.0.0", diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js index a852a5086..3adca7fcc 100644 --- a/src/structures/shared/Search.js +++ b/src/structures/shared/Search.js @@ -1,4 +1,4 @@ -const long = require('long'); +const Util = require('../../util/Util'); const { TypeError } = require('../../errors'); /** @@ -40,17 +40,17 @@ module.exports = function search(target, options) { if (typeof options === 'string') options = { content: options }; if (options.before) { if (!(options.before instanceof Date)) options.before = new Date(options.before); - options.maxID = long.fromNumber(options.before.getTime() - 14200704e5).shiftLeft(22).toString(); + options.maxID = Util.binaryToID((options.before.getTime() - 14200704e5).toString(2) + '0'.repeat(22)); } if (options.after) { if (!(options.after instanceof Date)) options.after = new Date(options.after); - options.minID = long.fromNumber(options.after.getTime() - 14200704e5).shiftLeft(22).toString(); + options.minID = Util.binaryToID((options.after.getTime() - 14200704e5).toString(2) + '0'.repeat(22)); } if (options.during) { if (!(options.during instanceof Date)) options.during = new Date(options.during); const t = options.during.getTime() - 14200704e5; - options.minID = long.fromNumber(t).shiftLeft(22).toString(); - options.maxID = long.fromNumber(t + 864e5).shiftLeft(22).toString(); + options.minID = Util.binaryToID(t.toString(2) + '0'.repeat(22)); + options.maxID = Util.binaryToID((t + 864e5).toString(2) + '0'.repeat(22)); } if (options.channel) options.channel = target.client.channels.resolveID(options.channel); if (options.author) options.author = target.client.users.resolveID(options.author); diff --git a/src/util/Snowflake.js b/src/util/Snowflake.js index f16839108..27f9f7440 100644 --- a/src/util/Snowflake.js +++ b/src/util/Snowflake.js @@ -1,4 +1,4 @@ -const Long = require('long'); +const Util = require('../util/Util'); // Discord epoch (2015-01-01T00:00:00.000Z) const EPOCH = 1420070400000; @@ -31,8 +31,9 @@ class SnowflakeUtil { */ static generate() { if (INCREMENT >= 4095) INCREMENT = 0; - const BINARY = `${pad((Date.now() - EPOCH).toString(2), 42)}0000100000${pad((INCREMENT++).toString(2), 12)}`; - return Long.fromString(BINARY, 2).toString(); + // eslint-disable-next-line max-len + const BINARY = `${(Date.now() - EPOCH).toString(2).padStart(42, '0')}0000100000${(INCREMENT++).toString(2).padStart(12, '0')}`; + return Util.binaryToID(BINARY); } /** @@ -52,7 +53,7 @@ class SnowflakeUtil { * @returns {DeconstructedSnowflake} Deconstructed snowflake */ static deconstruct(snowflake) { - const BINARY = pad(Long.fromString(snowflake).toString(2), 64); + const BINARY = Util.idToBinary(snowflake).toString(2).padStart(64, '0'); const res = { timestamp: parseInt(BINARY.substring(0, 42), 2) + EPOCH, workerID: parseInt(BINARY.substring(42, 47), 2), @@ -68,8 +69,4 @@ class SnowflakeUtil { } } -function pad(v, n, c = '0') { - return String(v).length >= n ? String(v) : (String(c).repeat(n) + v).slice(-n); -} - module.exports = SnowflakeUtil; diff --git a/src/util/Util.js b/src/util/Util.js index ecc1b93e1..46ce22367 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -1,4 +1,3 @@ -const Long = require('long'); const snekfetch = require('snekfetch'); const { Colors, DefaultOptions, Endpoints } = require('./Constants'); const { Error: DiscordError, RangeError, TypeError } = require('../errors'); @@ -299,7 +298,9 @@ class Util { */ static discordSort(collection) { return collection - .sort((a, b) => a.rawPosition - b.rawPosition || Long.fromString(a.id).sub(Long.fromString(b.id)).toNumber()); + .sort((a, b) => a.rawPosition - b.rawPosition || + parseInt(a.id.slice(0, -10)) - parseInt(b.id.slice(0, -10)) || + parseInt(a.id.slice(10)) - parseInt(b.id.slice(10))); } static setPosition(item, position, relative, sorted, route, reason) { @@ -316,6 +317,54 @@ class Util { } return f; } + + /** + * Transform a snowflake from a decimal string to a bit string + * @param {string} num Snowflake to be transformed + * @returns {string} + * @private + */ + static idToBinary(num) { + let bin = ''; + let high = parseInt(num.slice(0, -10)) || 0; + let low = parseInt(num.slice(-10)); + while (low > 0 || high > 0) { + bin = String(low & 1) + bin; + low = Math.floor(low / 2); + if (high > 0) { + low += 5000000000 * (high % 2); + high = Math.floor(high / 2); + } + } + return bin; + } + + + /** + * Transform a snowflake from a bit string to a decimal string + * @param {string} num Bit string to be transformed + * @returns {string} + * @private + */ + static binaryToID(num) { + let dec = ''; + + while (num.length > 50) { + const high = parseInt(num.slice(0, -32), 2); + const low = parseInt((high % 10).toString(2) + num.slice(-32), 2); + + dec = (low % 10).toString() + dec; + num = Math.floor(high / 10).toString(2) + Math.floor(low / 10).toString(2).padStart(32, '0'); + } + + num = parseInt(num, 2); + while (num > 0) { + dec = (num % 10).toString() + dec; + num = Math.floor(num / 10); + } + + return dec; + } } module.exports = Util; From 0f4ca39fa358be6aee3a2f295eba6852b3d66bb1 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 17:12:27 -0500 Subject: [PATCH 28/75] Add Node 9 to Travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fb2f6214c..0c886cf8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: node_js node_js: - "8" + - "9" cache: directories: - node_modules From b7c4df5dc1c98d10c11a96db625c8924f4a9cc12 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 17:19:34 -0500 Subject: [PATCH 29/75] Fix trailing space --- 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 8c796d337..16bda06e0 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -115,7 +115,7 @@ class Guild extends Base { /** * An array of enabled guild features, here are the possible values: - * * INVITE_SPLASH + * * INVITE_SPLASH * * MORE_EMOJI * * VERIFIED * * VIP_REGIONS From 547b9cc2f3814f7ad3affd91bd962d443088acd2 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 17:23:18 -0500 Subject: [PATCH 30/75] Only run Node 8 build for test stage --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0c886cf8c..25878d575 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,4 @@ language: node_js -node_js: - - "8" - - "9" cache: directories: - node_modules @@ -10,8 +7,12 @@ jobs: include: - stage: test script: bash ./travis/test.sh + node_js: + - 8 + - 9 - stage: deploy script: bash ./travis/deploy.sh + node_js: 9 env: global: - ENCRYPTION_LABEL: "af862fa96d3e" From 4a15ccab0f4df072c7794ce0baad7b837467a2eb Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 17:26:03 -0500 Subject: [PATCH 31/75] Maybe fix dumb Travis behaviour --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 25878d575..a82715f55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,7 @@ language: node_js +node_js: + - 8 + - 9 cache: directories: - node_modules @@ -7,9 +10,6 @@ jobs: include: - stage: test script: bash ./travis/test.sh - node_js: - - 8 - - 9 - stage: deploy script: bash ./travis/deploy.sh node_js: 9 From 297ac4e4dbf7134130d9278a9d6bcf910feb3fd0 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 17:33:54 -0500 Subject: [PATCH 32/75] Still mucking about with Travis --- .travis.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a82715f55..89d0baf36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,14 +2,10 @@ language: node_js node_js: - 8 - 9 -cache: - directories: - - node_modules install: npm install +script: bash ./travis/test.sh jobs: include: - - stage: test - script: bash ./travis/test.sh - stage: deploy script: bash ./travis/deploy.sh node_js: 9 @@ -17,5 +13,8 @@ env: global: - ENCRYPTION_LABEL: "af862fa96d3e" - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" +cache: + directories: + - node_modules dist: trusty sudo: false From 127d87dca82d560d488295510755fabc041dc7b7 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 17:45:06 -0500 Subject: [PATCH 33/75] Hopefully remove unnecessary work from Travis tests --- .travis.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 89d0baf36..675e03ebc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,11 @@ script: bash ./travis/test.sh jobs: include: - stage: deploy - script: bash ./travis/deploy.sh node_js: 9 -env: - global: - - ENCRYPTION_LABEL: "af862fa96d3e" - - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" + script: bash ./travis/deploy.sh + env: + - ENCRYPTION_LABEL: "af862fa96d3e" + - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" cache: directories: - node_modules From abd6156a90de362ac751c1d39aa4fa9474d2f5c9 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 17:50:30 -0500 Subject: [PATCH 34/75] Maybe fix Travis some more --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 675e03ebc..3b89d2afa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ jobs: node_js: 9 script: bash ./travis/deploy.sh env: - - ENCRYPTION_LABEL: "af862fa96d3e" - - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" + - ENCRYPTION_LABEL="af862fa96d3e" + - COMMIT_AUTHOR_EMAIL="amishshah.2k@gmail.com" cache: directories: - node_modules From c622143e3944afd3baae20e213a43e929fe6b890 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 17:54:50 -0500 Subject: [PATCH 35/75] Remove Node version check from deploy script --- travis/deploy.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/travis/deploy.sh b/travis/deploy.sh index 340087312..ab6116008 100644 --- a/travis/deploy.sh +++ b/travis/deploy.sh @@ -24,12 +24,6 @@ else SOURCE_TYPE="branch" fi -# For Node != 8, do nothing -if [ "$TRAVIS_NODE_VERSION" != "8" ]; then - echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - doing nothing." - exit 0 -fi - # Run the build npm run docs VERSIONED=false npm run webpack @@ -88,4 +82,3 @@ git config user.name "Travis CI" git config user.email "$COMMIT_AUTHOR_EMAIL" git commit -m "Webpack build for ${SOURCE_TYPE} ${SOURCE}: ${SHA}" || true git push $SSH_REPO $TARGET_BRANCH - From b859501b6f980c0f6117a27e21c38a3e4db3e2bc Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 20:30:13 -0500 Subject: [PATCH 36/75] Document and clean up some garbage --- src/structures/CategoryChannel.js | 2 +- src/structures/Guild.js | 43 ++++++++++++++++++------- src/util/Util.js | 53 ++++++++++++++++++------------- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index 2c063f73d..d7121a32b 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -6,7 +6,7 @@ const GuildChannel = require('./GuildChannel'); */ class CategoryChannel extends GuildChannel { /** - * The channels that are part of this category + * Channels that are part of this category * @type {?Collection} * @readonly */ diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 16bda06e0..1eff773a2 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1067,8 +1067,7 @@ class Guild extends Base { .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); } - return DataResolver.resolveImage(attachment) - .then(image => this.createEmoji(image, name, { roles, reason })); + return DataResolver.resolveImage(attachment).then(image => this.createEmoji(image, name, { roles, reason })); } /** @@ -1146,6 +1145,34 @@ class Guild extends Base { return this.name; } + /** + * Creates a collection of this guild's roles, sorted by their position and IDs. + * @returns {Collection} + * @private + */ + _sortedRoles() { + return Util.discordSort(this.roles); + } + + /** + * Creates a collection of this guild's or a specific category's channels, sorted by their position and IDs. + * @param {GuildChannel} [channel] Category to get the channels of + * @returns {Collection} + * @private + */ + _sortedChannels(channel) { + const category = channel.type === ChannelTypes.CATEGORY; + return Util.discordSort(this.channels.filter(c => + c.type === channel.type && (category || c.parent === channel.parent) + )); + } + + /** + * Handles a user speaking update in a voice channel. + * @param {Snowflake} user ID of the user that the update is for + * @param {boolean} speaking Whether the user is speaking + * @private + */ _memberSpeakUpdate(user, speaking) { const member = this.members.get(user); if (member && member.speaking !== speaking) { @@ -1159,23 +1186,15 @@ class Guild extends Base { this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking); } } - - _sortedRoles() { - return Util.discordSort(this.roles); - } - - _sortedChannels(channel) { - const category = channel.type === ChannelTypes.CATEGORY; - return Util.discordSort(this.channels.filter(c => - c.type === channel.type && (category || c.parent === channel.parent))); - } } +// TODO: Document this thing class VoiceStateCollection extends Collection { constructor(guild) { super(); this.guild = guild; } + set(id, voiceState) { const member = this.guild.members.get(id); if (member) { diff --git a/src/util/Util.js b/src/util/Util.js index 46ce22367..26df930ce 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -21,9 +21,7 @@ class Util { static splitMessage(text, { maxLength = 1950, char = '\n', prepend = '', append = '' } = {}) { if (text.length <= maxLength) return text; const splitText = text.split(char); - if (splitText.length === 1) { - throw new RangeError('SPLIT_MAX_LEN'); - } + if (splitText.length === 1) throw new RangeError('SPLIT_MAX_LEN'); const messages = ['']; let msg = 0; for (let i = 0; i < splitText.length; i++) { @@ -83,10 +81,7 @@ class Util { const [name, id] = text.split(':'); return { name, id }; } else { - return { - name: text, - id: null, - }; + return { name: text, id: null }; } } @@ -225,7 +220,6 @@ class Util { * @param {StringResolvable} data The string resolvable to resolve * @returns {string} */ - static resolveString(data) { if (typeof data === 'string') return data; if (data instanceof Array) return data.join('\n'); @@ -273,7 +267,6 @@ class Util { * @param {ColorResolvable} color Color to resolve * @returns {number} A color */ - static resolveColor(color) { if (typeof color === 'string') { if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1)); @@ -292,17 +285,29 @@ class Util { } /** - * Sorts by Discord's position and then by ID. + * Sorts by Discord's position and ID. * @param {Collection} collection Collection of objects to sort * @returns {Collection} */ static discordSort(collection) { - return collection - .sort((a, b) => a.rawPosition - b.rawPosition || - parseInt(a.id.slice(0, -10)) - parseInt(b.id.slice(0, -10)) || - parseInt(a.id.slice(10)) - parseInt(b.id.slice(10))); + return collection.sort((a, b) => + a.rawPosition - b.rawPosition || + parseInt(a.id.slice(0, -10)) - parseInt(b.id.slice(0, -10)) || + parseInt(a.id.slice(10)) - parseInt(b.id.slice(10)) + ); } + /** + * Sets the position of a Channel or Role. + * @param {Channel|Role} item Object to set the position of + * @param {number} position New position for the object + * @param {boolean} relative Whether `position` is relative to its current position + * @param {Collection} sorted A collection of the objects sorted properly + * @param {APIRouter} route Route to call PATCH on + * @param {string} [reason] Reason for the change + * @returns {Promise} Updated item list, with `id` and `position` properties + * @private + */ static setPosition(item, position, relative, sorted, route, reason) { let updatedItems = sorted.array(); Util.moveElementInArray(updatedItems, item, position, relative); @@ -310,17 +315,22 @@ class Util { return route.patch({ data: updatedItems, reason }).then(() => updatedItems); } + /** + * Alternative to Node's `path.basename` that we have for some (probably stupid) reason. + * @param {string} path Path to get the basename of + * @param {string} [ext] File extension to remove + * @returns {string} Basename of the path + * @private + */ static basename(path, ext) { let f = splitPathRe.exec(path).slice(1)[2]; - if (ext && f.substr(-1 * ext.length) === ext) { - f = f.substr(0, f.length - ext.length); - } + if (ext && f.substr(-1 * ext.length) === ext) f = f.substr(0, f.length - ext.length); return f; } /** - * Transform a snowflake from a decimal string to a bit string - * @param {string} num Snowflake to be transformed + * Transforms a snowflake from a decimal string to a bit string. + * @param {Snowflake} num Snowflake to be transformed * @returns {string} * @private */ @@ -339,11 +349,10 @@ class Util { return bin; } - /** - * Transform a snowflake from a bit string to a decimal string + * Transforms a snowflake from a bit string to a decimal string. * @param {string} num Bit string to be transformed - * @returns {string} + * @returns {Snowflake} * @private */ static binaryToID(num) { From a2074e8f255ffbf692b63e4f34f903d928ee4a1c Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 21:08:06 -0500 Subject: [PATCH 37/75] Bump deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 08e2b200d..208de266c 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,9 @@ "dependencies": { "pako": "^1.0.0", "prism-media": "^0.0.2", - "snekfetch": "^3.0.0", + "snekfetch": "^3.5.0", "tweetnacl": "^1.0.0", - "ws": "^3.0.0" + "ws": "^3.3.1" }, "peerDependencies": { "bufferutil": "^3.0.0", @@ -51,12 +51,12 @@ "devDependencies": { "@types/node": "^8.0.0", "discord.js-docgen": "hydrabolt/discord.js-docgen", - "eslint": "^4.0.0", + "eslint": "^4.11.0", "jsdoc-strip-async-await": "^0.1.0", "json-filter-loader": "^1.0.0", - "parallel-webpack": "^2.0.0", + "parallel-webpack": "^2.2.0", "uglifyjs-webpack-plugin": "^1.0.0-beta.2", - "webpack": "^3.0.0" + "webpack": "^3.8.0" }, "engines": { "node": ">=8.0.0" From 5703e01132f06feb467b9dda5e7481449b52fd00 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sun, 19 Nov 2017 05:25:26 +0100 Subject: [PATCH 38/75] update typings --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index 697fc933d..0b5b13f4a 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 697fc933de90209b81b69bd0fe87883e3c7a217d +Subproject commit 0b5b13f4a521cba0fc42aa0f9b2c4a1abca2de3d From f4ac06024e817745b943a3a77b9fedc9d25d3e76 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 18 Nov 2017 23:34:34 -0500 Subject: [PATCH 39/75] Improve ColorResolvable docs --- src/util/Util.js | 66 +++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 26df930ce..b1de917be 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -227,39 +227,34 @@ class Util { } /** - * Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following + * Can be a number, hex string, an RGB array like: + * ```js + * [255, 0, 255] // purple * ``` - * [ - * 'DEFAULT', - * 'AQUA', - * 'GREEN', - * 'BLUE', - * 'PURPLE', - * 'GOLD', - * 'ORANGE', - * 'RED', - * 'GREY', - * 'DARKER_GREY', - * 'NAVY', - * 'DARK_AQUA', - * 'DARK_GREEN', - * 'DARK_BLUE', - * 'DARK_PURPLE', - * 'DARK_GOLD', - * 'DARK_ORANGE', - * 'DARK_RED', - * 'DARK_GREY', - * 'LIGHT_GREY', - * 'DARK_NAVY', - * 'RANDOM', - * ] - * ``` - * or something like - * ``` - * [255, 0, 255] - * ``` - * for purple - * @typedef {string|number|Array} ColorResolvable + * or one of the following strings: + * - `DEFAULT` + * - `AQUA` + * - `GREEN` + * - `BLUE` + * - `PURPLE` + * - `GOLD` + * - `ORANGE` + * - `RED` + * - `GREY` + * - `DARKER_GREY` + * - `NAVY` + * - `DARK_AQUA` + * - `DARK_GREEN` + * - `DARK_BLUE` + * - `DARK_PURPLE` + * - `DARK_GOLD` + * - `DARK_ORANGE` + * - `DARK_RED` + * - `DARK_GREY` + * - `LIGHT_GREY` + * - `DARK_NAVY` + * - `RANDOM` + * @typedef {string|number|number[]} ColorResolvable */ /** @@ -275,11 +270,8 @@ class Util { color = (color[0] << 16) + (color[1] << 8) + color[2]; } - if (color < 0 || color > 0xFFFFFF) { - throw new RangeError('COLOR_RANGE'); - } else if (color && isNaN(color)) { - throw new TypeError('COLOR_CONVERT'); - } + if (color < 0 || color > 0xFFFFFF) throw new RangeError('COLOR_RANGE'); + else if (color && isNaN(color)) throw new TypeError('COLOR_CONVERT'); return color; } From a414e4884f707098317f437fd5caa7c6c118d577 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 01:28:46 -0500 Subject: [PATCH 40/75] Overhaul sharding --- .eslintrc.json | 6 ++- src/errors/Messages.js | 10 ++-- src/sharding/Shard.js | 91 ++++++++++++++++++++++++++++----- src/sharding/ShardingManager.js | 83 +++++++++++++++--------------- 4 files changed, 132 insertions(+), 58 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 8b21a1523..979a9acc1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -127,7 +127,11 @@ "semi-spacing": "error", "semi": "error", "space-before-blocks": "error", - "space-before-function-paren": ["error", "never"], + "space-before-function-paren": ["error", { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + }], "space-in-parens": "error", "space-infix-ops": "error", "space-unary-ops": "error", diff --git a/src/errors/Messages.js b/src/errors/Messages.js index c3116a203..545452703 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -20,9 +20,13 @@ const Messages = { SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.', SHARDING_CHILD_CONNECTION: 'Failed to send message to shard\'s process.', SHARDING_PARENT_CONNECTION: 'Failed to send message to master process.', - SHARDING_NO_SHARDS: 'No shards have been spawned', - SHARDING_IN_PROCESS: 'Shards are still being spawned', - SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards`, + SHARDING_NO_SHARDS: 'No shards have been spawned.', + SHARDING_IN_PROCESS: 'Shards are still being spawned.', + SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`, + SHARDING_PROCESS_EXISTS: id => `Shard ${id} already has an active process.`, + SHARDING_READY_TIMEOUT: id => `Shard ${id}'s Client took too long to become ready.`, + SHARDING_READY_DISCONNECTED: id => `Shard ${id}'s Client disconnected before becoming ready.`, + SHARDING_READY_DIED: id => `Shard ${id}'s process exited before its Client became ready.`, COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).', COLOR_CONVERT: 'Unable to convert color to a number.', diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 7e2a51ac7..f71c389df 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -3,6 +3,7 @@ const EventEmitter = require('events'); const path = require('path'); const Util = require('../util/Util'); const { Error } = require('../errors'); +const delayFor = require('util').promisify(setTimeout); /** * Represents a Shard spawned by the ShardingManager. @@ -15,6 +16,7 @@ class Shard extends EventEmitter { */ constructor(manager, id, args = []) { super(); + /** * Manager that created the shard * @type {ShardingManager} @@ -27,6 +29,12 @@ class Shard extends EventEmitter { */ this.id = id; + /** + * Arguments for the shard's process + * @type {string[]} + */ + this.args = args; + /** * Environment variables for the shard's process * @type {Object} @@ -37,20 +45,18 @@ class Shard extends EventEmitter { CLIENT_TOKEN: this.manager.token, }); - /** - * Process of the shard - * @type {ChildProcess} - */ - this.process = childProcess.fork(path.resolve(this.manager.file), args, { - env: this.env, - }).on('message', this._handleMessage.bind(this)); - /** * Whether the shard's {@link Client} is ready * @type {boolean} */ this.ready = false; + /** + * Process of the shard + * @type {?ChildProcess} + */ + this.process = null; + /** * Ongoing promises for calls to {@link Shard#eval}, mapped by the `script` they were called with * @type {Map} @@ -65,11 +71,55 @@ class Shard extends EventEmitter { */ this._fetches = new Map(); - // Handle the death of the process - this.process.once('exit', () => { - this.ready = false; - if (this.manager.respawn) this.manager.createShard(this.id).catch(err => { this.manager.emit('error', err); }); + /** + * Listener function for the {@link ChildProcess}' `exit` event + * @type {Function} + */ + this._exitListener = this._handleExit.bind(this); + } + + /** + * Forks a child process 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); + + this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { env: this.env }) + .on('message', this._handleMessage.bind(this)) + .on('exit', this._exitListener); + + /** + * Emitted upon the creation of the shard's child process. + * @event Shard#spawn + * @param {ChildProcess} process Child process that was created + */ + this.emit('spawn', this.process); + + if (!waitForReady) return this.process; + await new Promise((resolve, reject) => { + this.once('ready', resolve); + this.once('disconnect', () => reject(new Error('SHARDING_READY_DISCONNECTED', this.id))); + this.once('death', () => reject(new Error('SHARDING_READY_DIED', this.id))); + setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), 30000); }); + return this.process; + } + + /** + * Kills and restarts the shard's process. + * @param {number} [delay=500] How long to wait between killing the process and restarting it (in milliseconds) + * @param {boolean} [waitForReady=true] Whether to wait the {@link Client} has become ready before resolving + * @returns {Promise} + */ + async respawn(delay = 500, waitForReady = true) { + this.process.removeListener('exit', this._exitListener); + this.process.kill(); + this._handleExit(false); + if (delay > 0) await delayFor(delay); + return this.spawn(waitForReady); } /** @@ -215,6 +265,23 @@ class Shard extends EventEmitter { */ this.manager.emit('message', this, message); } + + /** + * Handles the shard's process exiting. + * @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again + * @private + */ + _handleExit(respawn = this.manager.respawn) { + /** + * Emitted upon the shard's child process exiting. + * @event Shard#death + * @param {ChildProcess} process Child process that exited + */ + this.emit('death', this.process); + this.ready = false; + this.process = null; + if (respawn) this.spawn().catch(err => this.emit('error', err)); + } } module.exports = Shard; diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 117fca4f9..0c7de0ab0 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -5,6 +5,7 @@ const Shard = require('./Shard'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const { Error, TypeError, RangeError } = require('../errors'); +const delayFor = require('util').promisify(setTimeout); /** * This is a utility class that can be used to help you spawn shards of your client. Each shard is completely separate @@ -82,33 +83,32 @@ class ShardingManager extends EventEmitter { /** * Spawns a single shard. - * @param {number} id The ID of the shard to spawn. **This is usually not necessary** - * @returns {Promise} + * @param {number} id ID of the shard to spawn. **This is usually not necessary** + * @returns {Shard} */ createShard(id = this.shards.size) { const shard = new Shard(this, id, this.shardArgs); this.shards.set(id, shard); /** - * Emitted upon launching a shard. + * Emitted upon creating a shard. * @event ShardingManager#launch - * @param {Shard} shard Shard that was launched + * @param {Shard} shard Shard that was created */ this.emit('launch', shard); - return Promise.resolve(shard); + return shard; } /** * Spawns multiple shards. * @param {number} [amount=this.totalShards] Number of shards to spawn - * @param {number} [delay=7500] How long to wait in between spawning each shard (in milliseconds) + * @param {number} [delay=5500] How long to wait in between spawning each shard (in milliseconds) + * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another * @returns {Promise>} */ - spawn(amount = this.totalShards, delay = 7500) { + async spawn(amount = this.totalShards, delay = 5500, waitForReady = true) { + // Obtain/verify the number of shards to spawn if (amount === 'auto') { - return Util.fetchRecommendedShards(this.token).then(count => { - this.totalShards = count; - return this._spawn(count, delay); - }); + amount = await Util.fetchRecommendedShards(this.token); } else { if (typeof amount !== 'number' || isNaN(amount)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); @@ -117,41 +117,22 @@ class ShardingManager extends EventEmitter { if (amount !== Math.floor(amount)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); } - return this._spawn(amount, delay); } - } - /** - * Actually spawns shards, unlike that poser above >:( - * @param {number} amount Number of shards to spawn - * @param {number} delay How long to wait in between spawning each shard (in milliseconds) - * @returns {Promise>} - * @private - */ - _spawn(amount, delay) { - return new Promise(resolve => { - if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size); - this.totalShards = amount; + // Make sure this many shards haven't already been spawned + if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size); + this.totalShards = amount; - this.createShard(); - if (this.shards.size >= this.totalShards) { - resolve(this.shards); - return; - } + // Spawn the shards + for (let s = 1; s <= amount; s++) { + const promises = []; + const shard = this.createShard(); + promises.push(shard.spawn(waitForReady)); + if (delay > 0 && s !== amount) promises.push(delayFor(delay)); + await Promise.all(promises); // eslint-disable-line no-await-in-loop + } - if (delay <= 0) { - while (this.shards.size < this.totalShards) this.createShard(); - resolve(this.shards); - } else { - const interval = setInterval(() => { - this.createShard(); - if (this.shards.size >= this.totalShards) { - clearInterval(interval); - resolve(this.shards); - } - }, delay); - } - }); + return this.shards; } /** @@ -194,6 +175,24 @@ class ShardingManager extends EventEmitter { for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop)); return Promise.all(promises); } + + /** + * Kills all running shards and respawns them. + * @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds) + * @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it + * (in milliseconds) + * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another + * @returns {Promise>} + */ + async respawn(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { + let s = 0; + for (const shard of this.shards) { + const promises = [shard.respawn(respawnDelay, waitForReady)]; + if (++s < this.shards.size && shardDelay > 0) promises.push(delayFor(shardDelay)); + await Promise.all(promises); // eslint-disable-line no-await-in-loop + } + return this.shards; + } } module.exports = ShardingManager; From 2a332d8d15b0f06d8fcc236a4aab0aeff0277d1f Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 01:54:10 -0500 Subject: [PATCH 41/75] Add ShardClientUtil#respawnAll --- src/sharding/Shard.js | 9 +++++++++ src/sharding/ShardClientUtil.js | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index f71c389df..6ae4cc600 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -255,6 +255,15 @@ class Shard extends EventEmitter { ); return; } + + // Shard is requesting a respawn of all shards + if (message._sRespawnAll) { + const { shardDelay, respawnDelay, waitForReady } = message._sRespawnAll; + this.manager.respawn(shardDelay, respawnDelay, waitForReady).catch(() => { + // Do nothing + }); + return; + } } /** diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 70e625824..df1bc8341 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -59,6 +59,7 @@ class ShardClientUtil { * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`); * }) * .catch(console.error); + * @see {@link ShardingManager#fetchClientValues} */ fetchClientValues(prop) { return new Promise((resolve, reject) => { @@ -80,6 +81,7 @@ class ShardClientUtil { * Evaluates a script on all shards, in the context of the Clients. * @param {string} script JavaScript to run on each shard * @returns {Promise>} Results of the script execution + * @see {@link ShardingManager#broadcastEval} */ broadcastEval(script) { return new Promise((resolve, reject) => { @@ -97,6 +99,19 @@ class ShardClientUtil { }); } + /** + * Requests a respawn of all shards. + * @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds) + * @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it + * (in milliseconds) + * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another + * @returns {Promise} Resolves upon the message being sent + * @see {@link ShardingManager#respawn} + */ + respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { + return this.send({ _sRespawnAll: { shardDelay, respawnDelay, waitForReady } }); + } + /** * Handles an IPC message. * @param {*} message Message received From 637ea09532e1b88204680bb78a443972d952a580 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 01:56:51 -0500 Subject: [PATCH 42/75] Fix lint error --- src/client/voice/util/Secretbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/util/Secretbox.js b/src/client/voice/util/Secretbox.js index 31f5b8d00..b21fb8f9d 100644 --- a/src/client/voice/util/Secretbox.js +++ b/src/client/voice/util/Secretbox.js @@ -15,7 +15,7 @@ const libs = { exports.methods = {}; -(async() => { +(async () => { for (const libName of Object.keys(libs)) { try { const lib = require(libName); From f777c19fbf3e770cc54531241c24ec19e6ae9952 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 02:03:44 -0500 Subject: [PATCH 43/75] Fix naming conflict with ShardingManager#respawn --- src/sharding/Shard.js | 2 +- src/sharding/ShardClientUtil.js | 2 +- src/sharding/ShardingManager.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 6ae4cc600..cbe85110b 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -259,7 +259,7 @@ class Shard extends EventEmitter { // Shard is requesting a respawn of all shards if (message._sRespawnAll) { const { shardDelay, respawnDelay, waitForReady } = message._sRespawnAll; - this.manager.respawn(shardDelay, respawnDelay, waitForReady).catch(() => { + this.manager.respawnAll(shardDelay, respawnDelay, waitForReady).catch(() => { // Do nothing }); return; diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index df1bc8341..b62cb36c4 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -106,7 +106,7 @@ class ShardClientUtil { * (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 - * @see {@link ShardingManager#respawn} + * @see {@link ShardingManager#respawnAll} */ respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { return this.send({ _sRespawnAll: { shardDelay, respawnDelay, waitForReady } }); diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 0c7de0ab0..85b2f2c67 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -184,7 +184,7 @@ class ShardingManager extends EventEmitter { * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another * @returns {Promise>} */ - async respawn(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { + async respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { let s = 0; for (const shard of this.shards) { const promises = [shard.respawn(respawnDelay, waitForReady)]; From 9cd097492c8a6cc2fe834d28fb31d4ad46054965 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 02:06:38 -0500 Subject: [PATCH 44/75] Update doc for ShardingManager#createShard id parameter --- src/sharding/ShardingManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 85b2f2c67..bbe5e7724 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -83,7 +83,8 @@ class ShardingManager extends EventEmitter { /** * Spawns a single shard. - * @param {number} id ID of the shard to spawn. **This is usually not necessary** + * @param {number} [id=this.shards.size] ID of the shard to spawn - + * **This is usually not necessary to manually specify.** * @returns {Shard} */ createShard(id = this.shards.size) { From 975da5f1a52e821a9409fe15f64b3ba4bd387520 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 02:21:29 -0500 Subject: [PATCH 45/75] Rewrite sharding class descriptions and link Client --- src/sharding/Shard.js | 5 +++-- src/sharding/ShardClientUtil.js | 5 +++-- src/sharding/ShardingManager.js | 11 +++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index cbe85110b..b54701f40 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -6,7 +6,8 @@ const { Error } = require('../errors'); const delayFor = require('util').promisify(setTimeout); /** - * Represents a Shard spawned by the ShardingManager. + * A self-contained shard spawned by the {@link ShardingManager}. + * @extends EventEmitter */ class Shard extends EventEmitter { /** @@ -171,7 +172,7 @@ class Shard extends EventEmitter { } /** - * Evaluates a script on the shard, in the context of the client. + * Evaluates a script on the shard, in the context of the {@link Client}. * @param {string} script JavaScript to run on the shard * @returns {Promise<*>} Result of the script execution */ diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index b62cb36c4..b0e9d57ed 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -3,7 +3,8 @@ const { Events } = require('../util/Constants'); const { Error } = require('../errors'); /** - * Helper class for sharded clients spawned as a child process, such as from a ShardingManager. + * Helper class for sharded clients spawned as a child process, such as from a {@link ShardingManager}. + * Utilises IPC to send and receive data to/from the master process and other shards. */ class ShardClientUtil { /** @@ -78,7 +79,7 @@ class ShardClientUtil { } /** - * Evaluates a script on all shards, in the context of the Clients. + * Evaluates a script on all shards, in the context of the {@link Clients}. * @param {string} script JavaScript to run on each shard * @returns {Promise>} Results of the script execution * @see {@link ShardingManager#broadcastEval} diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index bbe5e7724..4b0ad7b87 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -8,9 +8,12 @@ const { Error, TypeError, RangeError } = require('../errors'); const delayFor = require('util').promisify(setTimeout); /** - * This is a utility class that can be used to help you spawn shards of your client. Each shard is completely separate - * from the other. The Shard Manager takes a path to a file and spawns it under the specified amount of shards safely. - * If you do not select an amount of shards, the manager will automatically decide the best amount. + * This is a utility class that makes multi-process sharding of a bot an easy and painless experience. + * It works by spawning a self-contained {@link ChildProcess} for each individual shard, each containing its own 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 multi-process 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 { @@ -148,7 +151,7 @@ class ShardingManager extends EventEmitter { } /** - * Evaluates a script on all shards, in the context of the Clients. + * Evaluates a script on all shards, in the context of the {@link Client}s. * @param {string} script JavaScript to run on each shard * @returns {Promise>} Results of the script execution */ From 1338e9bd8e2a29ebe2ce9b45bc651adc1bf5601f Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 02:30:20 -0500 Subject: [PATCH 46/75] Update sharding docs some more --- src/sharding/Shard.js | 4 +++- src/sharding/ShardingManager.js | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index b54701f40..cff80ad38 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -6,7 +6,9 @@ const { Error } = require('../errors'); const delayFor = require('util').promisify(setTimeout); /** - * A self-contained shard spawned by the {@link ShardingManager}. + * A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains + * an instance of the bot and its {@link Client}. When its child process exits for any reason, the shard will spawn a + * new one to replace it as necessary. * @extends EventEmitter */ class Shard extends EventEmitter { diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 4b0ad7b87..f682c5887 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -9,11 +9,11 @@ const delayFor = require('util').promisify(setTimeout); /** * 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 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 multi-process 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} 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 { From acf82f32c3d25608ec0bcd03f095242cf937882a Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 02:31:06 -0500 Subject: [PATCH 47/75] Mark Shard#_exitListener as private --- src/sharding/Shard.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index cff80ad38..e5c59b395 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -77,6 +77,7 @@ class Shard extends EventEmitter { /** * Listener function for the {@link ChildProcess}' `exit` event * @type {Function} + * @private */ this._exitListener = this._handleExit.bind(this); } From 26b28813a83aa5888419e6538ff35d1bf3dae106 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 13:47:04 -0500 Subject: [PATCH 48/75] Use a custom promisified setTimeout --- src/sharding/Shard.js | 3 +-- src/sharding/ShardingManager.js | 5 ++--- src/util/Util.js | 12 ++++++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index e5c59b395..044882bec 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -3,7 +3,6 @@ const EventEmitter = require('events'); const path = require('path'); const Util = require('../util/Util'); const { Error } = require('../errors'); -const delayFor = require('util').promisify(setTimeout); /** * A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains @@ -122,7 +121,7 @@ class Shard extends EventEmitter { this.process.removeListener('exit', this._exitListener); this.process.kill(); this._handleExit(false); - if (delay > 0) await delayFor(delay); + if (delay > 0) await Util.delayFor(delay); return this.spawn(waitForReady); } diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index f682c5887..e8e8f8706 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -5,7 +5,6 @@ const Shard = require('./Shard'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const { Error, TypeError, RangeError } = require('../errors'); -const delayFor = require('util').promisify(setTimeout); /** * This is a utility class that makes multi-process sharding of a bot an easy and painless experience. @@ -132,7 +131,7 @@ class ShardingManager extends EventEmitter { const promises = []; const shard = this.createShard(); promises.push(shard.spawn(waitForReady)); - if (delay > 0 && s !== amount) promises.push(delayFor(delay)); + if (delay > 0 && s !== amount) promises.push(Util.delayFor(delay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } @@ -192,7 +191,7 @@ class ShardingManager extends EventEmitter { let s = 0; for (const shard of this.shards) { const promises = [shard.respawn(respawnDelay, waitForReady)]; - if (++s < this.shards.size && shardDelay > 0) promises.push(delayFor(shardDelay)); + if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } return this.shards; diff --git a/src/util/Util.js b/src/util/Util.js index b1de917be..d92dc94b7 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -366,6 +366,18 @@ class Util { return dec; } + + /** + * Creates a Promise that resolves after a specified duration. + * @param {number} ms How long to wait before resolving (in milliseconds) + * @returns {Promise} + * @private + */ + static delayFor(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); + } } module.exports = Util; From cfa512c447469956d738df938137f97104734363 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 21:16:14 -0500 Subject: [PATCH 49/75] Make structures for data stores extensible --- src/index.js | 1 + src/stores/DataStore.js | 3 ++- src/util/Structures.js | 54 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/util/Structures.js diff --git a/src/index.js b/src/index.js index de6a6e66c..90cf5b4c0 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ module.exports = { Permissions: require('./util/Permissions'), Snowflake: require('./util/Snowflake'), SnowflakeUtil: require('./util/Snowflake'), + Structures: require('./util/Structures'), Util: Util, util: Util, version: require('../package.json').version, diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 398910d50..fc9aa414d 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -1,4 +1,5 @@ const Collection = require('../util/Collection'); +const Structures = require('../util/Structures'); /** * Manages the creation, retrieval and deletion of a specific data model. @@ -8,7 +9,7 @@ class DataStore extends Collection { constructor(client, iterable, holds) { super(); Object.defineProperty(this, 'client', { value: client }); - Object.defineProperty(this, 'holds', { value: holds }); + Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) }); if (iterable) for (const item of iterable) this.create(item); } diff --git a/src/util/Structures.js b/src/util/Structures.js new file mode 100644 index 000000000..d09684c50 --- /dev/null +++ b/src/util/Structures.js @@ -0,0 +1,54 @@ +/** + * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore}s. + * When extending a built-in structure, it is important to both get the class you're extending from here, + * and to set it here afterwards. + * @example + * const { Structures } = require('discord.js); + * + * class CoolGuild extends Structures.get('Guild') { + * constructor(client, data) { + * super(client, data); + * this.cool = true; + * } + * } + * + * Structures.set('Guild', CoolGuild); + */ +class Structures { + constructor() { + throw new Error(`The ${this.constructor.name} class may not be instantiated.`); + } + + /** + * Retrieves a structure class. + * @param {string} name Name of the base structure + * @returns {Function} + */ + static get(name) { + return structures[name]; + } + + /** + * Overrides a structure class. + * @param {string} name Name of the base structure + * @param {Function} custom Extended structure class to override with + */ + static set(name, custom) { + structures[name] = custom; + } +} + +const structures = { + Channel: require('./structures/Channel'), + Emoji: require('./structures/Emoji'), + GuildChannel: require('./structures/GuildChannel'), + GuildMember: require('./structures/GuildMember'), + Guild: require('./structures/Guild'), + Message: require('./structures/Message'), + Presence: require('./structures/Presence'), + Reaction: require('./structures/Reaction'), + Role: require('./structures/Role'), + User: require('./structures/User'), +}; + +module.exports = Structures; From c29804e3f891095a7d2837fc7b38d9cd85a22573 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 21:19:46 -0500 Subject: [PATCH 50/75] i aint do nuffin --- src/util/Structures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index d09684c50..8c137ff8e 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -3,7 +3,7 @@ * When extending a built-in structure, it is important to both get the class you're extending from here, * and to set it here afterwards. * @example - * const { Structures } = require('discord.js); + * const { Structures } = require('discord.js'); * * class CoolGuild extends Structures.get('Guild') { * constructor(client, data) { From f13c6d0768f50117baf28e44fc1836497e7e9782 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 21:28:05 -0500 Subject: [PATCH 51/75] Add ID to logo in welcome --- 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 11815ee13..84b06ed33 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -1,7 +1,7 @@


- discord.js +


From 0291fe41d8d04c0c4ccb0b44274c9f6841d436a0 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 22:09:43 -0500 Subject: [PATCH 52/75] Fix structure paths --- src/util/Structures.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index 8c137ff8e..44586a96f 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -39,16 +39,16 @@ class Structures { } const structures = { - Channel: require('./structures/Channel'), - Emoji: require('./structures/Emoji'), - GuildChannel: require('./structures/GuildChannel'), - GuildMember: require('./structures/GuildMember'), - Guild: require('./structures/Guild'), - Message: require('./structures/Message'), - Presence: require('./structures/Presence'), - Reaction: require('./structures/Reaction'), - Role: require('./structures/Role'), - User: require('./structures/User'), + Channel: require('../structures/Channel'), + Emoji: require('../structures/Emoji'), + GuildChannel: require('../structures/GuildChannel'), + GuildMember: require('../structures/GuildMember'), + Guild: require('../structures/Guild'), + Message: require('../structures/Message'), + Presence: require('../structures/Presence'), + Reaction: require('../structures/Reaction'), + Role: require('../structures/Role'), + User: require('../structures/User'), }; module.exports = Structures; From dc379519d3c879d39cf94724d7876b285ef71e1a Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 22:30:37 -0500 Subject: [PATCH 53/75] Fix reaction structure name --- src/util/Structures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index 44586a96f..f63c884a5 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -45,8 +45,8 @@ const structures = { GuildMember: require('../structures/GuildMember'), Guild: require('../structures/Guild'), Message: require('../structures/Message'), + MessageReaction: require('../structures/MessageReaction'), Presence: require('../structures/Presence'), - Reaction: require('../structures/Reaction'), Role: require('../structures/Role'), User: require('../structures/User'), }; From 47dc8fd0466dbdc1af6c33f995fa9108c4a45841 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 22:41:45 -0500 Subject: [PATCH 54/75] Overhaul the way structures are extended --- src/util/Structures.js | 59 +++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index f63c884a5..8a22ee703 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -1,18 +1,5 @@ /** - * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore}s. - * When extending a built-in structure, it is important to both get the class you're extending from here, - * and to set it here afterwards. - * @example - * const { Structures } = require('discord.js'); - * - * class CoolGuild extends Structures.get('Guild') { - * constructor(client, data) { - * super(client, data); - * this.cool = true; - * } - * } - * - * Structures.set('Guild', CoolGuild); + * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}. */ class Structures { constructor() { @@ -20,21 +7,41 @@ class Structures { } /** - * Retrieves a structure class. - * @param {string} name Name of the base structure - * @returns {Function} + * Extends a structure. + * @param {string} name Name of the structure class to extend + * @param {Function} extender Function that takes the base class to extend as its only parameter and returns the + * extended class/prototype + * @returns {Function} Extended class/prototype returned from the extender + * @example + * const { Structures } = require('discord.js'); + * + * Structures.extend('Guild', Guild => + * class CoolGuild extends Guild { + * constructor(client, data) { + * super(client, data); + * this.cool = true; + * } + * } + * ); */ - static get(name) { - return structures[name]; - } + extend(name, extender) { + if (!structures[name]) throw new RangeError(`"${name}" is not a valid extensible structure.`); + if (typeof extender !== 'function') { + throw new TypeError('The extender must be a function that returns the extended class.'); + } + + const custom = extender(structures[name]); + if (typeof custom !== 'function') { + throw new TypeError('The extender function should return the extended class/prototype.'); + } + if (Object.getPrototypeOf(custom) !== structures[name]) { + throw new Error( + 'The class/prototype returned from the extender function must extend the existing structure class/prototype.' + ); + } - /** - * Overrides a structure class. - * @param {string} name Name of the base structure - * @param {Function} custom Extended structure class to override with - */ - static set(name, custom) { structures[name] = custom; + return custom; } } From 6d53d893a80332ca3e437a166c5c41b8f30ce63a Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 22:46:38 -0500 Subject: [PATCH 55/75] Make Structures.extend static and tweak error messages --- src/util/Structures.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index 8a22ee703..d118ebb0a 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -24,19 +24,19 @@ class Structures { * } * ); */ - extend(name, extender) { + static extend(name, extender) { if (!structures[name]) throw new RangeError(`"${name}" is not a valid extensible structure.`); if (typeof extender !== 'function') { - throw new TypeError('The extender must be a function that returns the extended class.'); + throw new TypeError('The extender must be a function that returns the extended structure class/prototype.'); } const custom = extender(structures[name]); if (typeof custom !== 'function') { - throw new TypeError('The extender function should return the extended class/prototype.'); + throw new TypeError('The extender function must return the extended structure class/prototype.'); } if (Object.getPrototypeOf(custom) !== structures[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 doesn't extend the existing structure class/prototype." ); } From f004e6ccca17eb4ae8efdb32bcaf6164ad15c6b2 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 19 Nov 2017 23:00:56 -0500 Subject: [PATCH 56/75] Reimplement Structures.get --- src/stores/DataStore.js | 2 +- src/util/Structures.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index fc9aa414d..35e326e82 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -9,7 +9,7 @@ class DataStore extends Collection { constructor(client, iterable, holds) { super(); Object.defineProperty(this, 'client', { value: client }); - Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) }); + Object.defineProperty(this, 'holds', { value: Structures.get(holds) }); if (iterable) for (const item of iterable) this.create(item); } diff --git a/src/util/Structures.js b/src/util/Structures.js index d118ebb0a..dd2eca2ba 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -6,6 +6,17 @@ class Structures { throw new Error(`The ${this.constructor.name} class may not be instantiated.`); } + /** + * Retrieves a structure class. + * @param {string|Function} structure Name of the structure or a class/prototype function to use the name of + * @returns {Function} + */ + static get(structure) { + if (typeof structure === 'string') return structures[structure]; + if (typeof structure === 'function') return structures[structure.name]; + throw new TypeError(`Structure to retrieve must be a string or class/prototype function, not ${typeof structure}.`); + } + /** * Extends a structure. * @param {string} name Name of the structure class to extend From 63b0c8d5cc80e2f1b86541c51a62bb1cd7044872 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 00:23:41 -0500 Subject: [PATCH 57/75] Fix circular dependency --- src/stores/DataStore.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 35e326e82..3c58b3899 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -1,5 +1,5 @@ const Collection = require('../util/Collection'); -const Structures = require('../util/Structures'); +let Structures; /** * Manages the creation, retrieval and deletion of a specific data model. @@ -8,6 +8,7 @@ const Structures = require('../util/Structures'); class DataStore extends Collection { constructor(client, iterable, holds) { super(); + if(!Structures) Structures = require('../util/Structures'); Object.defineProperty(this, 'client', { value: client }); Object.defineProperty(this, 'holds', { value: Structures.get(holds) }); if (iterable) for (const item of iterable) this.create(item); From 3728c718670d2e4f6002063eeec094b6f06b55a6 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 00:24:43 -0500 Subject: [PATCH 58/75] Fix missing space --- src/stores/DataStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 3c58b3899..1a196e77d 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -8,7 +8,7 @@ let Structures; class DataStore extends Collection { constructor(client, iterable, holds) { super(); - if(!Structures) Structures = require('../util/Structures'); + if (!Structures) Structures = require('../util/Structures'); Object.defineProperty(this, 'client', { value: client }); Object.defineProperty(this, 'holds', { value: Structures.get(holds) }); if (iterable) for (const item of iterable) this.create(item); From a2a4c3c196e4b0eaaecd7553f70ded60e317f8a4 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 00:26:57 -0500 Subject: [PATCH 59/75] Fix Presence structure --- src/util/Structures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index dd2eca2ba..f83b29ffe 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -64,7 +64,7 @@ const structures = { Guild: require('../structures/Guild'), Message: require('../structures/Message'), MessageReaction: require('../structures/MessageReaction'), - Presence: require('../structures/Presence'), + Presence: require('../structures/Presence').Presence, Role: require('../structures/Role'), User: require('../structures/User'), }; From 1e0ee2f8fa419ec8e456cdef45d445a2b85d8f90 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 01:11:19 -0500 Subject: [PATCH 60/75] Replace Structures.extend with set --- src/stores/DataStore.js | 2 +- src/util/Structures.js | 49 ++++++++++++----------------------------- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 1a196e77d..6f3e74146 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -10,7 +10,7 @@ class DataStore extends Collection { super(); if (!Structures) Structures = require('../util/Structures'); Object.defineProperty(this, 'client', { value: client }); - Object.defineProperty(this, 'holds', { value: Structures.get(holds) }); + Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) }); if (iterable) for (const item of iterable) this.create(item); } diff --git a/src/util/Structures.js b/src/util/Structures.js index f83b29ffe..fd2e44584 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -8,51 +8,30 @@ class Structures { /** * Retrieves a structure class. - * @param {string|Function} structure Name of the structure or a class/prototype function to use the name of + * @param {string} structure Name of the structure to retrieve * @returns {Function} */ static get(structure) { if (typeof structure === 'string') return structures[structure]; - if (typeof structure === 'function') return structures[structure.name]; - throw new TypeError(`Structure to retrieve must be a string or class/prototype function, not ${typeof structure}.`); + throw new TypeError(`"structure" argument must be a string (received ${typeof structure})`); } /** - * Extends a structure. - * @param {string} name Name of the structure class to extend - * @param {Function} extender Function that takes the base class to extend as its only parameter and returns the - * extended class/prototype - * @returns {Function} Extended class/prototype returned from the extender - * @example - * const { Structures } = require('discord.js'); - * - * Structures.extend('Guild', Guild => - * class CoolGuild extends Guild { - * constructor(client, data) { - * super(client, data); - * this.cool = true; - * } - * } - * ); + * Replaces a structure class with an extended one. + * @param {string} structure Name of the structure to replace + * @param {Function} extended Extended structure class/prototype function to replace with */ - static extend(name, extender) { - if (!structures[name]) throw new RangeError(`"${name}" is not a valid extensible structure.`); - if (typeof extender !== 'function') { - throw new TypeError('The extender must be a function that returns the extended structure class/prototype.'); - } - - const custom = extender(structures[name]); - if (typeof custom !== 'function') { - throw new TypeError('The extender function must return the extended structure class/prototype.'); - } - if (Object.getPrototypeOf(custom) !== structures[name]) { - throw new Error( - "The class/prototype returned from the extender function doesn't extend the existing structure class/prototype." + static set(structure, extended) { + if (!structures[structure]) throw new RangeError(`"${structure}" is not a valid extensible structure.`); + if (typeof extended !== 'function') { + throw new TypeError( + `"extended" argument must be a structure class/prototype function (received ${typeof extended})` ); } - - structures[name] = custom; - return custom; + if (Object.getPrototypeOf(extended) !== structures[structure]) { + throw new Error('The class/prototype function provided doesn\'t extend the existing structure class/prototype.'); + } + structures[structure] = extended; } } From cf07b7e3425d78a1abf971f6f8ddd026cfa449c1 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 01:13:36 -0500 Subject: [PATCH 61/75] Re-add docs --- src/util/Structures.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/util/Structures.js b/src/util/Structures.js index fd2e44584..90c6f23b2 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -1,5 +1,7 @@ /** * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}. + * When extending a built-in structure, it is important to both get the class you're extending from here, + * and to set it here afterwards. */ class Structures { constructor() { @@ -20,6 +22,17 @@ class Structures { * Replaces a structure class with an extended one. * @param {string} structure Name of the structure to replace * @param {Function} extended Extended structure class/prototype function to replace with + * @example + * const { Structures } = require('discord.js'); + * + * class CoolGuild extends Structures.get('Guild') { + * constructor(client, data) { + * super(client, data); + * this.cool = true; + * } + * } + * + * Structures.set('Guild', CoolGuild); */ static set(structure, extended) { if (!structures[structure]) throw new RangeError(`"${structure}" is not a valid extensible structure.`); From f3817e328b7691ec87d9f5236524627f59b9107b Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 01:37:19 -0500 Subject: [PATCH 62/75] JK, back to Structures.extend --- src/util/Structures.js | 44 +++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index 90c6f23b2..c81540586 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -1,7 +1,5 @@ /** * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}. - * When extending a built-in structure, it is important to both get the class you're extending from here, - * and to set it here afterwards. */ class Structures { constructor() { @@ -19,32 +17,46 @@ class Structures { } /** - * Replaces a structure class with an extended one. - * @param {string} structure Name of the structure to replace - * @param {Function} extended Extended structure class/prototype function to replace with + * Extends a structure. + * @param {string} structure Name of the structure class to extend + * @param {Function} extender Function that takes the base class to extend as its only parameter and returns the + * extended class/prototype + * @returns {Function} Extended class/prototype returned from the extender * @example * const { Structures } = require('discord.js'); * - * class CoolGuild extends Structures.get('Guild') { - * constructor(client, data) { - * super(client, data); - * this.cool = true; + * Structures.extend('Guild', Guild => { + * class CoolGuild extends Guild { + * constructor(client, data) { + * super(client, data); + * this.cool = true; + * } * } - * } * - * Structures.set('Guild', CoolGuild); + * return CoolGuild; + * }); */ - static set(structure, extended) { + static extend(structure, extender) { if (!structures[structure]) throw new RangeError(`"${structure}" is not a valid extensible structure.`); - if (typeof extended !== 'function') { + if (typeof extender !== 'function') { + const received = `(received ${typeof extender})`; throw new TypeError( - `"extended" argument must be a structure class/prototype function (received ${typeof extended})` + `"extender" argument must be a function that returns the extended structure class/prototype ${received}` ); } - if (Object.getPrototypeOf(extended) !== structures[structure]) { - throw new Error('The class/prototype function provided doesn\'t extend the existing structure class/prototype.'); + + const extended = extender(structures[structure]); + if (typeof extended !== 'function') { + throw new TypeError('The extender function must return the extended structure class/prototype.'); } + if (Object.getPrototypeOf(extended) !== structures[structure]) { + throw new Error( + 'The class/prototype returned from the extender function must extend the existing structure class/prototype.' + ); + } + structures[structure] = extended; + return extended; } } From aaa92c0b05f6b4abadeaa74b7acce626acca030b Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 20 Nov 2017 05:57:45 -0600 Subject: [PATCH 63/75] fix things (#2116) --- src/structures/ClientApplication.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index ccca3f7e0..073fce7ba 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -150,11 +150,12 @@ class ClientApplication extends Base { * @returns {Promise} */ fetchAssets() { - return this.client.api.applications(this.id).assets.get() + const types = Object.keys(ClientApplicationAssetTypes); + return this.client.api.oauth2.applications(this.id).assets.get() .then(assets => assets.map(a => ({ id: a.id, name: a.name, - type: Object.keys(ClientApplicationAssetTypes)[a.type - 1], + type: types[a.type - 1], }))); } @@ -167,7 +168,7 @@ class ClientApplication extends Base { */ createAsset(name, data, type) { return DataResolver.resolveBase64(data).then(b64 => - this.client.api.applications(this.id).assets.post({ data: { + this.client.api.oauth2.applications(this.id).assets.post({ data: { name, data: b64, type: ClientApplicationAssetTypes[type.toUpperCase()], From b5459a96fab428eb828f2c25a45038b301369cc5 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 22:20:32 -0500 Subject: [PATCH 64/75] Move ShardingManager#message event to Shard#message --- src/sharding/Shard.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 044882bec..4cb9a55df 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -270,12 +270,11 @@ class Shard extends EventEmitter { } /** - * Emitted upon recieving a message from a shard. - * @event ShardingManager#message - * @param {Shard} shard Shard that sent the message + * Emitted upon recieving a message from the child process. + * @event Shard#message * @param {*} message Message that was received */ - this.manager.emit('message', this, message); + this.emit('message', message); } /** From c447abad60b4e37c61e0d5386159c7a8121ed208 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 22:26:14 -0500 Subject: [PATCH 65/75] Clear evals and fetches on process death --- src/sharding/Shard.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 4cb9a55df..5bb442919 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -291,6 +291,8 @@ class Shard extends EventEmitter { this.emit('death', this.process); this.ready = false; this.process = null; + this._evals.clear(); + this._fetches.clear(); if (respawn) this.spawn().catch(err => this.emit('error', err)); } } From 527c729aca3dd657eb46b5bdb5415bc98e82222a Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 22:29:46 -0500 Subject: [PATCH 66/75] Possibly fix weird behaviour --- src/sharding/Shard.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 5bb442919..a91203f2c 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -282,17 +282,20 @@ class Shard extends EventEmitter { * @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again * @private */ - _handleExit(respawn = this.manager.respawn) { + _handleExit(respawn) { + if (typeof respawn === 'undefined') respawn = this.manager.respawn; /** * Emitted upon the shard's child process exiting. * @event Shard#death * @param {ChildProcess} process Child process that exited */ this.emit('death', this.process); + this.ready = false; this.process = null; this._evals.clear(); this._fetches.clear(); + if (respawn) this.spawn().catch(err => this.emit('error', err)); } } From c6244ee6e1401aa1146bc8f857f8989fa3d19b44 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 20 Nov 2017 22:37:35 -0500 Subject: [PATCH 67/75] Fix shards not respawning on exit --- src/sharding/Shard.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index a91203f2c..0e44f91a2 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -78,7 +78,7 @@ class Shard extends EventEmitter { * @type {Function} * @private */ - this._exitListener = this._handleExit.bind(this); + this._exitListener = this._handleExit.bind(this, undefined); } /** @@ -282,8 +282,7 @@ class Shard extends EventEmitter { * @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again * @private */ - _handleExit(respawn) { - if (typeof respawn === 'undefined') respawn = this.manager.respawn; + _handleExit(respawn = this.manager.respawn) { /** * Emitted upon the shard's child process exiting. * @event Shard#death From 0d188c0fba3ecd5c247e1d9269b42297c3f83eb8 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 24 Nov 2017 22:33:29 -0500 Subject: [PATCH 68/75] Rename ShardingManager#launch event to shardCreate --- src/sharding/ShardingManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index e8e8f8706..9596ead2e 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -94,10 +94,10 @@ class ShardingManager extends EventEmitter { this.shards.set(id, shard); /** * Emitted upon creating a shard. - * @event ShardingManager#launch + * @event ShardingManager#shardCreate * @param {Shard} shard Shard that was created */ - this.emit('launch', shard); + this.emit('shardCreate', shard); return shard; } From dcf48e2225ce68c004c312fac7125dd85c0f2872 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 24 Nov 2017 21:42:02 -0600 Subject: [PATCH 69/75] Fix inconsistency with Channel Creation: CustomStructures (#2121) * Fix inconsistancy with Channel Creation * Because static get is a function, it thinks we are create a new instance based on that function, rather than the returned class... --- src/structures/Channel.js | 25 +++++++++++++++---------- src/util/Structures.js | 6 +++++- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 33b851f09..04867b118 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -66,32 +66,37 @@ class Channel extends Base { } static create(client, data, guild) { - const DMChannel = require('./DMChannel'); - const GroupDMChannel = require('./GroupDMChannel'); - const TextChannel = require('./TextChannel'); - const VoiceChannel = require('./VoiceChannel'); - const CategoryChannel = require('./CategoryChannel'); - const GuildChannel = require('./GuildChannel'); + const Structures = require('../util/Structures'); let channel; if (data.type === ChannelTypes.DM) { + const DMChannel = Structures.get('DMChannel'); channel = new DMChannel(client, data); } else if (data.type === ChannelTypes.GROUP) { + const GroupDMChannel = Structures.get('GroupDMChannel'); channel = new GroupDMChannel(client, data); } else { guild = guild || client.guilds.get(data.guild_id); if (guild) { switch (data.type) { - case ChannelTypes.TEXT: + case ChannelTypes.TEXT: { + const TextChannel = Structures.get('TextChannel'); channel = new TextChannel(guild, data); break; - case ChannelTypes.VOICE: + } + case ChannelTypes.VOICE: { + const VoiceChannel = Structures.get('VoiceChannel'); channel = new VoiceChannel(guild, data); break; - case ChannelTypes.CATEGORY: + } + case ChannelTypes.CATEGORY: { + const CategoryChannel = Structures.get('CategoryChannel'); channel = new CategoryChannel(guild, data); break; - default: + } + default: { + const GuildChannel = Structures.get('GuildChannel'); channel = new GuildChannel(guild, data); + } } guild.channels.set(channel.id, channel); } diff --git a/src/util/Structures.js b/src/util/Structures.js index c81540586..a1cb7e156 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -61,8 +61,12 @@ class Structures { } const structures = { - Channel: require('../structures/Channel'), Emoji: require('../structures/Emoji'), + DMChannel: require('../structures/DMChannel'), + GroupDMChannel: require('../structures/GroupDMChannel'), + TextChannel: require('../structures/TextChannel'), + VoiceChannel: require('../structures/VoiceChannel'), + CategoryChannel: require('../structures/CategoryChannel'), GuildChannel: require('../structures/GuildChannel'), GuildMember: require('../structures/GuildMember'), Guild: require('../structures/Guild'), From 7cd0a9525d49a520c1fd599a961fb22816d781d8 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 24 Nov 2017 23:05:00 -0500 Subject: [PATCH 70/75] Fix ClientUser not extending custom User --- src/client/websocket/packets/handlers/Ready.js | 3 ++- src/index.js | 7 ++++++- src/structures/ClientUser.js | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index 4fc5363cf..b1a833d5f 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -1,6 +1,6 @@ const AbstractHandler = require('./AbstractHandler'); const { Events } = require('../../../../util/Constants'); -const ClientUser = require('../../../../structures/ClientUser'); +let ClientUser; class ReadyHandler extends AbstractHandler { handle(packet) { @@ -12,6 +12,7 @@ class ReadyHandler extends AbstractHandler { data.user.user_settings = data.user_settings; data.user.user_guild_settings = data.user_guild_settings; + if (!ClientUser) ClientUser = require('../../../../structures/ClientUser'); const clientUser = new ClientUser(client, data.user); client.user = clientUser; client.readyAt = new Date(); diff --git a/src/index.js b/src/index.js index 90cf5b4c0..1108f23f6 100644 --- a/src/index.js +++ b/src/index.js @@ -47,7 +47,10 @@ module.exports = { CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), ClientApplication: require('./structures/ClientApplication'), - ClientUser: require('./structures/ClientUser'), + get ClientUser() { + // This is a getter so that it properly extends any custom User class + return require('./structures/ClientUser'); + }, ClientUserChannelOverride: require('./structures/ClientUserChannelOverride'), ClientUserGuildSettings: require('./structures/ClientUserGuildSettings'), ClientUserSettings: require('./structures/ClientUserSettings'), @@ -81,3 +84,5 @@ module.exports = { WebSocket: require('./WebSocket'), }; + +Object. diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index ef6433924..b0b3452d4 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -1,4 +1,4 @@ -const User = require('./User'); +const Structures = require('../util/Structures'); const Collection = require('../util/Collection'); const ClientUserSettings = require('./ClientUserSettings'); const ClientUserGuildSettings = require('./ClientUserGuildSettings'); @@ -11,7 +11,7 @@ const Guild = require('./Guild'); * Represents the logged in client's Discord user. * @extends {User} */ -class ClientUser extends User { +class ClientUser extends Structures.get('User') { _patch(data) { super._patch(data); From be02875f05246f73ecc2b0cb58409426b2309dbb Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 24 Nov 2017 23:16:21 -0500 Subject: [PATCH 71/75] I don't even --- src/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/index.js b/src/index.js index 1108f23f6..244c6a5ea 100644 --- a/src/index.js +++ b/src/index.js @@ -84,5 +84,3 @@ module.exports = { WebSocket: require('./WebSocket'), }; - -Object. From 2f84d950775c2d50b9f254f61885533440b9deaf Mon Sep 17 00:00:00 2001 From: 1Computer1 <1Computer1@users.noreply.github.com> Date: Sun, 26 Nov 2017 04:58:17 -0500 Subject: [PATCH 72/75] Add more options to MessageMentions#has (#2131) * Add more options to MessageMentions#has * Rename ignoreSelf to ignoreDirect --- src/structures/MessageMentions.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 1b49c5ea8..ee4a0e69d 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -117,19 +117,27 @@ class MessageMentions { } /** - * Check if a user is mentioned. + * Checks if a user, guild member, role, or channel is mentioned. * Takes into account user mentions, role mentions, and @everyone/@here mentions. * @param {UserResolvable|GuildMember|Role|GuildChannel} data User/GuildMember/Role/Channel to check - * @param {boolean} [strict=true] If role mentions and everyone/here mentions should be included + * @param {Object} [options] Options + * @param {boolean} [options.ignoreDirect=false] - Whether to ignore direct mentions to the item + * @param {boolean} [options.ignoreRoles=false] - Whether to ignore role mentions to a guild member + * @param {boolean} [options.ignoreEveryone=false] - Whether to ignore everyone/here mentions * @returns {boolean} */ - has(data, strict = true) { - if (strict && this.everyone) return true; - if (strict && data instanceof GuildMember) { + has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) { + if (!ignoreEveryone && this.everyone) return true; + if (!ignoreRoles && data instanceof GuildMember) { for (const role of this.roles.values()) if (data.roles.has(role.id)) return true; } - const id = data.id || data; - return this.users.has(id) || this.channels.has(id) || this.roles.has(id); + + if (!ignoreDirect) { + const id = data.id || data; + return this.users.has(id) || this.channels.has(id) || this.roles.has(id); + } + + return false; } } From efd1c4c51625fd3c16ce3535f4bd340551dbd8ae Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Sun, 26 Nov 2017 11:59:21 +0200 Subject: [PATCH 73/75] docs: Remove leftover docstring from 11.2 (#2115) * Fix leftover docstring from 11.2 * Here too * Update Guild.js * Update GuildMember.js --- src/structures/Guild.js | 3 +-- src/structures/GuildMember.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1eff773a2..fb6a6eed3 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -815,8 +815,7 @@ class Guild extends Base { /** * Bans a user from the guild. * @param {UserResolvable} user The user to ban - * @param {Object} [options] Ban options. If a number, the number of days to delete messages for, if a - * string, the ban reason. Supplying an object allows you to do both. + * @param {Object} [options] Options for the ban * @param {number} [options.days=0] Number of days of messages to delete * @param {string} [options.reason] Reason for banning * @returns {Promise} Result object will be resolved as specifically as possible. diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 05266e1b8..21f84ec98 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -521,8 +521,7 @@ class GuildMember extends Base { /** * Bans this guild member. - * @param {Object} [options] Ban options. If a number, the number of days to delete messages for, if a - * string, the ban reason. Supplying an object allows you to do both. + * @param {Object} [options] Options for the ban * @param {number} [options.days=0] Number of days of messages to delete * @param {string} [options.reason] Reason for banning * @returns {Promise} From 134ef7a61b6d0c9c8ec23fc13a0a7e1b00295b5e Mon Sep 17 00:00:00 2001 From: Yukine Date: Thu, 30 Nov 2017 05:36:03 +0100 Subject: [PATCH 74/75] added a new Typedef for Bans you can get from .fetchBans() and fixed a little typo in the fetchAuditLogs() method (#2108) * added a new Typedef for Bans you can get from .fetchBans() due recent change aswell as fixed a little typo in the fetchAuditLogs() mehtod so .type has no the correct type defined * little change due request * fixed indentation * Update Guild.js * Update Guild.js --- src/structures/Guild.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index fb6a6eed3..307e5eda2 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -441,10 +441,16 @@ class Guild extends Base { return this.members.resolve(user); } + /** + * An object containing information about a guild member's ban. + * @typedef {Object} BanInfo + * @property {User} user User that was banned + * @property {?string} reason Reason the user was banned + */ + /** * Fetches a collection of banned users in this guild. - * The returned collection contains user objects keyed under `user` and reasons keyed under `reason`. - * @returns {Promise>} + * @returns {Promise>} */ fetchBans() { return this.client.api.guilds(this.id).bans.get().then(bans => @@ -506,7 +512,7 @@ class Guild extends Base { * @param {Snowflake|GuildAuditLogsEntry} [options.after] Limit to entries from after specified entry * @param {number} [options.limit] Limit number of entries * @param {UserResolvable} [options.user] Only show entries involving this user - * @param {ActionType|number} [options.type] Only show entries involving this action type + * @param {AuditLogAction|number} [options.type] Only show entries involving this action type * @returns {Promise} */ fetchAuditLogs(options = {}) { From 3e3674b1af200eb3947018f210bdf49b0e200b1b Mon Sep 17 00:00:00 2001 From: bdistin Date: Thu, 30 Nov 2017 13:39:58 -0600 Subject: [PATCH 75/75] Fix channels.resolve (#2137) --- src/stores/DataStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 6f3e74146..c4256dfe2 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -10,7 +10,7 @@ class DataStore extends Collection { super(); if (!Structures) Structures = require('../util/Structures'); Object.defineProperty(this, 'client', { value: client }); - Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) }); + Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) || holds }); if (iterable) for (const item of iterable) this.create(item); }