From 9d36be58ef93172d927bb7302c7efa9b5731292c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 7 Jan 2017 21:19:37 +0000 Subject: [PATCH 01/16] Fix typo in readme (#1070) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eae457c31..0408d3b9f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Using opusscript is only recommended for development environments where node-opu For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [uws](https://www.npmjs.com/package/uws) for much a much faster WebSocket connection (`npm install uws --save`) +- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws --save`) - [erlpack](https://github.com/hammerandchisel/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install hammerandchisel/erlpack --save`) ## Example Usage From 4717c34ff6f62f5fdb0c51639fc392d09924466d Mon Sep 17 00:00:00 2001 From: Zack Campbell Date: Sat, 7 Jan 2017 19:00:36 -0600 Subject: [PATCH 02/16] Update typings submodule (#1069) Added search stuff --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index c8b3f8b89..14c4b674c 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit c8b3f8b8931d1318f1143ca26574ae1f9b4c5aa2 +Subproject commit 14c4b674cfab537277e80ce57b8b68717e4055d1 From 2a668ac9973324a67ad95ee823ba6df2cfa3473c Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 8 Jan 2017 04:02:44 -0500 Subject: [PATCH 03/16] This expands the consistency of .find and .exists to include the id property (#1072) * provide a more consistent api for .find * remove random warning * make code more concise --- src/util/Collection.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index bafe710b6..7cf63ec06 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -135,8 +135,6 @@ class Collection extends Map { * Searches for a single item where its specified property's value is identical to the given value * (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to * [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find). - * Do not use this to obtain an item by its ID. Instead, use `collection.get(id)`. See - * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details. * @param {string|Function} propOrFn The property to test against, or the function to test with * @param {*} [value] The expected value - only applicable and required if using a property for the first argument * @returns {*} @@ -148,7 +146,7 @@ class Collection extends Map { find(propOrFn, value) { if (typeof propOrFn === 'string') { if (typeof value === 'undefined') throw new Error('Value must be specified.'); - if (propOrFn === 'id') throw new RangeError('Don\'t use .find() with IDs. Instead, use .get(id).'); + if (propOrFn === 'id') return this.get(value); for (const item of this.values()) { if (item[propOrFn] === value) return item; } @@ -197,8 +195,6 @@ class Collection extends Map { /** * Searches for the existence of a single item where its specified property's value is identical to the given value * (`item[prop] === value`). - * Do not use this to check for an item by its ID. Instead, use `collection.has(id)`. See - * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) for details. * @param {string} prop The property to test against * @param {*} value The expected value * @returns {boolean} @@ -208,7 +204,7 @@ class Collection extends Map { * } */ exists(prop, value) { - if (prop === 'id') throw new RangeError('Don\'t use .exists() with IDs. Instead, use .has(id).'); + if (prop === 'id') return this.has(value); return Boolean(this.find(prop, value)); } From fde3a976aace2456a1f40c2a12c975f0232c2c19 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 8 Jan 2017 10:17:10 +0000 Subject: [PATCH 04/16] Revert "This expands the consistency of .find and .exists to include the id property" (#1074) --- src/util/Collection.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index 7cf63ec06..bafe710b6 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -135,6 +135,8 @@ class Collection extends Map { * Searches for a single item where its specified property's value is identical to the given value * (`item[prop] === value`), or the given function returns a truthy value. In the latter case, this is identical to * [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find). + * Do not use this to obtain an item by its ID. Instead, use `collection.get(id)`. See + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details. * @param {string|Function} propOrFn The property to test against, or the function to test with * @param {*} [value] The expected value - only applicable and required if using a property for the first argument * @returns {*} @@ -146,7 +148,7 @@ class Collection extends Map { find(propOrFn, value) { if (typeof propOrFn === 'string') { if (typeof value === 'undefined') throw new Error('Value must be specified.'); - if (propOrFn === 'id') return this.get(value); + if (propOrFn === 'id') throw new RangeError('Don\'t use .find() with IDs. Instead, use .get(id).'); for (const item of this.values()) { if (item[propOrFn] === value) return item; } @@ -195,6 +197,8 @@ class Collection extends Map { /** * Searches for the existence of a single item where its specified property's value is identical to the given value * (`item[prop] === value`). + * Do not use this to check for an item by its ID. Instead, use `collection.has(id)`. See + * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has) for details. * @param {string} prop The property to test against * @param {*} value The expected value * @returns {boolean} @@ -204,7 +208,7 @@ class Collection extends Map { * } */ exists(prop, value) { - if (prop === 'id') return this.has(value); + if (prop === 'id') throw new RangeError('Don\'t use .exists() with IDs. Instead, use .has(id).'); return Boolean(this.find(prop, value)); } From b68283e57ae68c3060ad249ffa534e27c98e0130 Mon Sep 17 00:00:00 2001 From: Zack Campbell Date: Sun, 8 Jan 2017 05:59:45 -0600 Subject: [PATCH 05/16] Make _array & _keyArray non-enumerable (#1075) Because Map has no enumerable properties --- src/util/Collection.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index bafe710b6..bca75f5b8 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -9,17 +9,19 @@ class Collection extends Map { /** * Cached array for the `array()` method - will be reset to `null` whenever `set()` or `delete()` are called. + * @name Collection#_array * @type {?Array} * @private */ - this._array = null; + Object.defineProperty(this, '_array', { value: null, writable: true, configurable: true }); /** * Cached array for the `keyArray()` method - will be reset to `null` whenever `set()` or `delete()` are called. + * @name Collection#_keyArray * @type {?Array} * @private */ - this._keyArray = null; + Object.defineProperty(this, '_keyArray', { value: null, writable: true, configurable: true }); } set(key, val) { From 4a7284b86e551ec95b2eec17993ef7e35702312f Mon Sep 17 00:00:00 2001 From: ooookai Date: Sun, 8 Jan 2017 13:34:06 -0600 Subject: [PATCH 06/16] move function getRoute(url) into class APIRequest (#1065) this.route = getRoute(this.url); >>> this.route = this.getRoute(this.url); --- src/client/rest/APIRequest.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index 36c2d8fed..5e83c143b 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -1,16 +1,6 @@ const request = require('superagent'); const Constants = require('../../util/Constants'); -function getRoute(url) { - let route = url.split('?')[0]; - if (route.includes('/channels/') || route.includes('/guilds/')) { - const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/'); - const majorID = route.substring(startInd).split('/')[2]; - route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID); - } - return route; -} - class APIRequest { constructor(rest, method, url, auth, data, file) { this.rest = rest; @@ -19,7 +9,17 @@ class APIRequest { this.auth = auth; this.data = data; this.file = file; - this.route = getRoute(this.url); + this.route = this.getRoute(this.url); + } + + getRoute(url) { + let route = url.split('?')[0]; + if (route.includes('/channels/') || route.includes('/guilds/')) { + const startInd = route.includes('/channels/') ? route.indexOf('/channels/') : route.indexOf('/guilds/'); + const majorID = route.substring(startInd).split('/')[2]; + route = route.replace(/(\d{8,})/g, ':id').replace(':id', majorID); + } + return route; } getAuth() { From 5e7ae847de43d654f263ce91daccc5ec756abd49 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 8 Jan 2017 13:49:56 -0600 Subject: [PATCH 07/16] switch to proper querystring parser because why not (#1077) --- src/client/rest/RESTMethods.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 5ecae4610..3773a964a 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -1,3 +1,4 @@ +const querystring = require('querystring'); const Constants = require('../../util/Constants'); const Collection = require('../../util/Collection'); const splitMessage = require('../../util/SplitMessage'); @@ -123,11 +124,7 @@ class RESTMethods { search(target, options) { options = transformSearchOptions(options, this.client); - const queryString = Object.keys(options) - .filter(k => options[k]) - .map(k => [k, options[k]]) - .map(x => x.join('=')) - .join('&'); + const queryString = querystring.stringify(options); let type; if (target instanceof Channel) { From 47707d245dfea4dfd26fd56c258ce571d8e02afc Mon Sep 17 00:00:00 2001 From: Enchanted13 Date: Mon, 9 Jan 2017 10:35:11 -0500 Subject: [PATCH 08/16] Changed return type of Guild.defaultChannel (#1079) The default channel for a Guild is always the first TextChannel in the Guild, it can't be a VoiceChannel. --- src/structures/Guild.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index faa5ce55a..c9de61847 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -278,8 +278,8 @@ class Guild { } /** - * The `#general` GuildChannel of the server. - * @type {GuildChannel} + * The `#general` TextChannel of the server. + * @type {TextChannel} * @readonly */ get defaultChannel() { From c37cd3fd913773cfb53d784d12ddffad5fbc727f Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 10 Jan 2017 15:52:12 -0600 Subject: [PATCH 09/16] clean up webhooks and fix sending messages with webhooks (#1078) * clean up webhooks and fix sending messages with webhooks * whoops * fix up options * Update Webhook.js * Update Webhook.js * Update Webhook.js --- src/client/rest/RESTMethods.js | 8 +- src/structures/Webhook.js | 151 ++++++++++++++++++--------------- 2 files changed, 87 insertions(+), 72 deletions(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 3773a964a..eab95e338 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -562,7 +562,8 @@ class RESTMethods { return this.rest.makeRequest('delete', Constants.Endpoints.webhook(webhook.id, webhook.token), false); } - sendWebhookMessage(webhook, content, { avatarURL, tts, disableEveryone, embeds } = {}, file = null) { + sendWebhookMessage(webhook, content, { avatarURL, tts, disableEveryone, embeds, username } = {}, file = null) { + username = username || webhook.name; if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); if (content) { if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { @@ -570,13 +571,12 @@ class RESTMethods { } } return this.rest.makeRequest('post', `${Constants.Endpoints.webhook(webhook.id, webhook.token)}?wait=true`, false, { - username: webhook.name, + username, avatar_url: avatarURL, content, tts, - file, embeds, - }); + }, file); } sendSlackWebhookMessage(webhook, body) { diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 153d8f3a5..89aa89d34 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,5 +1,4 @@ const path = require('path'); -const escapeMarkdown = require('../util/EscapeMarkdown'); /** * Represents a webhook @@ -59,21 +58,73 @@ class Webhook { */ this.channelID = data.channel_id; - /** - * The owner of the webhook - * @type {User} - */ - if (data.user) this.owner = data.user; + if (data.user) { + /** + * The owner of the webhook + * @type {?User|Object} + */ + this.owner = this.client.users ? this.client.users.get(data.user.id) : data.user; + } else { + this.owner = null; + } } /** - * Options that can be passed into sendMessage, sendTTSMessage, sendFile, sendCode + * Options that can be passed into send, sendMessage, sendFile, sendEmbed, and sendCode * @typedef {Object} WebhookMessageOptions + * @property {string} [username=this.name] Username override for the message + * @property {string} [avatarURL] Avatar URL override for the message * @property {boolean} [tts=false] Whether or not the message should be spoken aloud - * @property {boolean} [disableEveryone=this.options.disableEveryone] Whether or not @everyone and @here + * @property {string} [nonce=''] The nonce for the message + * @property {Object} [embed] An embed for the message + * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) + * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here * should be replaced with plain-text + * @property {FileOptions|string} [file] A file to send with the message + * @property {string|boolean} [code] Language for optional codeblock formatting to apply + * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if + * it exceeds the character limit. If an object is provided, these are the options for splitting the message. */ + /** + * Send a message with this webhook + * @param {StringResolvable} content The content to send. + * @param {WebhookMessageOptions} [options={}] The options to provide. + * @returns {Promise} + * @example + * // send a message + * webhook.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + */ + send(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + if (options.file) { + if (typeof options.file === 'string') options.file = { attachment: options.file }; + if (!options.file.name) { + if (typeof options.file.attachment === 'string') { + options.file.name = path.basename(options.file.attachment); + } else if (options.file.attachment && options.file.attachment.path) { + options.file.name = path.basename(options.file.attachment.path); + } else { + options.file.name = 'file.jpg'; + } + } + return this.client.resolver.resolveBuffer(options.file.attachment).then(file => + this.client.rest.methods.sendWebhookMessage(this, content, options, { + file, + name: options.file.name, + }) + ); + } + return this.client.rest.methods.sendWebhookMessage(this, content, options); + } + /** * Send a message with this webhook * @param {StringResolvable} content The content to send. @@ -86,7 +137,30 @@ class Webhook { * .catch(console.error); */ sendMessage(content, options = {}) { - return this.client.rest.methods.sendWebhookMessage(this, content, options); + return this.send(content, options); + } + + /** + * Send a file with this webhook + * @param {BufferResolvable} attachment The file to send + * @param {string} [name='file.jpg'] The name and extension of the file + * @param {StringResolvable} [content] Text message to send with the attachment + * @param {WebhookMessageOptions} [options] The options to provide + * @returns {Promise} + */ + sendFile(attachment, name, content, options = {}) { + return this.send(content, Object.assign(options, { file: { attachment, name } })); + } + + /** + * Send a code block with this webhook + * @param {string} lang Language for the code block + * @param {StringResolvable} content Content of the code block + * @param {WebhookMessageOptions} options The options to provide + * @returns {Promise} + */ + sendCode(lang, content, options = {}) { + return this.send(content, Object.assign(options, { code: lang })); } /** @@ -110,65 +184,6 @@ class Webhook { return this.client.rest.methods.sendSlackWebhookMessage(this, body); } - /** - * Send a text-to-speech message with this webhook - * @param {StringResolvable} content The content to send - * @param {WebhookMessageOptions} [options={}] The options to provide - * @returns {Promise} - * @example - * // send a TTS message - * webhook.sendTTSMessage('hello!') - * .then(message => console.log(`Sent tts message: ${message.content}`)) - * .catch(console.error); - */ - sendTTSMessage(content, options = {}) { - Object.assign(options, { tts: true }); - return this.client.rest.methods.sendWebhookMessage(this, content, options); - } - - /** - * Send a file with this webhook - * @param {BufferResolvable} attachment The file to send - * @param {string} [fileName="file.jpg"] The name and extension of the file - * @param {StringResolvable} [content] Text message to send with the attachment - * @param {WebhookMessageOptions} [options] The options to provide - * @returns {Promise} - */ - sendFile(attachment, fileName, content, options = {}) { - if (!fileName) { - if (typeof attachment === 'string') { - fileName = path.basename(attachment); - } else if (attachment && attachment.path) { - fileName = path.basename(attachment.path); - } else { - fileName = 'file.jpg'; - } - } - return this.client.resolver.resolveBuffer(attachment).then(file => - this.client.rest.methods.sendWebhookMessage(this, content, options, { - file, - name: fileName, - }) - ); - } - - /** - * Send a code block with this webhook - * @param {string} lang Language for the code block - * @param {StringResolvable} content Content of the code block - * @param {WebhookMessageOptions} options The options to provide - * @returns {Promise} - */ - sendCode(lang, content, options = {}) { - if (options.split) { - if (typeof options.split !== 'object') options.split = {}; - if (!options.split.prepend) options.split.prepend = `\`\`\`${lang || ''}\n`; - if (!options.split.append) options.split.append = '\n```'; - } - content = escapeMarkdown(this.client.resolver.resolveString(content), true); - return this.sendMessage(`\`\`\`${lang || ''}\n${content}\n\`\`\``, options); - } - /** * Edit the webhook. * @param {string} name The new name for the Webhook From 8b0e5aad3837ecc704eeca83f6466972e05637c5 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 10 Jan 2017 19:22:03 -0500 Subject: [PATCH 10/16] Fix sendEmbed with array content --- src/structures/interface/TextBasedChannel.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js index 5e06cb543..7b20dd4f5 100644 --- a/src/structures/interface/TextBasedChannel.js +++ b/src/structures/interface/TextBasedChannel.js @@ -2,6 +2,7 @@ const path = require('path'); const Message = require('../Message'); const MessageCollector = require('../MessageCollector'); const Collection = require('../../util/Collection'); +let GuildMember; /** * Interface for classes that have text-channel-like features @@ -23,7 +24,7 @@ class TextBasedChannel { } /** - * Options that can be passed into send, sendMessage, sendFile, sendEmbed, sendCode, and Message#reply + * Options provided when sending or editing a message * @typedef {Object} MessageOptions * @property {boolean} [tts=false] Whether or not the message should be spoken aloud * @property {string} [nonce=''] The nonce for the message @@ -35,6 +36,7 @@ class TextBasedChannel { * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if * it exceeds the character limit. If an object is provided, these are the options for splitting the message. + * @property {UserResolvable} [reply] User to reply to (prefixes the message with a mention, except in DMs) */ /** @@ -70,6 +72,7 @@ class TextBasedChannel { } else if (!options) { options = {}; } + if (options.file) { if (typeof options.file === 'string') options.file = { attachment: options.file }; if (!options.file.name) { @@ -81,6 +84,7 @@ class TextBasedChannel { options.file.name = 'file.jpg'; } } + return this.client.resolver.resolveBuffer(options.file.attachment).then(file => this.client.rest.methods.sendMessage(this, content, options, { file, @@ -88,12 +92,13 @@ class TextBasedChannel { }) ); } + return this.client.rest.methods.sendMessage(this, content, options); } /** * Send a message to this channel - * @param {StringResolvable} content Text for the message + * @param {StringResolvable} [content] Text for the message * @param {MessageOptions} [options={}] Options for the message * @returns {Promise} * @example @@ -114,7 +119,7 @@ class TextBasedChannel { * @returns {Promise} */ sendEmbed(embed, content, options) { - if (!options && typeof content === 'object') { + if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; content = ''; } else if (!options) { From 5caa7df1d8e2f5c2986f1fd7ffc40056604e7b1c Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 10 Jan 2017 19:25:05 -0500 Subject: [PATCH 11/16] Add centralised reply option to message options --- src/client/rest/RESTMethods.js | 36 +++++++++++++++++++++++++++++----- src/structures/Message.js | 15 +++++++++----- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index eab95e338..e0e91366c 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -46,21 +46,37 @@ class RESTMethods { return this.rest.makeRequest('get', Constants.Endpoints.botGateway, true); } - sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code } = {}, file = null) { - return new Promise((resolve, reject) => { + sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code, reply } = {}, file = null) { + return new Promise((resolve, reject) => { // eslint-disable-line complexity if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); if (content) { + if (split && typeof split !== 'object') split = {}; + + // Wrap everything in a code block if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { content = escapeMarkdown(this.client.resolver.resolveString(content), true); content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; } + // Add zero-width spaces to @everyone/@here if (disableEveryone || (typeof disableEveryone === 'undefined' && this.client.options.disableEveryone)) { content = content.replace(/@(everyone|here)/g, '@\u200b$1'); } - if (split) content = splitMessage(content, typeof split === 'object' ? split : {}); + // Add the reply prefix + if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + content = `${mention}${content ? `, ${content}` : ''}`; + if (split) split.prepend = `${mention}, ${split.prepend || ''}`; + } + + // Split the content + if (split) content = splitMessage(content, split); + } else if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + content = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; } const send = chan => { @@ -89,12 +105,22 @@ class RESTMethods { }); } - updateMessage(message, content, { embed, code } = {}) { - content = this.client.resolver.resolveString(content); + updateMessage(message, content, { embed, code, reply } = {}) { + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + + // Wrap everything in a code block if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { content = escapeMarkdown(this.client.resolver.resolveString(content), true); content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; } + + // Add the reply prefix + if (reply && message.channel.type !== 'dm') { + const id = this.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + content = `${mention}${content ? `, ${content}` : ''}`; + } + return this.rest.makeRequest('patch', Constants.Endpoints.channelMessage(message.channel.id, message.id), true, { content, embed, }).then(data => this.client.actions.MessageUpdate.handle(data).updated); diff --git a/src/structures/Message.js b/src/structures/Message.js index 138169389..252ca98d6 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -470,8 +470,8 @@ class Message { /** * Reply to the message - * @param {StringResolvable} content The content for the message - * @param {MessageOptions} [options = {}] The options to provide + * @param {StringResolvable} [content] The content for the message + * @param {MessageOptions} [options] The options to provide * @returns {Promise} * @example * // reply to a message @@ -479,9 +479,14 @@ class Message { * .then(msg => console.log(`Sent a reply to ${msg.author}`)) * .catch(console.error); */ - reply(content, options = {}) { - content = `${this.guild || this.channel.type === 'group' ? `${this.author}, ` : ''}${content}`; - return this.channel.send(content, options); + reply(content, options) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } else if (!options) { + options = {}; + } + return this.channel.send(content, Object.assign(options, { reply: this.member || this.author })); } /** From b2822c584aa862d7680f7d97b7820f2d9511c7c7 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Tue, 10 Jan 2017 19:43:25 -0500 Subject: [PATCH 12/16] Minor doc updates --- src/structures/RichEmbed.js | 2 +- src/structures/interface/TextBasedChannel.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/RichEmbed.js b/src/structures/RichEmbed.js index fbd9383d3..268fa31d8 100644 --- a/src/structures/RichEmbed.js +++ b/src/structures/RichEmbed.js @@ -1,5 +1,5 @@ /** - * A rich embed to be sent with a message + * A rich embed to be sent with a message with a fluent interface for creation * @param {Object} [data] Data to set in the rich embed */ class RichEmbed { diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js index 7b20dd4f5..79ea4e203 100644 --- a/src/structures/interface/TextBasedChannel.js +++ b/src/structures/interface/TextBasedChannel.js @@ -28,7 +28,7 @@ class TextBasedChannel { * @typedef {Object} MessageOptions * @property {boolean} [tts=false] Whether or not the message should be spoken aloud * @property {string} [nonce=''] The nonce for the message - * @property {Object} [embed] An embed for the message + * @property {RichEmbed|Object} [embed] An embed for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here * should be replaced with plain-text From a3091f5262e5e7c44b29cededdb240575b6500f2 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Wed, 11 Jan 2017 16:59:10 -0600 Subject: [PATCH 13/16] Handle 4011 ws event code (#1083) * 4011 * Update WebSocketManager.js * smh gawdl3y --- src/client/ClientManager.js | 1 + src/client/websocket/WebSocketManager.js | 3 +-- src/structures/interface/TextBasedChannel.js | 1 - src/util/Constants.js | 1 + 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 0cfbbfdf4..57699f158 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -35,6 +35,7 @@ class ClientManager { this.client.ws.once('close', event => { if (event.code === 4004) reject(new Error(Constants.Errors.BAD_LOGIN)); if (event.code === 4010) reject(new Error(Constants.Errors.INVALID_SHARD)); + if (event.code === 4011) reject(new Error(Constants.Errors.SHARDING_REQUIRED)); }); this.client.once(Constants.Events.READY, () => { resolve(token); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index b67af5993..89136d276 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -249,8 +249,7 @@ class WebSocketManager extends EventEmitter { * @param {CloseEvent} event The WebSocket close event */ if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT, event); - if (event.code === 4004) return; - if (event.code === 4010) return; + if ([4004, 4010, 4011].includes(event.code)) return; if (!this.reconnecting && event.code !== 1000) this.tryReconnect(); } diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js index 79ea4e203..74659afac 100644 --- a/src/structures/interface/TextBasedChannel.js +++ b/src/structures/interface/TextBasedChannel.js @@ -2,7 +2,6 @@ const path = require('path'); const Message = require('../Message'); const MessageCollector = require('../MessageCollector'); const Collection = require('../../util/Collection'); -let GuildMember; /** * Interface for classes that have text-channel-like features diff --git a/src/util/Constants.js b/src/util/Constants.js index dc80da5aa..bb02b3705 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -72,6 +72,7 @@ exports.Errors = { INVALID_RATE_LIMIT_METHOD: 'Unknown rate limiting method.', BAD_LOGIN: 'Incorrect login details were provided.', INVALID_SHARD: 'Invalid shard settings were provided.', + SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.', INVALID_TOKEN: 'An invalid token was provided.', }; From 0d4eab8d241e770cbea065688ccf85aa93fe07d1 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 12 Jan 2017 12:43:22 -0600 Subject: [PATCH 14/16] add color resolvable, and color constants from the client (#1080) * add color resolvable, and color constants from the client * fix up docs * Update ClientDataResolver.js * add easter eggs * Update ClientDataResolver.js * Update RESTMethods.js --- src/client/ClientDataResolver.js | 115 +++++++++++++++++++++++-------- src/client/rest/RESTMethods.js | 5 +- src/structures/RichEmbed.js | 19 ++--- src/structures/Role.js | 2 +- src/util/Constants.js | 28 ++++++++ 5 files changed, 122 insertions(+), 47 deletions(-) diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index c6bf38717..73d4961c2 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -163,33 +163,33 @@ class ClientDataResolver { * Possible strings: * ```js * [ - * "CREATE_INSTANT_INVITE", - * "KICK_MEMBERS", - * "BAN_MEMBERS", - * "ADMINISTRATOR", - * "MANAGE_CHANNELS", - * "MANAGE_GUILD", - * "ADD_REACTIONS", // add reactions to messages - * "READ_MESSAGES", - * "SEND_MESSAGES", - * "SEND_TTS_MESSAGES", - * "MANAGE_MESSAGES", - * "EMBED_LINKS", - * "ATTACH_FILES", - * "READ_MESSAGE_HISTORY", - * "MENTION_EVERYONE", - * "EXTERNAL_EMOJIS", // use external emojis - * "CONNECT", // connect to voice - * "SPEAK", // speak on voice - * "MUTE_MEMBERS", // globally mute members on voice - * "DEAFEN_MEMBERS", // globally deafen members on voice - * "MOVE_MEMBERS", // move member's voice channels - * "USE_VAD", // use voice activity detection - * "CHANGE_NICKNAME", - * "MANAGE_NICKNAMES", // change nicknames of others - * "MANAGE_ROLES_OR_PERMISSIONS", - * "MANAGE_WEBHOOKS", - * "MANAGE_EMOJIS" + * 'CREATE_INSTANT_INVITE', + * 'KICK_MEMBERS', + * 'BAN_MEMBERS', + * 'ADMINISTRATOR', + * 'MANAGE_CHANNELS', + * 'MANAGE_GUILD', + * 'ADD_REACTIONS', // add reactions to messages + * 'READ_MESSAGES', + * 'SEND_MESSAGES', + * 'SEND_TTS_MESSAGES', + * 'MANAGE_MESSAGES', + * 'EMBED_LINKS', + * 'ATTACH_FILES', + * 'READ_MESSAGE_HISTORY', + * 'MENTION_EVERYONE', + * 'EXTERNAL_EMOJIS', // use external emojis + * 'CONNECT', // connect to voice + * 'SPEAK', // speak on voice + * 'MUTE_MEMBERS', // globally mute members on voice + * 'DEAFEN_MEMBERS', // globally deafen members on voice + * 'MOVE_MEMBERS', // move member's voice channels + * 'USE_VAD', // use voice activity detection + * 'CHANGE_NICKNAME', + * 'MANAGE_NICKNAMES', // change nicknames of others + * 'MANAGE_ROLES_OR_PERMISSIONS', + * 'MANAGE_WEBHOOKS', + * 'MANAGE_EMOJIS' * ] * ``` * @typedef {string|number} PermissionResolvable @@ -317,6 +317,67 @@ class ClientDataResolver { } return null; } + + /** + * Can be a Hex Literal, Hex String, Number, RGB Array, or one of the following + * ``` + * [ + * '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', + * ] + * ``` + * or something like + * ``` + * [255, 0, 255] + * ``` + * for purple + * @typedef {String|number|Array} ColorResolvable + */ + + /** + * @param {ColorResolvable} color Color to resolve + * @returns {number} A color + */ + static resolveColor(color) { + if (typeof color === 'string') { + color = Constants.Colors[color] || parseInt(color.replace('#', ''), 16); + } else if (color instanceof Array) { + color = (color[0] << 16) + (color[1] << 8) + color[2]; + } + if (color < 0 || color > 0xFFFFFF) { + throw new RangeError('Color must be within the range 0 - 16777215 (0xFFFFFF).'); + } else if (color && isNaN(color)) { + throw new TypeError('Unable to convert color to a number.'); + } + return color; + } + + /** + * @param {ColorResolvable} color Color to resolve + * @returns {number} A color + */ + resolveColor(color) { + return ClientDataResolver.resolveColor(color); + } } module.exports = ClientDataResolver; diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index e0e91366c..414959582 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -459,10 +459,7 @@ class RESTMethods { const data = {}; data.name = _data.name || role.name; data.position = typeof _data.position !== 'undefined' ? _data.position : role.position; - data.color = _data.color || role.color; - if (typeof data.color === 'string' && data.color.startsWith('#')) { - data.color = parseInt(data.color.replace('#', ''), 16); - } + data.color = this.client.resolver.resolveColor(_data.color || role.color); data.hoist = typeof _data.hoist !== 'undefined' ? _data.hoist : role.hoist; data.mentionable = typeof _data.mentionable !== 'undefined' ? _data.mentionable : role.mentionable; diff --git a/src/structures/RichEmbed.js b/src/structures/RichEmbed.js index 268fa31d8..c8c504fb1 100644 --- a/src/structures/RichEmbed.js +++ b/src/structures/RichEmbed.js @@ -1,3 +1,5 @@ +const ClientDataResolver = require('../client/ClientDataResolver'); + /** * A rich embed to be sent with a message with a fluent interface for creation * @param {Object} [data] Data to set in the rich embed @@ -101,24 +103,11 @@ class RichEmbed { /** * Sets the color of this embed - * @param {string|number|number[]} color The color to set + * @param {ColorResolvable} color The color to set * @returns {RichEmbed} This embed */ setColor(color) { - let radix = 10; - if (color instanceof Array) { - color = (color[0] << 16) + (color[1] << 8) + color[2]; - } else if (typeof color === 'string' && color.startsWith('#')) { - radix = 16; - color = color.replace('#', ''); - } - color = parseInt(color, radix); - if (color < 0 || color > 0xFFFFFF) { - throw new RangeError('RichEmbed color must be within the range 0 - 16777215 (0xFFFFFF).'); - } else if (color && isNaN(color)) { - throw new TypeError('Unable to convert RichEmbed color to a number.'); - } - this.color = color; + this.color = ClientDataResolver.resolveColor(color); return this; } diff --git a/src/structures/Role.js b/src/structures/Role.js index 329a2264c..92759b552 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -180,7 +180,7 @@ class Role { * The data for a role * @typedef {Object} RoleData * @property {string} [name] The name of the role - * @property {number|string} [color] The color of the role, either a hex string or a base 10 number + * @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number * @property {boolean} [hoist] Whether or not the role should be hoisted * @property {number} [position] The position of the role * @property {string[]} [permissions] The permissions of the role diff --git a/src/util/Constants.js b/src/util/Constants.js index bb02b3705..5d9d7fecc 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -368,6 +368,34 @@ const PermissionFlags = exports.PermissionFlags = { MANAGE_EMOJIS: 1 << 30, }; +exports.Colors = { + DEFAULT: 0x000000, + AQUA: 0x1ABC9C, + GREEN: 0x2ECC71, + BLUE: 0x3498DB, + PURPLE: 0x9B59B6, + GOLD: 0xF1C40F, + ORANGE: 0xE67E22, + RED: 0xE74C3C, + GREY: 0x95A5A6, + NAVY: 0x34495E, + DARK_AQUA: 0x11806A, + DARK_GREEN: 0x1F8B4C, + DARK_BLUE: 0x206694, + DARK_PURPLE: 0x71368A, + DARK_GOLD: 0xC27C0E, + DARK_ORANGE: 0xA84300, + DARK_RED: 0x992D22, + DARK_GREY: 0x979C9F, + DARKER_GREY: 0x7F8C8D, + LIGHT_GREY: 0xBCC0C0, + DARK_NAVY: 0x2C3E50, + BLURPLE: 0x7289DA, + GREYPLE: 0x99AAB5, + DARK_BUT_NOT_BLACK: 0x2C2F33, + NOT_QUITE_BLACK: 0x23272A, +}; + let _ALL_PERMISSIONS = 0; for (const key in PermissionFlags) _ALL_PERMISSIONS |= PermissionFlags[key]; exports.ALL_PERMISSIONS = _ALL_PERMISSIONS; From 74fd0421baf8b516f3e2f5143f71521e8086906a Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 13 Jan 2017 00:18:00 -0500 Subject: [PATCH 15/16] Add missing descriptions --- src/structures/interface/TextBasedChannel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js index 74659afac..c3e7c93e0 100644 --- a/src/structures/interface/TextBasedChannel.js +++ b/src/structures/interface/TextBasedChannel.js @@ -40,8 +40,8 @@ class TextBasedChannel { /** * @typedef {Object} FileOptions - * @property {BufferResolvable} attachment - * @property {string} [name='file.jpg'] + * @property {BufferResolvable} attachment File to attach + * @property {string} [name='file.jpg'] Filename of the attachment */ /** From 5ac410f35296dab3cb8ca6b6c9e4a181c2eccb20 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 13 Jan 2017 12:48:12 -0600 Subject: [PATCH 16/16] Cleanup webhooks (#1094) * clean up webhooks and fix sending messages with webhooks * whoops * fix up options * Update Webhook.js * Update Webhook.js * Update Webhook.js * fix docstring --- src/structures/Webhook.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 89aa89d34..dc6f50fff 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -76,7 +76,7 @@ class Webhook { * @property {string} [avatarURL] Avatar URL override for the message * @property {boolean} [tts=false] Whether or not the message should be spoken aloud * @property {string} [nonce=''] The nonce for the message - * @property {Object} [embed] An embed for the message + * @property {Object[]} [embeds] An array of embeds for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here * should be replaced with plain-text