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 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/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/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() { diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 5ecae4610..414959582 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'); @@ -45,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 => { @@ -88,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); @@ -123,11 +150,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) { @@ -436,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; @@ -565,7 +585,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)) { @@ -573,13 +594,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/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/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() { 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 })); } /** diff --git a/src/structures/RichEmbed.js b/src/structures/RichEmbed.js index fbd9383d3..c8c504fb1 100644 --- a/src/structures/RichEmbed.js +++ b/src/structures/RichEmbed.js @@ -1,5 +1,7 @@ +const ClientDataResolver = require('../client/ClientDataResolver'); + /** - * 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 { @@ -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/structures/Webhook.js b/src/structures/Webhook.js index 153d8f3a5..dc6f50fff 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[]} [embeds] An array of embeds for the message + * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) + * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here * should be replaced with plain-text + * @property {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 diff --git a/src/structures/interface/TextBasedChannel.js b/src/structures/interface/TextBasedChannel.js index 829eef683..ebd8f67bc 100644 --- a/src/structures/interface/TextBasedChannel.js +++ b/src/structures/interface/TextBasedChannel.js @@ -23,11 +23,11 @@ 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 - * @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 @@ -35,12 +35,13 @@ 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) */ /** * @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 */ /** @@ -70,6 +71,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 +83,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 +91,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 +118,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) { 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) { diff --git a/src/util/Constants.js b/src/util/Constants.js index dc80da5aa..5d9d7fecc 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.', }; @@ -367,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; diff --git a/typings b/typings index c8b3f8b89..14c4b674c 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit c8b3f8b8931d1318f1143ca26574ae1f9b4c5aa2 +Subproject commit 14c4b674cfab537277e80ce57b8b68717e4055d1