From 94a4a068b9400000a3b057a1ae781cb9eb585bc4 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 29 Oct 2017 14:53:34 +0100 Subject: [PATCH 01/61] 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 02/61] 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 03/61] 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 04/61] 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 05/61] 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 06/61] 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 07/61] 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 08/61] 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 09/61] 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 10/61] 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 11/61] 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 12/61] 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 13/61] 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 14/61] 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 15/61] 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 16/61] 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 17/61] 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 18/61] 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 19/61] 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 20/61] 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 21/61] 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 22/61] 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 23/61] 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 24/61] 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 25/61] 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 26/61] 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 27/61] 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 28/61] 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 29/61] 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 30/61] 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 31/61] 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 32/61] 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 33/61] 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 34/61] 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 35/61] 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 36/61] 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 37/61] 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 38/61] 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 39/61] 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 40/61] 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 41/61] 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 42/61] 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 43/61] 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 44/61] 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 45/61] 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 46/61] 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 47/61] 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 48/61] 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 49/61] 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 50/61] 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 51/61] 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 52/61] 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 53/61] 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 54/61] 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 55/61] 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 56/61] 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 57/61] 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 58/61] 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 59/61] 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 60/61] 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); } From 20689a51a1c37ff7b7edaff357621640d68bac67 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 2 Dec 2017 17:02:17 -0500 Subject: [PATCH 61/61] Add exports for new util functions --- src/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.js b/src/index.js index 244c6a5ea..42de34564 100644 --- a/src/index.js +++ b/src/index.js @@ -37,8 +37,11 @@ module.exports = { UserStore: require('./stores/UserStore'), // Shortcuts to Util methods + discordSort: Util.discordSort, escapeMarkdown: Util.escapeMarkdown, fetchRecommendedShards: Util.fetchRecommendedShards, + resolveColor: Util.resolveColor, + resolveString: Util.resolveString, splitMessage: Util.splitMessage, // Structures